//========================================================================== // dRICh: Dual Ring Imaging Cherenkov Detector //-------------------------------------------------------------------------- // // Author: Christopher Dilks (Duke University) // // - Design Adapted from Standalone Fun4all and GEMC implementations // [ Evaristo Cisbani, Cristiano Fanelli, Alessio Del Dotto, et al. ] // //========================================================================== #include <XML/Helper.h> #include "TMath.h" #include "TString.h" #include "GeometryHelpers.h" #include "Math/Point2D.h" #include "DDRec/Surface.h" #include "DDRec/DetectorData.h" #include "DD4hep/OpticalSurfaces.h" #include "DD4hep/DetFactoryHelper.h" #include "DD4hep/Printout.h" using namespace std; using namespace dd4hep; using namespace dd4hep::rec; // create the detector static Ref_t createDetector(Detector& desc, xml::Handle_t handle, SensitiveDetector sens) { xml::DetElement detElem = handle; std::string detName = detElem.nameStr(); int detID = detElem.id(); DetElement det(detName, detID); xml::Component dims = detElem.dimensions(); OpticalSurfaceManager surfMgr = desc.surfaceManager(); // attributes ----------------------------------------------------------- // TODO [low priority]: some attributes have default values, some do not; // make this more consistent // - vessel double vesselZ0 = dims.attr<double>(_Unicode(z0)); double vesselLength = dims.attr<double>(_Unicode(length)); double vesselRmin0 = dims.attr<double>(_Unicode(rmin0)); double vesselRmin1 = dims.attr<double>(_Unicode(rmin1)); double vesselRmax0 = dims.attr<double>(_Unicode(rmax0)); double vesselRmax1 = dims.attr<double>(_Unicode(rmax1)); double vesselRmax2 = dims.attr<double>(_Unicode(rmax2)); double snoutLength = dims.attr<double>(_Unicode(snout_length)); int nSectors = getAttrOrDefault<int>(dims, _Unicode(nsectors), 6); double wallThickness = getAttrOrDefault<double>(dims, _Unicode(wall_thickness), 0.5*cm); double windowThickness = getAttrOrDefault<double>(dims, _Unicode(window_thickness), 0.1*cm); auto vesselMat = desc.material(getAttrOrDefault(detElem, _Unicode(material), "Aluminum")); auto gasvolMat = desc.material(getAttrOrDefault(detElem, _Unicode(gas), "AirOptical")); auto vesselVis = desc.visAttributes(detElem.attr<std::string>(_Unicode(vis_vessel))); auto gasvolVis = desc.visAttributes(detElem.attr<std::string>(_Unicode(vis_gas))); // - radiator (applies to aerogel and filter) auto radiatorElem = detElem.child(_Unicode(radiator)); double radiatorRmin = getAttrOrDefault<double>(radiatorElem, _Unicode(rmin), 10.*cm); double radiatorRmax = getAttrOrDefault<double>(radiatorElem, _Unicode(rmax), 80.*cm); double radiatorPhiw = getAttrOrDefault<double>(radiatorElem, _Unicode(phiw), 2*M_PI/nSectors); double radiatorPitch = getAttrOrDefault<double>(radiatorElem, _Unicode(pitch), 0.*degree); double radiatorFrontplane = getAttrOrDefault<double>(radiatorElem, _Unicode(frontplane), 2.5*cm); // - aerogel auto aerogelElem = radiatorElem.child(_Unicode(aerogel)); auto aerogelMat = desc.material(aerogelElem.attr<std::string>(_Unicode(material))); auto aerogelVis = desc.visAttributes(aerogelElem.attr<std::string>(_Unicode(vis))); double aerogelThickness = getAttrOrDefault<double>(aerogelElem, _Unicode(thickness), 2.5*cm); // - filter auto filterElem = radiatorElem.child(_Unicode(filter)); auto filterMat = desc.material(filterElem.attr<std::string>(_Unicode(material))); auto filterVis = desc.visAttributes(filterElem.attr<std::string>(_Unicode(vis))); double filterThickness = getAttrOrDefault<double>(filterElem, _Unicode(thickness), 2.5*cm); // - mirror auto mirrorElem = detElem.child(_Unicode(mirror)); auto mirrorMat = desc.material(mirrorElem.attr<std::string>(_Unicode(material))); auto mirrorVis = desc.visAttributes(mirrorElem.attr<std::string>(_Unicode(vis))); auto mirrorSurf = surfMgr.opticalSurface(getAttrOrDefault(mirrorElem, _Unicode(surface), "MirrorOpticalSurface")); double mirrorBackplane = getAttrOrDefault<double>(mirrorElem, _Unicode(backplane), 240.*cm); double mirrorThickness = getAttrOrDefault<double>(mirrorElem, _Unicode(thickness), 2.*mm); double mirrorRadius = getAttrOrDefault<double>(mirrorElem, _Unicode(radius), 190*cm); double mirrorCenterX = getAttrOrDefault<double>(mirrorElem, _Unicode(centerx), 95*cm); double mirrorRmin = getAttrOrDefault<double>(mirrorElem, _Unicode(rmin), 10.*cm); double mirrorRmax = getAttrOrDefault<double>(mirrorElem, _Unicode(rmax), 150.*cm); double mirrorPhiw = getAttrOrDefault<double>(mirrorElem, _Unicode(phiw), 2*M_PI/nSectors); int mirrorDebug = getAttrOrDefault<int>(mirrorElem, _Unicode(debug), 0); // - sensor module auto sensorElem = detElem.child(_Unicode(sensors)).child(_Unicode(module)); auto sensorMat = desc.material(sensorElem.attr<std::string>(_Unicode(material))); auto sensorVis = desc.visAttributes(sensorElem.attr<std::string>(_Unicode(vis))); auto sensorSurf = surfMgr.opticalSurface(getAttrOrDefault(sensorElem, _Unicode(surface), "MirrorOpticalSurface")); double sensorSide = sensorElem.attr<double>(_Unicode(side)); double sensorGap = sensorElem.attr<double>(_Unicode(gap)); double sensorThickness = sensorElem.attr<double>(_Unicode(thickness)); // - sensor sphere auto sensorSphElem = detElem.child(_Unicode(sensors)).child(_Unicode(sphere)); double sensorSphRadius = sensorSphElem.attr<double>(_Unicode(radius)); double sensorSphCenterX = sensorSphElem.attr<double>(_Unicode(centerx)); double sensorSphCenterY = sensorSphElem.attr<double>(_Unicode(centery)); double sensorSphCenterZ = sensorSphElem.attr<double>(_Unicode(centerz)); int sensorSphDebug = getAttrOrDefault<int>(sensorSphElem, _Unicode(debug), 0); // - sensor sphere patch cuts auto sensorSphPatchElem = detElem.child(_Unicode(sensors)).child(_Unicode(sphericalpatch)); double sensorSphPatchThetaMin = sensorSphPatchElem.attr<double>(_Unicode(thetamin)); double sensorSphPatchThetaMax = sensorSphPatchElem.attr<double>(_Unicode(thetamax)); double sensorSphPatchWidthFactor = sensorSphPatchElem.attr<double>(_Unicode(widthfactor)); double sensorSphPatchTaper = sensorSphPatchElem.attr<double>(_Unicode(taper)); // BUILD VESSEL ==================================================================== /* - `vessel`: aluminum enclosure, the mother volume of the dRICh * - `gasvol`: gas volume, which fills `vessel`; all other volumes defined below * are children of `gasvol` * - the dRICh vessel geometry has two regions: the snout refers to the conic region * in the front, housing the aerogel, while the tank refers to the cylindrical * region, housing the rest of the detector components */ // snout solids double boreDelta = vesselRmin1 - vesselRmin0; Cone vesselSnout( snoutLength/2.0, vesselRmin0, vesselRmax0, vesselRmin0 + boreDelta * snoutLength / vesselLength, vesselRmax1 ); Cone gasvolSnout( /* note: `gasvolSnout` extends a bit into the tank, so it touches `gasvolTank` * - the extension distance is equal to the tank `windowThickness`, so the * length of `gasvolSnout` == length of `vesselSnout` * - the extension backplane radius is calculated using similar triangles */ snoutLength/2.0, vesselRmin0 + wallThickness, vesselRmax0 - wallThickness, vesselRmin0 + boreDelta * (snoutLength-windowThickness) / vesselLength + wallThickness, vesselRmax1 - wallThickness + windowThickness * (vesselRmax1 - vesselRmax0) / snoutLength ); // tank solids Cone vesselTank( (vesselLength - snoutLength)/2.0, vesselSnout.rMin2(), vesselRmax2, vesselRmin1, vesselRmax2 ); Cone gasvolTank( (vesselLength - snoutLength)/2.0 - windowThickness, gasvolSnout.rMin2(), vesselRmax2 - wallThickness, vesselRmin1 + wallThickness, vesselRmax2 - wallThickness ); // snout + tank solids UnionSolid vesselSolid( vesselTank, vesselSnout, Position(0., 0., -vesselLength/2.) ); UnionSolid gasvolSolid( gasvolTank, gasvolSnout, Position(0., 0., -vesselLength/2. + windowThickness) ); // volumes Volume vesselVol(detName, vesselSolid, vesselMat); Volume gasvolVol(detName + "_gas", gasvolSolid, gasvolMat); vesselVol.setVisAttributes(vesselVis); gasvolVol.setVisAttributes(gasvolVis); // reference position auto snoutFront = Position(0., 0., -(vesselLength + snoutLength)/2.); // sensitive detector type sens.setType("photoncounter"); // SECTOR LOOP ////////////////////////////////// for(int isec=0; isec<nSectors; isec++) { if(mirrorDebug*isec>0 || sensorSphDebug*isec>0) continue; // if debugging, draw only 1 sector double sectorRotation = isec * 360/nSectors * degree; // sector rotation about z axis // BUILD RADIATOR ==================================================================== // derived attributes auto radiatorPos = Position(0., 0., radiatorFrontplane) + snoutFront; // solid and volume: create aerogel and filter sectors Tube aerogelSolid(radiatorRmin, radiatorRmax, aerogelThickness/2, -radiatorPhiw/2.0, radiatorPhiw/2.0); Tube filterSolid( radiatorRmin, radiatorRmax, filterThickness/2, -radiatorPhiw/2.0, radiatorPhiw/2.0); Volume aerogelVol("aerogel_v", aerogelSolid, aerogelMat); Volume filterVol( "filter_v", filterSolid, filterMat); aerogelVol.setVisAttributes(aerogelVis); filterVol.setVisAttributes(filterVis); // placement auto aerogelPV = gasvolVol.placeVolume(aerogelVol, RotationZ(sectorRotation) // rotate about beam axis to sector * Translation3D(radiatorPos.x(), radiatorPos.y(), radiatorPos.z()) // re-center to snoutFront * RotationY(radiatorPitch) // change polar angle to specified pitch ); auto filterPV = gasvolVol.placeVolume(filterVol, RotationZ(sectorRotation) // rotate about beam axis to sector * Translation3D(radiatorPos.x(), radiatorPos.y(), radiatorPos.z()) // re-center to snoutFront * RotationY(radiatorPitch) // change polar angle * Translation3D(0., 0., (aerogelThickness+filterThickness)/2.) // move to aerogel backplane ); // properties // TODO [critical]: define skin properties for aerogel and filter DetElement aerogelDE(det, Form("aerogel_de%d", isec), isec); aerogelDE.setPlacement(aerogelPV); //SkinSurface aerogelSkin(desc, aerogelDE, Form("mirror_optical_surface%d", isec), aerogelSurf, aerogelVol); //aerogelSkin.isValid(); DetElement filterDE(det, Form("filter_de%d", isec), isec); filterDE.setPlacement(filterPV); //SkinSurface filterSkin(desc, filterDE, Form("mirror_optical_surface%d", isec), filterSurf, filterVol); //filterSkin.isValid(); // BUILD MIRROR ==================================================================== // derived attributes auto mirrorPos = Position(mirrorCenterX, 0., mirrorBackplane) + snoutFront; double mirrorThetaRot = std::asin(mirrorCenterX/mirrorRadius); double mirrorTheta1 = mirrorThetaRot - std::asin((mirrorCenterX-mirrorRmin)/mirrorRadius); double mirrorTheta2 = mirrorThetaRot + std::asin((mirrorRmax-mirrorCenterX)/mirrorRadius); // if debugging, draw full sphere if(mirrorDebug>0) { mirrorTheta1=0; mirrorTheta2=M_PI; mirrorPhiw=2*M_PI; }; // solid and volume: create sphere at origin, with specified angular limits Sphere mirrorSolid( mirrorRadius-mirrorThickness, mirrorRadius, mirrorTheta1, mirrorTheta2, -mirrorPhiw/2.0, mirrorPhiw/2.0 ); Volume mirrorVol("mirror_v", mirrorSolid, mirrorMat); mirrorVol.setVisAttributes(mirrorVis); // placement (note: transformations are in reverse order) auto mirrorPV = gasvolVol.placeVolume(mirrorVol, RotationZ(sectorRotation) // rotate about beam axis to sector * Translation3D(0,0,-mirrorRadius) // move longitudinally so it intersects snoutFront * Translation3D(mirrorPos.x(), mirrorPos.y(), mirrorPos.z()) // re-center to snoutFront * RotationY(-mirrorThetaRot) // rotate about origin ); // properties DetElement mirrorDE(det, Form("mirror_de%d", isec), isec); mirrorDE.setPlacement(mirrorPV); SkinSurface mirrorSkin(desc, mirrorDE, Form("mirror_optical_surface%d", isec), mirrorSurf, mirrorVol); mirrorSkin.isValid(); // BUILD SENSORS ==================================================================== // if debugging sphere properties, restrict number of sensors drawn if(sensorSphDebug>0) { sensorSide = 2*M_PI*sensorSphRadius / 64; }; // solid and volume: single sensor module Box sensorSolid(sensorSide/2., sensorSide/2., sensorThickness/2.); Volume sensorVol("sensor_v", sensorSolid, sensorMat); sensorVol.setVisAttributes(sensorVis); // sensitivity sensorVol.setSensitiveDetector(sens); // SENSOR MODULE LOOP ------------------------ /* ALGORITHM: generate sphere of positions * - NOTE: there are two coordinate systems here: * - "global" the main ATHENA coordinate system * - "generator" (vars end in `Gen`) is a local coordinate system for * generating points on a sphere; it is related to the global system by * a rotation; we do this so the "patch" (subset of generated * positions) of sensors we choose to build is near the equator, where * point distribution is more uniform * - PROCEDURE: loop over `thetaGen`, with subloop over `phiGen`, each divided evenly * - the number of points to generate depends how many sensors (+`sensorGap`) * can fit within each ring of constant `thetaGen` or `phiGen` * - we divide the relevant circumference by the sensor * size(+`sensorGap`), and this number is allowed to be a fraction, * because likely we don't care about generating a full sphere and * don't mind a "seam" at the overlap point * - if we pick a patch of the sphere near the equator, and not near * the poles or seam, the sensor distribution will appear uniform */ // initialize module number for this sector int imod=1; // thetaGen loop: iterate less than "0.5 circumference / sensor size" times double nTheta = M_PI*sensorSphRadius / (sensorSide+sensorGap); for(int t=0; t<(int)(nTheta+0.5); t++) { double thetaGen = t/((double)nTheta) * M_PI; // phiGen loop: iterate less than "circumference at this latitude / sensor size" times double nPhi = 2*M_PI * sensorSphRadius * std::sin(thetaGen) / (sensorSide+sensorGap); for(int p=0; p<(int)(nPhi+0.5); p++) { double phiGen = p/((double)nPhi) * 2*M_PI - M_PI; // shift to [-pi,pi] // determine global phi and theta // - convert {radius,thetaGen,phiGen} -> {xGen,yGen,zGen} double xGen = sensorSphRadius * std::sin(thetaGen) * std::cos(phiGen); double yGen = sensorSphRadius * std::sin(thetaGen) * std::sin(phiGen); double zGen = sensorSphRadius * std::cos(thetaGen); // - convert {xGen,yGen,zGen} -> global {x,y,z} via rotation double x = zGen; double y = xGen; double z = yGen; // - convert global {x,y,z} -> global {phi,theta} double phi = std::atan2(y,x); double theta = std::acos(z/sensorSphRadius); // cut spherical patch // TODO [low priority]: instead of cutting a patch with complicated parameters, // can we use something simpler such as Union or // Intersection of solids? // - applied on global coordinates // - theta cuts are signed, since we will offset the patch along +x; // from the offset position, toward barrel is positive, toward beam is negative double thetaSigned = (x<0?-1:1) * theta; // - position of yz planes, associated to theta cuts double xmin = sensorSphRadius * std::sin(sensorSphPatchThetaMin/2); double xmax = sensorSphRadius * std::sin(sensorSphPatchThetaMax/2); // - we want a phi wedge, but offset from the origin to allow more width, so // define phiCheck to account for the offset; the amount of the offset, // and hence the width, is controlled by `sensorSphPatchWidthFactor` double phiCheck = std::atan2(y,(x+sensorSphCenterX)/sensorSphPatchWidthFactor); // - apply cuts (only if not debugging) // - NOTE: use `x<xmax` for straight cuts, or `theta<sensorSphPatchThetaMax` for // rounded cuts (which allows for more sensors) if( ( std::fabs(phiCheck)<sensorSphPatchTaper && x>xmin && theta<sensorSphPatchThetaMax && z>0 ) || sensorSphDebug>0 ) { // placement (note: transformations are in reverse order) // - transformations operate on global coordinates; the corresponding // generator coordinates are provided in the comments auto sensorPV = gasvolVol.placeVolume(sensorVol, RotationZ(sectorRotation) // rotate about beam axis to sector * Translation3D(sensorSphCenterX, sensorSphCenterY, sensorSphCenterZ) // move sphere to specified center * Translation3D(snoutFront.x(), snoutFront.y(), snoutFront.z()) // move sphere to reference position * RotationX(phiGen) // rotate about `zGen` * RotationZ(thetaGen) // rotate about `yGen` * Translation3D(sensorSphRadius, 0., 0.) // push radially to spherical surface * RotationY(M_PI/2) // rotate sensor to be compatible with generator coords ); // properties sensorPV.addPhysVolID("sector", isec).addPhysVolID("module", imod); DetElement sensorDE(det, Form("sensor_de%d_%d", isec, imod), 10000*isec+imod); sensorDE.setPlacement(sensorPV); SkinSurface sensorSkin(desc, sensorDE, Form("sensor_optical_surface%d", isec), sensorSurf, sensorVol); sensorSkin.isValid(); // increment sensor module number imod++; }; // end patch cuts }; // end phiGen loop }; // end thetaGen loop // END SENSOR MODULE LOOP ------------------------ }; // END SECTOR LOOP ////////////////////////// // place gas volume PlacedVolume gasvolPV = vesselVol.placeVolume(gasvolVol,Position(0, 0, 0)); DetElement gasvolDE(det, "gasvol_de", 0); gasvolDE.setPlacement(gasvolPV); // place mother volume (vessel) Volume motherVol = desc.pickMotherVolume(det); PlacedVolume vesselPV = motherVol.placeVolume(vesselVol, Position(0, 0, vesselZ0) - snoutFront ); vesselPV.addPhysVolID("system", detID); det.setPlacement(vesselPV); return det; }; // clang-format off DECLARE_DETELEMENT(athena_DRICH, createDetector)