From b77649d258dfd29dca13f6a509b0f0e16d8de8ac Mon Sep 17 00:00:00 2001 From: Berkin Ulukutlu Date: Tue, 28 Jul 2026 22:37:35 +0200 Subject: [PATCH] ALICE3 TRK add the simplified-realistic OT barrel layout Adds `TRKBase.layoutMLOT=kSimplifiedRealistic`: the ML is unchanged from kSegmented, but the OT barrel is built from the parts it is actually made of instead of solid silicon slabs. Geometry follows ALICE3 OT WP1 Material (26.06.2025); all tunable dimensions are in constants::OT in Specs.h. Module: cold plate, eight pure-silicon chips (2 in phi x 4 in z, read-out edges facing the module edges), FPC, one ZIF connector, SMD capacitors over the chip footprints and two mounting brackets, stacked flush about the chip mid-plane. Stave: two module rows overlapping in phi and staggered in r, with a cooling pipe between them and an end-of-stave FR4 readout card past the last module. Barrel: each layer is four quarter barrels, cut at mid-rapidity and on the vertical plane so the region of the beam-pipe supports stays free of staves. The stave count is no longer hard-coded; it is the smallest even number that still leaves rowActiveOverlap of active double coverage at the layer radius. The OT radii (440/615/793 mm) are chosen to improve overlap uniformity, giving 30/42/54 staves per ring with 1.5-1.9 mm ofoverlap, and keeping the outermost stave envelope clear of the OT shell. Carbon fibre separation walls close the quarter barrels azimuthally and at mid-rapidity (TRKServices::createOTBarrelWalls, thickness configurable via TRKBase.otBarrelWallThickness). The layer envelopes are slotted along both cuts so the walls run continuously in r rather than being interrupted at every layer. Support half-rings carry the stave space frames. New passive materials: carbon fibre, FPC (Kapton+Cu), LCP+Cu, BaTiO3, PEEK, FR4 and copper. Also needed to make the layout work end to end: - ProcessHits resolves stave/half-stave/module from the TGeo path, since the realistic OT builds them as assembly volumes whose CurrentVolOffID copy numbers all read 0; - ClustererSpec passed a file name where loadGeometry expects a prefix, so the clusterer could not open the geometry for any layout; - CheckDigitsTRK split VD/ML/OT on hard-coded chip indices that no longer matched the layout; they are now read from GeometryTGeo. --- Detectors/Upgrades/ALICE3/TRK/README.md | 34 +- .../ALICE3/TRK/base/include/TRKBase/Specs.h | 69 +++ .../TRK/base/include/TRKBase/TRKBaseParam.h | 4 +- .../ALICE3/TRK/macros/test/CheckDigitsTRK.C | 78 ++- .../include/TRKSimulation/TRKLayer.h | 39 +- .../include/TRKSimulation/TRKServices.h | 7 + .../ALICE3/TRK/simulation/src/Detector.cxx | 109 +++- .../ALICE3/TRK/simulation/src/TRKLayer.cxx | 482 +++++++++++++++--- .../ALICE3/TRK/simulation/src/TRKServices.cxx | 79 ++- .../TRK/simulation/src/TRKSimulationLinkDef.h | 1 + .../ALICE3/TRK/workflow/src/ClustererSpec.cxx | 2 +- 11 files changed, 802 insertions(+), 102 deletions(-) diff --git a/Detectors/Upgrades/ALICE3/TRK/README.md b/Detectors/Upgrades/ALICE3/TRK/README.md index efe07ab092eb2..2afd61d66eb46 100644 --- a/Detectors/Upgrades/ALICE3/TRK/README.md +++ b/Detectors/Upgrades/ALICE3/TRK/README.md @@ -15,8 +15,9 @@ Configurables for various sub-detectors are presented in the following Table: | Subsystem | Available options | Comments | | ------------------ | ------------------------------------------------------- | ---------------------------------------------------------------- | | `TRKBase.layoutVD` | `kIRIS4` (default), `kIRISFullCyl`, `kIRIS5`, `kIRIS4a` | [link to definitions](./base/include/TRKBase/TRKBaseParam.h) | -| `TRKBase.layoutMLOT` | `kCylindrical`, `kSegmented` (default) | `kSegmented` produces a Turbo layout for ML and a Staggered layout for OT | +| `TRKBase.layoutMLOT` | `kCylindrical`, `kSegmented` (default), `kSimplifiedRealistic` | `kCylindrical`: simple silicon tubes. `kSegmented`: Turbo ML + solid-module OT. `kSimplifiedRealistic`: same ML as `kSegmented`, but a detailed OT barrel (see below) | | `TRKBase.layoutSRV` | `kPeacockv1` (default), `kLOISymm` | `kLOISymm` produces radially symmetric service volumes, as used in the LoI | +| `TRKBase.otBarrelWallThickness` | thickness in cm (default `0.2`) | Carbon fibre separation walls of the OT quarter barrels — side panels and mid-rapidity disks (`kPeacockv1` + `kSimplifiedRealistic`); `0` disables them. Does not cover the load-bearing outer shell (4 mm) | For example, a geometry with fully cylindrical tracker barrel (for all layers in VD, ML and OT) can be obtained by ```bash @@ -26,7 +27,7 @@ o2-sim-serial-run5 -n 1 -g pythia8hi -m A3IP TRK FT3 TF3 \ ## Custom Geometry Configuration -The geometry of the ML and OT layers can be overridden by providing a custom plain-text configuration file via `TRKBase.configFile=filename.txt`. The parser interprets the file differently depending on the active `TRKBase.layoutMLOT` setting (`kCylindrical` or `kSegmented`). +The geometry of the ML and OT layers can be overridden by providing a custom plain-text configuration file via `TRKBase.configFile=filename.txt`. The parser interprets the file differently depending on the active `TRKBase.layoutMLOT` setting (`kCylindrical`, or `kSegmented`/`kSimplifiedRealistic` which share the same syntax). ### General Syntax Rules * **Separators:** All columns **must** be separated by a single TAB (`\t`). Using spaces will result in a parsing error. @@ -55,9 +56,9 @@ When `TRKBase.layoutMLOT=kCylindrical` is used, each layer requires a minimum of 80.0 255.9 0.1 ``` -### 2. Segmented Layout (`kSegmented`) +### 2. Segmented / Simplified-Realistic Layout (`kSegmented`, `kSimplifiedRealistic`) -When `TRKBase.layoutMLOT=kSegmented` is used, each layer requires a minimum of 5 base parameters to define the geometry. The parser distinguishes between Middle Layers (ML) and Outer Layers (OT) based on the sequential layer index. +Both layouts use the same configuration-file syntax (only the OT geometry implementation differs). Each layer requires a minimum of 5 base parameters to define the geometry. The parser distinguishes between Middle Layers (ML) and Outer Layers (OT) based on the sequential layer index. * *(Note: The 5 base parameters map directly to: Inner Radius (`rInn`), Thickness (`thick`), Tilt Angle (`tiltAngle`), Number of Staves (`nStaves`), and Number of Modules per stave (`nMods`)).* @@ -66,8 +67,9 @@ The first 5 valid lines are parsed as `TRKMLLayer` objects. These layers **requi * **Format:** `rInn` \t `thick` \t `tiltAngle` \t `nStaves` \t `nMods` \t `stagOffset` \t `[optional_mode]` **Outer Layers (OT) - Indices 5 and above** -From the 6th valid line onwards, lines are parsed as `TRKOTLayer` objects. These layers do **not** have a staggering offset. The optional mode parameter shifts to the 6th column. +From the 6th valid line onwards, lines are parsed as OT layer objects (`TRKOTLayer` for `kSegmented`, `TRKOTLayerRealistic` for `kSimplifiedRealistic`). These layers do **not** have a staggering offset. The optional mode parameter shifts to the 6th column. * **Format:** `rInn` \t `thick` \t `tiltAngle` \t `nStaves` \t `nMods` \t `[optional_mode]` +* *(Note: for `kSimplifiedRealistic`, `nStaves` is recomputed internally from the average radius and stave width to guarantee the neighbour overlap; the value in the file is ignored for the OT.)* **Example for `kSegmented`:** @@ -89,5 +91,27 @@ From the 6th valid line onwards, lines are parsed as `TRKOTLayer` objects. These 80.0 0.01 0.0 56 22 1 ``` +## Simplified-Realistic OT geometry (`kSimplifiedRealistic`) + +`kSimplifiedRealistic` keeps the ML layers identical to `kSegmented` but replaces the solid-silicon OT modules with a more detailed, but still simplified, description (`TRKOTLayerRealistic`). All tunable dimensions live in [`Specs.h`](./base/include/TRKBase/Specs.h) (`constants::OT`); values that depend on others are computed in the source. + +The geometry specification is based on [ALICE3 OT WP1 Material (26.06.2025)](https://indico.cern.ch/event/1562183/contributions/6580808/attachments/3093672/5480049/ALICE3_OT_WP1_Material_260625.pdf). + +**Module** — a flush stack about the chip mid-plane: cold plate (carbon fibre), 8 pure-silicon chips (2 in φ × 4 in z, dead zones facing the outer module edges), FPC (Kapton+Cu), one ZIF connector centred on a short edge, SMD capacitors over the chip footprints (skipping any under the connector), and two mounting brackets on the cold plate. + +**Stave** — two module rows overlapping in φ (so each row's dead zone is covered by the other row's sensor) and offset in r by `rowRadialStagger`, plus a cooling pipe between them. The rows straddle the stave frame origin, so a stave placed on the barrel circle is tangent at its own centre: every row-to-row radial step in the barrel — inside a stave and between neighbours — is then the same `rowRadialStagger`, in every layer. + +**End-of-stave card** — one readout PCB per stave (`constants::OT::eosCard`), mounted past the last module at the outer z end, coplanar with the two rows: a 120 × 80 mm, 1.5 mm FR4 board with four copper planes spread symmetrically through the thickness. The cards reach |z| ≈ 141 cm, just short of the OT barrel service disk. + +**Barrel** — each layer is built in **four parts**: two z-half-barrels (±η), each split azimuthally into two 180° halves. Both η half-barrels are cut on the **same vertical plane** (x = 0), so the region where the vertical beam-pipe supports run is free of staves over the whole barrel (the supports themselves are not in the geometry). Neighbouring staves overlap (≥ 1 mm active double-coverage); at the cut plane the two azimuthal halves instead leave a gap wide enough for the separation wall plus `barrelWallClearance` on each side. The stave count is the smallest even number that still meets the required overlap at the layer radius, so the OT radii (440, 615, 793 mm) are chosen at the top of a stave-count band, giving 30/42/54 staves per ring with 1.5-1.9 mm of overlap. The cooling pipe faces the larger radius on the inner two OT layers and the smaller radius on the flipped outer layer. + +**Separation walls** — the quarter barrels are closed on every side but the one carrying the end-of-stave cards (`TRKServices::createOTBarrelWalls`, `kPeacockv1` + `kSimplifiedRealistic`): radially by the ML/OT and outer carbon shells, azimuthally by a rectangular carbon fibre wall in the cut plane, and at mid-rapidity by a half disk at z = 0, one per quarter barrel. + +Both walls run **continuously** from one shell to the other: each OT layer envelope is a tube with a slot cut along the vertical cut plane *and* one at z = 0 (`TGeoCompositeShape`), so the walls pass through the layers instead of being interrupted by them, and what is left of each envelope is the four quarter barrels. The slots clear the walls by `barrelWallSlotMargin`; the staves stay `barrelWallClearance` away from them. + +Only the outer shell is load bearing, so it is the only 4 mm wall; the ML/OT shell is 2 mm and the side panels and mid-rapidity disks default to 2 mm via `TRKBase.otBarrelWallThickness`. + +The OT services (cables/cooling bundles, cold plates) are built by `TRKServices` for all layouts via `TRKBase.layoutSRV`. + diff --git a/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/Specs.h b/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/Specs.h index b484e13f3546e..1943417ab6edc 100644 --- a/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/Specs.h +++ b/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/Specs.h @@ -125,6 +125,75 @@ constexpr double width{halfstave::width * 2}; // wid constexpr double length{halfstave::length}; // length of the stave constexpr int nRows{static_cast(width / moduleMLOT::chip::pitchX)}; // number of rows in the stave constexpr int nCols{static_cast(length / moduleMLOT::chip::pitchZ)}; // number of columns in the stave +constexpr int nModulesPerRow{11}; // modules along z per row +constexpr double interModuleGap{0.2 * mm}; // z-gap between module FPCs + +// Component dimensions of the simplified-realistic OT stave +namespace fpc +{ +constexpr double length{116.8 * mm}; // z-extent +constexpr double width{52.2 * mm}; // phi-extent +constexpr double thickness{0.200 * mm}; // r-extent, Kapton+Cu stack +} // namespace fpc +namespace coldPlate +{ +constexpr double length{116.8 * mm}; // z-extent +constexpr double width{47.2 * mm}; // phi-extent +constexpr double thickness{0.4 * mm}; // r-extent +} // namespace coldPlate +namespace connector +{ +constexpr double width{25.0 * mm}; // phi-extent +constexpr double length{10.0 * mm}; // z-extent +constexpr double thickness{2.0 * mm}; // r-extent +} // namespace connector +namespace capacitor +{ +constexpr double width{1.0 * mm}; // phi-extent +constexpr double length{0.5 * mm}; // z-extent +constexpr double thickness{0.3 * mm}; // r-extent +constexpr int perChip{5}; +} // namespace capacitor +namespace bracket +{ +constexpr double length{10.0 * mm}; // z-extent +constexpr double width{5.0 * mm}; // phi-extent +constexpr double thickness{8.0 * mm}; // r-extent +} // namespace bracket +namespace coolingPipe +{ +constexpr double rInner{0.4 * cm}; +constexpr double rOuter{0.5 * cm}; +constexpr double rLocalOffset{3.5 * cm}; // chip mid-plane to pipe axis, local r +} // namespace coolingPipe +namespace eosCard // end-of-stave readout card, one per stave +{ +constexpr double length{120 * mm}; // z-extent +constexpr double width{80 * mm}; // phi-extent +constexpr double thickness{1.5 * mm}; // r-extent, FR4 + copper planes +constexpr int nCopperLayers{4}; // copper planes, spread over the thickness +constexpr double copperThickness{35 * mu}; // per copper plane (1 oz) +constexpr double zGap{2.0 * mm}; // z-clearance from the last module +} // namespace eosCard + +namespace supportRing // carbon fibre half-rings the stave space frames mount on +{ +constexpr double radialHeight{30 * mm}; // r-extent of the ring cross-section +constexpr double zWidth{12 * mm}; // z-extent of the ring cross-section +constexpr double wallThickness{2 * mm}; // the ring is hollow +constexpr double zClearance{1.0 * mm}; // z-clearance to the nearest wall and to the cooling pipe +} // namespace supportRing + +constexpr double sensorThickness{moduleMLOT::silicon::thickness}; // pure-silicon chip (no metal stack) +constexpr double interChipGap{0.2 * mm}; // gap between chips within a module +constexpr double rowActiveOverlap{1.0 * mm}; // active overlap between the two rows of a stave +constexpr double rowRadialStagger{2.0 * mm}; // radial step between any two overlapping rows +constexpr double halfBarrelChipGap{1.0 * mm}; // gap between the two azimuthal half-barrels +constexpr double barrelWallClearance{1.0 * mm}; // clearance between a separation wall and the nearest stave +constexpr double barrelWallSlotMargin{0.1 * mm}; // clearance between a separation wall and its envelope slot +constexpr double connectorZDepth{3.0 * cm}; // connector inset from module short edge in z +constexpr double bracketZDepth{3.0 * cm}; // bracket inset from cold-plate short edge in z +constexpr double barrelHalvesZGap{0.8 * cm}; // z-gap between the two eta half-barrels } // namespace OT namespace apts /// parameters for the APTS response diff --git a/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/TRKBaseParam.h b/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/TRKBaseParam.h index 65194ad6edfcb..50cc396c8f899 100644 --- a/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/TRKBaseParam.h +++ b/Detectors/Upgrades/ALICE3/TRK/base/include/TRKBase/TRKBaseParam.h @@ -30,6 +30,7 @@ enum eVDLayout { enum eMLOTLayout { kCylindrical = 0, kSegmented, + kSimplifiedRealistic, }; enum eSrvLayout { @@ -39,7 +40,8 @@ enum eSrvLayout { struct TRKBaseParam : public o2::conf::ConfigurableParamHelper { std::string configFile = ""; - float serviceTubeX0 = 0.02f; // X0 Al2O3 + float serviceTubeX0 = 0.02f; // X0 Al2O3 + float otBarrelWallThickness = 0.2f; // cm, carbon fibre separation walls of the OT quarter barrels, 0 disables them bool irisOpen = false; bool includeLowServices = false; diff --git a/Detectors/Upgrades/ALICE3/TRK/macros/test/CheckDigitsTRK.C b/Detectors/Upgrades/ALICE3/TRK/macros/test/CheckDigitsTRK.C index 400457fc98585..bd5ff7367308d 100644 --- a/Detectors/Upgrades/ALICE3/TRK/macros/test/CheckDigitsTRK.C +++ b/Detectors/Upgrades/ALICE3/TRK/macros/test/CheckDigitsTRK.C @@ -75,7 +75,7 @@ void addTLines(float pitch) gPad->Update(); } -void CheckDigits(std::string digifile = "trkdigits.root", std::string hitfile = "o2sim_HitsTRK.root", std::string inputGeom = "o2sim_geometry.root") +void CheckDigitsTRK(std::string digifile = "trkdigits.root", std::string hitfile = "o2sim_HitsTRK.root", std::string inputGeom = "o2sim_geometry.root") { gStyle->SetPalette(55); @@ -101,6 +101,27 @@ void CheckDigits(std::string digifile = "trkdigits.root", std::string hitfile = const int nMLOTLayers = gman->getNumberOfLayersMLOT(); const int nTotalLayers = nVDLayers + nMLOTLayers; + // Chip-ID range of each sub-detector, taken from the geometry: these boundaries + // move with every layout change, so they must not be written into the cuts. + const int nChips = gman->getNumberOfChips(); + const int firstOTLayer = nTotalLayers - o2::trk::constants::OT::nLayers; + int firstChipML = nChips, firstChipOT = nChips; + for (int i = 0; i < nChips; ++i) { + const int l = gman->getLayerTRK(i); + if (l >= nVDLayers && i < firstChipML) { + firstChipML = i; + } + if (l >= firstOTLayer) { + firstChipOT = i; + break; + } + } + const TString cutVD = TString::Format("id < %d", firstChipML); + const TString cutML = TString::Format("id >= %d && id < %d", firstChipML, firstChipOT); + const TString cutOT = TString::Format("id >= %d", firstChipOT); + Info("CheckDigitsTRK", "%d chips: VD 0-%d, ML %d-%d, OT %d-%d", + nChips, firstChipML - 1, firstChipML, firstChipOT - 1, firstChipOT, nChips - 1); + SegmentationChip seg; // seg.Print(); @@ -301,39 +322,46 @@ void CheckDigits(std::string digifile = "trkdigits.root", std::string hitfile = auto canvXY = new TCanvas("canvXY", "", 1600, 2400); canvXY->Divide(2, 3); canvXY->cd(1); - nt->Draw("y:x >>h_y_vs_x_VD(1000, -3, 3, 1000, -3, 3)", "id < 12 ", "colz"); + nt->Draw("y:x >>h_y_vs_x_VD(1000, -3, 3, 1000, -3, 3)", cutVD, "colz"); canvXY->cd(2); - nt->Draw("y:z>>h_y_vs_z_VD(1000, -26, 26, 1000, -3, 3)", "id < 12 ", "colz"); + nt->Draw("y:z>>h_y_vs_z_VD(1000, -26, 26, 1000, -3, 3)", cutVD, "colz"); canvXY->cd(3); - nt->Draw("y:x>>h_y_vs_x_ML(1000, -25, 25, 1000, -25, 25)", "id >= 12 && id < 5132 ", "colz"); + nt->Draw("y:x>>h_y_vs_x_ML(1000, -25, 25, 1000, -25, 25)", cutML, "colz"); canvXY->cd(4); - nt->Draw("y:z>>h_y_vs_z_ML(1000, -70, 70, 1000, -25, 25)", "id >= 12 && id < 5132 ", "colz"); + nt->Draw("y:z>>h_y_vs_z_ML(1000, -70, 70, 1000, -25, 25)", cutML, "colz"); canvXY->cd(5); - nt->Draw("y:x>>h_y_vs_x_OT(1000, -85, 85, 1000, -85, 85)", "id >= 5132 ", "colz"); + nt->Draw("y:x>>h_y_vs_x_OT(1000, -85, 85, 1000, -85, 85)", cutOT + " && z > 0", "colz"); canvXY->cd(6); - nt->Draw("y:z>>h_y_vs_z_OT(1000, -85, 85, 1000, -130, 130)", "id >= 5132 ", "colz"); + nt->Draw("y:z>>h_y_vs_z_OT(1000, -85, 85, 1000, -130, 130)", cutOT, "colz"); canvXY->SaveAs("trkdigits_y_vs_x_vs_z.pdf"); + auto canvXY_OT = new TCanvas("canvXY_OT", "", 4000, 2000); + canvXY_OT->Divide(2, 1); + canvXY_OT->cd(1); + nt->Draw("y:x>>h_y_vs_x_OT_pos(2000, -85, 85, 2000, -85, 85)", cutOT + " && z > 0", "colz"); + canvXY_OT->cd(2); + nt->Draw("y:x>>h_y_vs_x_OT_neg(2000, -85, 85, 2000, -85, 85)", cutOT + " && z < 0", "colz"); + canvXY_OT->SaveAs("trkdigits_y_vs_x_OT.pdf"); // z distributions auto canvZ = new TCanvas("canvZ", "", 800, 2400); canvZ->Divide(1, 3); canvZ->cd(1); - nt->Draw("z>>h_z_VD(500, -26, 26)", "id < 12 "); + nt->Draw("z>>h_z_VD(500, -26, 26)", cutVD); canvZ->cd(2); - nt->Draw("z>>h_z_ML(500, -70, 70)", "id >= 12 && id < 5132 "); + nt->Draw("z>>h_z_ML(500, -70, 70)", cutML); canvZ->cd(3); - nt->Draw("z>>h_z_OT(500, -85, 85)", "id >= 5132 "); + nt->Draw("z>>h_z_OT(500, -85, 85)", cutOT); canvZ->SaveAs("trkdigits_z.pdf"); // dz distributions (difference between local position of digits and hits in x and z) auto canvdZ = new TCanvas("canvdZ", "", 800, 2400); canvdZ->Divide(1, 3); canvdZ->cd(1); - nt->Draw("dz>>h_dz_VD(500, -0.05, 0.05)", "id < 12 "); + nt->Draw("dz>>h_dz_VD(500, -0.05, 0.05)", cutVD); canvdZ->cd(2); - nt->Draw("dz>>h_dz_ML(500, -0.05, 0.05)", "id >= 12 && id < 5132 "); + nt->Draw("dz>>h_dz_ML(500, -0.05, 0.05)", cutML); canvdZ->cd(3); - nt->Draw("dz>>h_dz_OT(500, -0.05, 0.05)", "id >= 5132 "); + nt->Draw("dz>>h_dz_OT(500, -0.05, 0.05)", cutOT); canvdZ->SaveAs("trkdigits_dz.pdf"); canvdZ->SaveAs("trkdigits_dz.root"); @@ -341,39 +369,39 @@ void CheckDigits(std::string digifile = "trkdigits.root", std::string hitfile = auto canvdXdZ = new TCanvas("canvdXdZ", "", 1600, 2400); canvdXdZ->Divide(2, 3); canvdXdZ->cd(1); - nt->Draw("dx:dz>>h_dx_vs_dz_VD(500, -0.005, 0.005, 500, -0.005, 0.005)", "id < 12", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_VD(500, -0.005, 0.005, 500, -0.005, 0.005)", cutVD, "colz"); addTLines(o2::trk::SegmentationChip::PitchRowVD); auto h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_VD"); LOG(info) << "dx, dz"; Info("VD", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); Info("VD", "RMS(dz)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZ->cd(2); - nt->Draw("dx:dz>>h_dx_vs_dz_VD_z(500, -0.005, 0.005, 500, -0.005, 0.005)", "id < 12 && abs(z)<0.5", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_VD_z(500, -0.005, 0.005, 500, -0.005, 0.005)", cutVD + " && abs(z)<0.5", "colz"); addTLines(o2::trk::SegmentationChip::PitchRowVD); h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_VD_z"); Info("VD |z|<1", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); Info("VD |z|<1", "RMS(dz)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZ->cd(3); - nt->Draw("dx:dz>>h_dx_vs_dz_ML(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 12 && id < 5132", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_ML(600, -0.03, 0.03, 600, -0.03, 0.03)", cutML, "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_ML"); Info("ML", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); Info("ML", "RMS(dz)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZ->cd(4); - nt->Draw("dx:dz>>h_dx_vs_dz_ML_z(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 12 && id < 5132 && abs(z)<2", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_ML_z(600, -0.03, 0.03, 600, -0.03, 0.03)", cutML + " && abs(z)<2", "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_ML_z"); Info("ML |z|<2", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); Info("ML |z|<2", "RMS(dz)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZ->SaveAs("trkdigits_dx_vs_dz.pdf"); canvdXdZ->cd(5); - nt->Draw("dx:dz>>h_dx_vs_dz_OT(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 5132", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_OT(600, -0.03, 0.03, 600, -0.03, 0.03)", cutOT, "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_OT"); Info("OT", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); Info("OT", "RMS(dz)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZ->cd(6); - nt->Draw("dx:dz>>h_dx_vs_dz_OT_z(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 5132 && abs(z)<2", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_OT_z(600, -0.03, 0.03, 600, -0.03, 0.03)", cutOT + " && abs(z)<2", "colz"); h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_OT_z"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); Info("OT |z|<2", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); @@ -385,39 +413,39 @@ void CheckDigits(std::string digifile = "trkdigits.root", std::string hitfile = auto canvdXdZHit = new TCanvas("canvdXdZHit", "", 1600, 2400); canvdXdZHit->Divide(2, 3); canvdXdZHit->cd(1); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_VD(300, -0.03, 0.03, 300, -0.03, 0.03)", "id < 12", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_VD(300, -0.03, 0.03, 300, -0.03, 0.03)", cutVD, "colz"); addTLines(o2::trk::SegmentationChip::PitchRowVD); LOG(info) << "dxH, dzH"; h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_VD"); Info("VD", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); Info("VD", "RMS(dzH)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZHit->cd(2); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_VD_z(300, -0.03, 0.03, 300, -0.03, 0.03)", "id < 12 && abs(z)<2", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_VD_z(300, -0.03, 0.03, 300, -0.03, 0.03)", cutVD + " && abs(z)<2", "colz"); addTLines(o2::trk::SegmentationChip::PitchRowVD); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_VD_z"); Info("VD |z|<2", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); Info("VD |z|<2", "RMS(dzH)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZHit->cd(3); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_ML(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 12 && id < 5132", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_ML(300, -0.03, 0.03, 300, -0.03, 0.03)", cutML, "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_ML"); Info("ML", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); Info("ML", "RMS(dzH)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZHit->cd(4); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_ML_z(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 12 && id < 5132 && abs(z)<2", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_ML_z(300, -0.03, 0.03, 300, -0.03, 0.03)", cutML + " && abs(z)<2", "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_ML_z"); Info("ML |z|<2", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); Info("ML |z|<2", "RMS(dzH)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZHit->SaveAs("trkdigits_dxH_vs_dzH.pdf"); canvdXdZHit->cd(5); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_OT(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 5132", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_OT(300, -0.03, 0.03, 300, -0.03, 0.03)", cutOT, "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_OT"); Info("OT", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); Info("OT", "RMS(dzH)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZHit->cd(6); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_OT_z(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 5132 && abs(z)<2", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_OT_z(300, -0.03, 0.03, 300, -0.03, 0.03)", cutOT + " && abs(z)<2", "colz"); addTLines(o2::trk::SegmentationChip::PitchRowMLOT); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_OT_z"); Info("OT |z|<2", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKLayer.h b/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKLayer.h index e900cfa679ffe..2839e2db9307d 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKLayer.h +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKLayer.h @@ -13,7 +13,6 @@ #define ALICEO2_TRK_LAYER_H #include "TRKBase/Specs.h" -#include "TRKBase/TRKBaseParam.h" #include #include @@ -77,8 +76,8 @@ class TRKSegmentedLayer : public TRKCylindricalLayer TGeoVolume* createSensor() override; TGeoVolume* createDeadzone(); TGeoVolume* createMetalStack() override; - TGeoVolume* createChip(); - TGeoVolume* createModule(); + virtual TGeoVolume* createChip(); + virtual TGeoVolume* createModule(); virtual TGeoVolume* createStave() = 0; void createLayer(TGeoVolume* motherVolume) override = 0; @@ -155,6 +154,40 @@ class TRKOTLayer : public TRKSegmentedLayer ClassDefOverride(TRKOTLayer, 0); }; +// Simplified-realistic OT barrel: modules built from their real parts (FPC, cold plate, +// connector, capacitors, brackets), two-row staves overlapping in phi, four quarter +// barrels. Dimensions in constants::OT. +class TRKOTLayerRealistic : public TRKSegmentedLayer +{ + public: + TRKOTLayerRealistic() = default; + TRKOTLayerRealistic(int layerNumber, std::string layerName, float rInn, float tiltAngle, int numberOfStaves, int numberOfModules, float thickOrX2X0, MatBudgetParamMode mode); + ~TRKOTLayerRealistic() override = default; + + TGeoVolume* createChip() override; + TGeoVolume* createModule() override; + TGeoVolume* createStave() override; + TGeoVolume* createHalfStave(); + void createLayer(TGeoVolume* motherVolume) override; + + private: + TGeoVolume* createFPC(); + TGeoVolume* createColdPlate(); + TGeoVolume* createCoolingPipe(); + TGeoVolume* createEndOfStaveCard(); + TGeoVolume* createSupportRing(double rMin, double rMax, double phi1, double phi2, int id); + void addConnector(TGeoVolume* moduleVol, double rMid); + void addCapacitors(TGeoVolume* moduleVol, double rMid); + void addBrackets(TGeoVolume* moduleVol, double rMid); + + double getRowHalfLength() const; // z half-length of one module row (= one eta half-barrel) + double getPipeTrim() const; // z removed from the mid-rapidity pipe end to clear the support ring + + std::pair getBoundingRadii(double staveWidth) const override; + + ClassDefOverride(TRKOTLayerRealistic, 0); +}; + } // namespace trk } // namespace o2 #endif // ALICEO2_TRK_LAYER_H diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKServices.h b/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKServices.h index dedbbb096b8e8..dcbc2d724a862 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKServices.h +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/TRKServices.h @@ -55,11 +55,18 @@ class TRKServices : public FairModule void createServicesAroundBeamPipe(TGeoVolume* motherVolume); void createMLServicesPeacock(TGeoVolume* motherVolume); void createOTServicesPeacock(TGeoVolume* motherVolume); + void createOTBarrelWalls(TGeoVolume* motherVolume); void createVacuumCompositeShape(); void excavateFromVacuum(TString shapeToExcavate); void registerVacuum(TGeoVolume* motherVolume); protected: + // Carbon fibre shells bounding the OT barrel in r; the separation walls span between them. + static constexpr float sMLOTShellRMax = 39.5f; // cm, outer radius of the ML/OT separation shell + static constexpr float sMLOTShellThickness = 0.2f; // cm + static constexpr float sOTShellRMin = 82.0f; // cm, inner radius of the OT outer shell + static constexpr float sOTShellThickness = 0.4f; // cm, load bearing, hence thicker + // Vacuum TString mVacuumCompositeFormula; // Coldplate diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/src/Detector.cxx b/Detectors/Upgrades/ALICE3/TRK/simulation/src/Detector.cxx index 196727b2c140f..0043076cde901 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/src/Detector.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/src/Detector.cxx @@ -126,6 +126,32 @@ void Detector::configMLOT() } break; } + case kSimplifiedRealistic: { + // Same ML as segmented; OT uses the detailed (realistic) barrel. + const std::vector tiltAngles{11.2f, 11.9f, 11.4f, 0.f, 0.f, 0.f, 0.f, 0.f}; + const std::vector nMods{11, 11, 11, 11, 11, 22, 22, 22}; + const std::vector stagOffsets{0.f, 0.f, 0.f, 1.17f, 0.89f}; + + // OT radii chosen at the top of a stave-count band, where the paving closes with + // ~1.5-1.9 mm of neighbour overlap; the outermost also clears TRKServices::sOTShellRMin. + std::vector rInnReal = rInn; + rInnReal[constants::ML::nLayers + 0] = 44.0f; + rInnReal[constants::ML::nLayers + 1] = 61.5f; + rInnReal[constants::ML::nLayers + 2] = 79.3f; + // OT counts are informational: TRKOTLayerRealistic derives its own from the radius. + const std::vector nStaves{10, 14, 18, 26, 38, 30, 42, 54}; + + LOGP(warning, "Loading simplified-realistic configuration for ALICE3 TRK"); + for (int i{0}; i < constants::ML::nLayers + constants::OT::nLayers; ++i) { + std::string name = GeometryTGeo::getTRKLayerPattern() + std::to_string(i); + if (i < constants::ML::nLayers) { + mLayers.push_back(std::make_unique(i, name, rInnReal[i], stagOffsets[i], tiltAngles[i], nStaves[i], nMods[i], thick, MatBudgetParamMode::Thickness)); + } else { + mLayers.push_back(std::make_unique(i, name, rInnReal[i], tiltAngles[i], nStaves[i], nMods[i], thick, MatBudgetParamMode::Thickness)); + } + } + break; + } default: LOGP(fatal, "Unknown option {} for configMLOT", static_cast(trkPars.layoutMLOT)); break; @@ -189,7 +215,8 @@ void Detector::configFromFile(std::string fileName) mLayers.push_back(std::make_unique(layerCount, name, rInn, length, thick, matBudgetMode)); break; } - case kSegmented: { + case kSegmented: + case kSimplifiedRealistic: { // Expected column mapping in the text file (separated by \t): // tmpBuff[0] = rInn // tmpBuff[1] = thick @@ -231,7 +258,11 @@ void Detector::configFromFile(std::string fileName) matBudgetMode = static_cast(static_cast(tmpBuff[5])); } - mLayers.push_back(std::make_unique(layerCount, name, rInn, tiltAngle, nStaves, nMods, thick, matBudgetMode)); + if (trkPars.layoutMLOT == kSimplifiedRealistic) { + mLayers.push_back(std::make_unique(layerCount, name, rInn, tiltAngle, nStaves, nMods, thick, matBudgetMode)); + } else { + mLayers.push_back(std::make_unique(layerCount, name, rInn, tiltAngle, nStaves, nMods, thick, matBudgetMode)); + } } break; } @@ -283,6 +314,13 @@ void Detector::createMaterials() float epsilCer = 1.0E-4; // .10000E+01; float stminCer = 0.0; // cm "Default value used" + // Tracking parameters shared by the passive materials below + float tmaxfdPas = 0.1; + float stemaxPas = 1.0; + float deemaxPas = 0.1; + float epsilPas = 1.0E-4; + float stminPas = 0.0; + // AIR float aAir[4] = {12.0107, 14.0067, 15.9994, 39.948}; float zAir[4] = {6., 7., 8., 18.}; @@ -293,11 +331,59 @@ void Detector::createMaterials() float aCf[2] = {12.0107, 1.00794}; float zCf[2] = {6., 1.}; + // FPC: Kapton+Cu effective mixture, X0 ~ 5 cm + float aFpc[2] = {63.546f, 12.0107f}; // Cu, C (Kapton proxy) + float zFpc[2] = {29.f, 6.f}; + float wFpc[2] = {0.40f, 0.60f}; + float dFpc = 3.4f; + + // ZIF connector: LCP+Cu effective mixture, X0 ~ 2.9 cm + float aLcpCu[2] = {63.546f, 12.0107f}; + float zLcpCu[2] = {29.f, 6.f}; + float wLcpCu[2] = {0.60f, 0.40f}; + float dLcpCu = 5.5f; + + // SMD capacitors: BaTiO3 ceramic, X0 ~ 1.9 cm + float aBaTiO3[3] = {137.327f, 47.867f, 15.9994f}; + float zBaTiO3[3] = {56.f, 22.f, 8.f}; + float wBaTiO3[3] = {0.5879f, 0.2054f, 0.2067f}; + float dBaTiO3 = 6.0f; + + // FR4 (PCB laminate) for the end-of-stave cards: 60% glass (SiO2) + 40% epoxy by weight + float aFr4[4] = {28.0855f, 15.9994f, 12.0107f, 1.00794f}; // Si, O, C, H + float zFr4[4] = {14.f, 8.f, 6.f, 1.f}; + float wFr4[4] = {0.2804f, 0.3836f, 0.3040f, 0.0320f}; + float dFr4 = 1.85f; + o2::base::Detector::Mixture(1, "AIR$", aAir, zAir, dAir, 4, wAir); o2::base::Detector::Medium(1, "AIR$", 1, 0, ifield, fieldm, tmaxfdAir, stemaxAir, deemaxAir, epsilAir, stminAir); o2::base::Detector::Material(3, "SILICON$", 0.28086E+02, 0.14000E+02, 0.23300E+01, 0.93600E+01, 0.99900E+03); o2::base::Detector::Medium(3, "SILICON$", 3, 0, ifield, fieldm, tmaxfdSi, stemaxSi, deemaxSi, epsilSi, stminSi); + + // Carbon fibre: density tuned so X0 ~ 27 cm + o2::base::Detector::Material(4, "CARBONFIBER$", 12.0107f, 6.f, 1.45f, 27.0f, 999.f); + o2::base::Detector::Medium(4, "CARBONFIBER$", 4, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); + + o2::base::Detector::Mixture(5, "FPC$", aFpc, zFpc, dFpc, 2, wFpc); + o2::base::Detector::Medium(5, "FPC$", 5, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); + + o2::base::Detector::Mixture(6, "LCPCU$", aLcpCu, zLcpCu, dLcpCu, 2, wLcpCu); + o2::base::Detector::Medium(6, "LCPCU$", 6, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); + + o2::base::Detector::Mixture(7, "BATIO3$", aBaTiO3, zBaTiO3, dBaTiO3, 3, wBaTiO3); + o2::base::Detector::Medium(7, "BATIO3$", 7, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); + + // PEEK polymer for mounting brackets: X0 ~ 20 cm + o2::base::Detector::Material(8, "PEEK$", 12.0107f, 6.f, 1.32f, 20.0f, 999.f); + o2::base::Detector::Medium(8, "PEEK$", 8, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); + + o2::base::Detector::Mixture(9, "FR4$", aFr4, zFr4, dFr4, 4, wFr4); + o2::base::Detector::Medium(9, "FR4$", 9, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); + + // Copper planes of the end-of-stave cards: X0 = 1.436 cm + o2::base::Detector::Material(10, "COPPER$", 63.546f, 29.f, 8.96f, 1.436f, 999.f); + o2::base::Detector::Medium(10, "COPPER$", 10, 0, ifield, fieldm, tmaxfdPas, stemaxPas, deemaxPas, epsilPas, stminPas); } void Detector::createGeometry() @@ -562,6 +648,25 @@ bool Detector::ProcessHits(FairVolume* vol) } else { LOGP(fatal, "Wrong number of halfstaves for layer {}", layer); } + } else if (trkPars.layoutMLOT == o2::trk::eMLOTLayout::kSimplifiedRealistic) { + // Stave/half-stave/module are assembly volumes here, for which CurrentVolOffID copy + // numbers all read 0; resolve the indices from the TGeo path at the hit mid-point. + const TVector3 mid = (mTrackData.mPositionStart.Vect() + positionStop.Vect()) * 0.5; + gGeoManager->PushPath(); + if (gGeoManager->FindNode(mid.X(), mid.Y(), mid.Z())) { + auto copyUp = [](int up) { TGeoNode* n = gGeoManager->GetMother(up); return n ? n->GetNumber() : 0; }; + chip = copyUp(1); + mod = copyUp(2); + if (mGeometryTGeo->getNumberOfHalfStaves(layer) == 2) { + halfstave = copyUp(3); + stave = copyUp(4); + } else if (mGeometryTGeo->getNumberOfHalfStaves(layer) == 1) { + stave = copyUp(3); + } else { + LOGP(fatal, "Wrong number of halfstaves for layer {}", layer); + } + } + gGeoManager->PopPath(); } } /// if VD, for the moment the volume is the "chipID" so no need to retrieve other elments diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKLayer.cxx b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKLayer.cxx index 5206985992ecf..9712d4fe32df9 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKLayer.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKLayer.cxx @@ -15,11 +15,14 @@ #include "TRKBase/GeometryTGeo.h" #include "TRKBase/Specs.h" +#include "TRKBase/TRKBaseParam.h" #include +#include #include #include #include +#include #include #include #include @@ -136,31 +139,20 @@ TGeoVolume* TRKSegmentedLayer::createChip() chipVol->SetLineColor(kYellow); TGeoVolume* sensVol = createSensor(); - TGeoCombiTrans* transSens = new TGeoCombiTrans(); - TGeoVolume* deadVol = createDeadzone(); - TGeoCombiTrans* transDead = new TGeoCombiTrans(); - TGeoVolume* metalVol = createMetalStack(); + TGeoCombiTrans* transSens = new TGeoCombiTrans(); + TGeoCombiTrans* transDead = new TGeoCombiTrans(); TGeoCombiTrans* transMetal = new TGeoCombiTrans(); - if (!mIsFlipped) { - transSens->SetTranslation(-sDeadzoneWidth / 2, (mChipThickness - sSensorThickness) / 2, 0); - transDead->SetTranslation((sChipWidth - sDeadzoneWidth) / 2, (mChipThickness - sSensorThickness) / 2, 0); - transMetal->SetTranslation(0, -sSensorThickness / 2, 0); - } else { - transSens->SetTranslation(-sDeadzoneWidth / 2, -(mChipThickness - sSensorThickness) / 2, 0); - transDead->SetTranslation((sChipWidth - sDeadzoneWidth) / 2, -(mChipThickness - sSensorThickness) / 2, 0); - transMetal->SetTranslation(0, sSensorThickness / 2, 0); - } + const double sensY = mIsFlipped ? -(mChipThickness - sSensorThickness) / 2 : (mChipThickness - sSensorThickness) / 2; + const double metalY = mIsFlipped ? sSensorThickness / 2 : -sSensorThickness / 2; + transSens->SetTranslation(-sDeadzoneWidth / 2, sensY, 0); + transDead->SetTranslation((sChipWidth - sDeadzoneWidth) / 2, sensY, 0); + transMetal->SetTranslation(0, metalY, 0); - LOGP(debug, "Inserting {} in {} ", sensVol->GetName(), chipVol->GetName()); chipVol->AddNode(sensVol, 1, transSens); - - LOGP(debug, "Inserting {} in {} ", deadVol->GetName(), chipVol->GetName()); chipVol->AddNode(deadVol, 1, transDead); - - LOGP(debug, "Inserting {} in {} ", metalVol->GetName(), chipVol->GetName()); chipVol->AddNode(metalVol, 1, transMetal); return chipVol; @@ -393,14 +385,12 @@ TGeoVolume* TRKOTLayer::createHalfStave() TGeoVolume* halfStaveVol = new TGeoVolume(halfStaveName.c_str(), halfStave, medSi); halfStaveVol->SetLineColor(kYellow); - int nModulesPerHalfBarrel = mNumberOfModules / 2; // assuming mNumberOfModules is always even, which should be the case given the current specifications + int nModulesPerHalfBarrel = mNumberOfModules / 2; for (int iModule = 0; iModule < nModulesPerHalfBarrel; iModule++) { - TGeoVolume* moduleVol = createModule(); double zPos = -0.5 * nModulesPerHalfBarrel * sModuleLength + (iModule + 0.5) * sModuleLength; TGeoCombiTrans* trans = new TGeoCombiTrans(); trans->SetTranslation(0, 0, zPos); - LOGP(debug, "Inserting {} in {} ", moduleVol->GetName(), halfStaveVol->GetName()); - halfStaveVol->AddNode(moduleVol, iModule, trans); + halfStaveVol->AddNode(createModule(), iModule, trans); } return halfStaveVol; @@ -411,82 +401,456 @@ TGeoVolume* TRKOTLayer::createStave() std::string staveName = GeometryTGeo::getTRKStavePattern() + std::to_string(mLayerNumber); TGeoVolume* staveVol = new TGeoVolumeAssembly(staveName.c_str()); - TGeoVolume* halfStaveVolLeft = createHalfStave(); TGeoCombiTrans* transLeft = new TGeoCombiTrans(); transLeft->SetTranslation(-(sHalfStaveWidth - sInStaveOverlap) / 2, 0, 0); - LOGP(debug, "Inserting {} in {} ", halfStaveVolLeft->GetName(), staveVol->GetName()); - staveVol->AddNode(halfStaveVolLeft, 0, transLeft); + staveVol->AddNode(createHalfStave(), 0, transLeft); - TGeoVolume* halfStaveVolRight = createHalfStave(); TGeoCombiTrans* transRight = new TGeoCombiTrans(); transRight->SetTranslation((sHalfStaveWidth - sInStaveOverlap) / 2, 0.2, 0); - LOGP(debug, "Inserting {} in {} ", halfStaveVolRight->GetName(), staveVol->GetName()); - staveVol->AddNode(halfStaveVolRight, 1, transRight); + staveVol->AddNode(createHalfStave(), 1, transRight); return staveVol; } void TRKOTLayer::createLayer(TGeoVolume* motherVolume) { - // Retrieve exact bounding boundaries automatically inherited from TRKSegmentedLayer auto [rMin, rMax] = getBoundingRadii(sStaveWidth); TGeoMedium* medAir = gGeoManager->GetMedium("TRK_AIR$"); - // TGeoTube* layer = new TGeoTube(mInnerRadius - 0.333 * sLogicalVolumeThickness, mInnerRadius + 0.667 * sLogicalVolumeThickness, mLength / 2); TGeoTube* layer = new TGeoTube(rMin, rMax, (mLength + sGapBetweenOuterTrackerBarrelHalves) / 2); TGeoVolume* layerVol = new TGeoVolume(mLayerName.c_str(), layer, medAir); layerVol->SetLineColor(kYellow); - // Compute the number of staves int nStavesHalfBarrel = (int)std::ceil(mInnerRadius * 2 * TMath::Pi() / sStaveWidth); - nStavesHalfBarrel += nStavesHalfBarrel % 2; // Require an even number of staves + nStavesHalfBarrel += nStavesHalfBarrel % 2; - // Nominal average radius used as the placement barycenter for all staves const double avgRadius = 0.5 * (mInnerRadius + mOuterRadius); - - // Compute the size of the overlap region - double theta = 2. * TMath::Pi() / nStavesHalfBarrel; - double theta1 = std::atan(sStaveWidth / 2 / mInnerRadius); - double st = std::sin(theta); - double ct = std::cos(theta); - double theta2 = std::atan((mInnerRadius * st - sStaveWidth / 2 * ct) / (mInnerRadius * ct + sStaveWidth / 2 * st)); - double overlap = (theta1 - theta2) * mInnerRadius; - LOGP(info, "Creating a layer with two half barrels, each with {} staves and {} mm overlap", nStavesHalfBarrel, overlap * 10); - - float lengthHalfBarrel = mLength / 2; - int nStaves = nStavesHalfBarrel * 2; // since we now have two half-barrels (separated by a small gap), we double the number of staves + const double theta = 2. * TMath::Pi() / nStavesHalfBarrel; + const float lengthHalfBarrel = mLength / 2; + const int nStaves = nStavesHalfBarrel * 2; + LOGP(info, "Creating OT layer {} with two half-barrels of {} staves each", mLayerNumber, nStavesHalfBarrel); for (int iStave = 0; iStave < nStaves; iStave++) { - TGeoVolume* staveVol = createStave(); - int whichHalfBarrel = iStave / nStavesHalfBarrel; // 0 for the first half (negative z), 1 for the second half (positive z) - TGeoCombiTrans* trans = new TGeoCombiTrans(); + int whichHalfBarrel = iStave / nStavesHalfBarrel; double phi = theta * iStave; - double phiDeg = phi * TMath::RadToDeg(); - // TGeoRotation* rot = new TGeoRotation("rot", phiDeg + 90 + mTiltAngle, 0, 0); TGeoRotation* rot = new TGeoRotation("rot"); if (whichHalfBarrel == 1) { - rot->RotateY(180.); // degrees, rotate the second half barrel by 180 degrees around Y to achieve the correct staggering orientation + rot->RotateY(180.); } - rot->RotateZ(phiDeg + 90 + (whichHalfBarrel == 0 ? +1 : -1) * mTiltAngle); // phi in degrees, tilting depends on the half-barrel side - trans->SetRotation(rot); - // trans->SetTranslation(mInnerRadius * std::cos(phi), mInnerRadius * std::sin(phi), 0); - // trans->SetTranslation(avgRadius * std::cos(phi), avgRadius * std::sin(phi), 0); + rot->RotateZ(phi * TMath::RadToDeg() + 90 + (whichHalfBarrel == 0 ? +1 : -1) * mTiltAngle); double zPos = (whichHalfBarrel == 0 ? -1 : 1) * (0.5 * lengthHalfBarrel + sGapBetweenOuterTrackerBarrelHalves / 2); + TGeoCombiTrans* trans = new TGeoCombiTrans(); + trans->SetRotation(rot); trans->SetTranslation(avgRadius * std::cos(phi), avgRadius * std::sin(phi), zPos); - LOGP(debug, "Inserting {} in {} ", staveVol->GetName(), layerVol->GetName()); - layerVol->AddNode(staveVol, iStave, trans); + layerVol->AddNode(createStave(), iStave, trans); } - LOGP(debug, "Inserting {} in {} ", layerVol->GetName(), motherVolume->GetName()); motherVolume->AddNode(layerVol, 1, nullptr); } std::pair TRKOTLayer::getBoundingRadii(double staveWidth) const { auto [radiusMin, radiusMax] = TRKSegmentedLayer::getBoundingRadii(staveWidth); - return {radiusMin - 0.201f, radiusMax}; } + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +TRKOTLayerRealistic::TRKOTLayerRealistic(int layerNumber, std::string layerName, float rInn, float tiltAngle, int numberOfStaves, int numberOfModules, float thickOrX2X0, MatBudgetParamMode mode) + : TRKSegmentedLayer(layerNumber, layerName, rInn, tiltAngle, numberOfStaves, numberOfModules, thickOrX2X0, mode) +{ + // Outermost layer is flipped: cooling pipe and support rings on the inner side. + if (mLayerNumber == constants::ML::nLayers + 2) { + mIsFlipped = true; + } +} + +TGeoVolume* TRKOTLayerRealistic::createChip() +{ + TGeoMedium* medSi = gGeoManager->GetMedium("TRK_SILICON$"); + std::string chipName = GeometryTGeo::getTRKChipPattern() + std::to_string(mLayerNumber); + TGeoShape* chip = new TGeoBBox(sChipWidth / 2, constants::OT::sensorThickness / 2, sChipLength / 2); + TGeoVolume* chipVol = new TGeoVolume(chipName.c_str(), chip, medSi); + chipVol->SetLineColor(kYellow); + + // Active sensor and passive read-out edge tile the chip width. + chipVol->AddNode(createSensor(), 1, new TGeoTranslation(-sDeadzoneWidth / 2, 0, 0)); + chipVol->AddNode(createDeadzone(), 1, new TGeoTranslation((sChipWidth - sDeadzoneWidth) / 2, 0, 0)); + return chipVol; +} + +TGeoVolume* TRKOTLayerRealistic::createFPC() +{ + TGeoMedium* med = gGeoManager->GetMedium("TRK_FPC$"); + TGeoShape* shape = new TGeoBBox(constants::OT::fpc::width / 2, constants::OT::fpc::thickness / 2, constants::OT::fpc::length / 2); + TGeoVolume* vol = new TGeoVolume((GeometryTGeo::getTRKModulePattern() + std::to_string(mLayerNumber) + "_FPC").c_str(), shape, med); + vol->SetLineColor(kOrange); + return vol; +} + +TGeoVolume* TRKOTLayerRealistic::createColdPlate() +{ + TGeoMedium* med = gGeoManager->GetMedium("TRK_CARBONFIBER$"); + TGeoShape* shape = new TGeoBBox(constants::OT::coldPlate::width / 2, constants::OT::coldPlate::thickness / 2, constants::OT::coldPlate::length / 2); + TGeoVolume* vol = new TGeoVolume((GeometryTGeo::getTRKModulePattern() + std::to_string(mLayerNumber) + "_ColdPlate").c_str(), shape, med); + vol->SetLineColor(kGray + 2); + return vol; +} + +double TRKOTLayerRealistic::getRowHalfLength() const +{ + const int nModulesPerRow = mNumberOfModules / 2; + return (nModulesPerRow * constants::OT::fpc::length + (nModulesPerRow - 1) * constants::OT::interModuleGap) / 2; +} + +double TRKOTLayerRealistic::getPipeTrim() const +{ + // The mid-rapidity ring sits at the pipe radius, between the z = 0 wall and the pipe. + const double wallThickness = TRKBaseParam::Instance().otBarrelWallThickness; + const double pipeStart = wallThickness + 2 * constants::OT::supportRing::zClearance + constants::OT::supportRing::zWidth; + return std::max(0., pipeStart - constants::OT::barrelHalvesZGap / 2); +} + +TGeoVolume* TRKOTLayerRealistic::createSupportRing(double rMin, double rMax, double phi1, double phi2, int id) +{ + // Hollow rectangular-section half-ring, open at the two azimuthal ends. + TGeoMedium* med = gGeoManager->GetMedium("TRK_CARBONFIBER$"); + const double t = constants::OT::supportRing::wallThickness; + const double dz = constants::OT::supportRing::zWidth / 2; + const std::string base = GeometryTGeo::getTRKLayerPattern() + std::to_string(mLayerNumber) + "_SupportRing" + std::to_string(id); + new TGeoTubeSeg((base + "_outsh").c_str(), rMin, rMax, dz, phi1, phi2); + new TGeoTubeSeg((base + "_insh").c_str(), rMin + t, rMax - t, dz - t, phi1, phi2); + TGeoShape* shape = new TGeoCompositeShape((base + "sh").c_str(), (base + "_outsh-" + base + "_insh").c_str()); + TGeoVolume* vol = new TGeoVolume(base.c_str(), shape, med); + vol->SetLineColor(kGray + 2); + return vol; +} + +TGeoVolume* TRKOTLayerRealistic::createCoolingPipe() +{ + TGeoMedium* med = gGeoManager->GetMedium("TRK_CARBONFIBER$"); + TGeoShape* tube = new TGeoTube(constants::OT::coolingPipe::rInner, constants::OT::coolingPipe::rOuter, + getRowHalfLength() - getPipeTrim() / 2); + TGeoVolume* vol = new TGeoVolume((GeometryTGeo::getTRKStavePattern() + std::to_string(mLayerNumber) + "_CoolingPipe").c_str(), tube, med); + vol->SetLineColor(kBlue + 2); + return vol; +} + +TGeoVolume* TRKOTLayerRealistic::createEndOfStaveCard() +{ + TGeoMedium* medFR4 = gGeoManager->GetMedium("TRK_FR4$"); + TGeoMedium* medCu = gGeoManager->GetMedium("TRK_COPPER$"); + const std::string name = GeometryTGeo::getTRKStavePattern() + std::to_string(mLayerNumber) + "_EOSCard"; + + TGeoShape* board = new TGeoBBox(constants::OT::eosCard::width / 2, constants::OT::eosCard::thickness / 2, constants::OT::eosCard::length / 2); + TGeoVolume* cardVol = new TGeoVolume(name.c_str(), board, medFR4); + cardVol->SetLineColor(kGreen + 3); + + TGeoShape* plane = new TGeoBBox(constants::OT::eosCard::width / 2, constants::OT::eosCard::copperThickness / 2, constants::OT::eosCard::length / 2); + TGeoVolume* planeVol = new TGeoVolume((name + "_Cu").c_str(), plane, medCu); + planeVol->SetLineColor(kOrange + 7); + + // Evenly spaced, the outermost two flush with the board surfaces. + const int nPlanes = constants::OT::eosCard::nCopperLayers; + const double span = constants::OT::eosCard::thickness - constants::OT::eosCard::copperThickness; + for (int iPlane = 0; iPlane < nPlanes; iPlane++) { + const double y = (nPlanes > 1) ? -span / 2 + iPlane * span / (nPlanes - 1) : 0.; + cardVol->AddNode(planeVol, iPlane, new TGeoTranslation(0, y, 0)); + } + + return cardVol; +} + +void TRKOTLayerRealistic::addConnector(TGeoVolume* moduleVol, double rMid) +{ + TGeoMedium* med = gGeoManager->GetMedium("TRK_LCPCU$"); + std::string name = GeometryTGeo::getTRKModulePattern() + std::to_string(mLayerNumber) + "_Connector"; + TGeoShape* shape = new TGeoBBox(constants::OT::connector::width / 2, constants::OT::connector::thickness / 2, constants::OT::connector::length / 2); + TGeoVolume* vol = new TGeoVolume(name.c_str(), shape, med); + vol->SetLineColor(kBlue); + + // Centred in phi, inset from the module short edge in z. + const double z = constants::OT::fpc::length / 2 - constants::OT::connectorZDepth; + moduleVol->AddNode(vol, 0, new TGeoTranslation(0, rMid, z)); +} + +void TRKOTLayerRealistic::addCapacitors(TGeoVolume* moduleVol, double rMid) +{ + TGeoMedium* med = gGeoManager->GetMedium("TRK_BATIO3$"); + std::string name = GeometryTGeo::getTRKModulePattern() + std::to_string(mLayerNumber) + "_Cap"; + TGeoShape* shape = new TGeoBBox(constants::OT::capacitor::width / 2, constants::OT::capacitor::thickness / 2, constants::OT::capacitor::length / 2); + TGeoVolume* vol = new TGeoVolume(name.c_str(), shape, med); + vol->SetLineColor(kCyan); + + const double pitchX = sChipWidth + constants::OT::interChipGap; + const double pitchZ = sChipLength + constants::OT::interChipGap; + const double chipX[2] = {-0.5 * pitchX, +0.5 * pitchX}; + const double chipZ[4] = {-1.5 * pitchZ, -0.5 * pitchZ, +0.5 * pitchZ, +1.5 * pitchZ}; + const double dX[5] = {-0.80, +0.80, -0.80, +0.80, 0.0}; // per chip: 4 corners + centre [cm] + const double dZ[5] = {-0.95, -0.95, +0.95, +0.95, 0.0}; + + // Skip capacitors that fall under the connector footprint (+1 mm clearance). + const double connZ = constants::OT::fpc::length / 2 - constants::OT::connectorZDepth; + const double skipX = constants::OT::connector::width / 2 + constants::OT::capacitor::width / 2 + 0.1; + const double skipZ = constants::OT::connector::length / 2 + constants::OT::capacitor::length / 2 + 0.1; + + int capCopy = 0; + for (int iZ = 0; iZ < 4; iZ++) { + for (int iX = 0; iX < 2; iX++) { + for (int iCap = 0; iCap < constants::OT::capacitor::perChip; iCap++) { + const double x = chipX[iX] + dX[iCap]; + const double z = chipZ[iZ] + dZ[iCap]; + if (std::abs(x) < skipX && std::abs(z - connZ) < skipZ) { + continue; + } + moduleVol->AddNode(vol, capCopy++, new TGeoTranslation(x, rMid, z)); + } + } + } +} + +void TRKOTLayerRealistic::addBrackets(TGeoVolume* moduleVol, double rMid) +{ + TGeoMedium* med = gGeoManager->GetMedium("TRK_PEEK$"); + std::string name = GeometryTGeo::getTRKModulePattern() + std::to_string(mLayerNumber) + "_Bracket"; + TGeoShape* shape = new TGeoBBox(constants::OT::bracket::width / 2, constants::OT::bracket::thickness / 2, constants::OT::bracket::length / 2); + TGeoVolume* vol = new TGeoVolume(name.c_str(), shape, med); + vol->SetLineColor(kGreen + 2); + + const double z = constants::OT::coldPlate::length / 2 - constants::OT::bracketZDepth; + moduleVol->AddNode(vol, 0, new TGeoTranslation(0, rMid, -z)); + moduleVol->AddNode(vol, 1, new TGeoTranslation(0, rMid, +z)); +} + +TGeoVolume* TRKOTLayerRealistic::createModule() +{ + std::string modName = GeometryTGeo::getTRKModulePattern() + std::to_string(mLayerNumber); + TGeoVolume* moduleVol = new TGeoVolumeAssembly(modName.c_str()); + + // Flush component stack about the chip mid-plane (local r = 0). + const double chipHalf = constants::OT::sensorThickness / 2; + const double fpcMidY = -(chipHalf + constants::OT::fpc::thickness / 2); + const double coldPlateMidY = +(chipHalf + constants::OT::coldPlate::thickness / 2); + const double connMidY = -(chipHalf + constants::OT::fpc::thickness + constants::OT::connector::thickness / 2); + const double capMidY = -(chipHalf + constants::OT::fpc::thickness + constants::OT::capacitor::thickness / 2); + const double bracketMidY = +(chipHalf + constants::OT::coldPlate::thickness + constants::OT::bracket::thickness / 2); + + // 8 chips: 2 phi columns x 4 z rows, on a uniform chip+gap pitch. + const double pitchX = sChipWidth + constants::OT::interChipGap; + const double pitchZ = sChipLength + constants::OT::interChipGap; + const double chipX[2] = {-0.5 * pitchX, +0.5 * pitchX}; + const double chipZ[4] = {-1.5 * pitchZ, -0.5 * pitchZ, +0.5 * pitchZ, +1.5 * pitchZ}; + + moduleVol->AddNode(createColdPlate(), 0, new TGeoTranslation(0, coldPlateMidY, 0)); + + int chipCopy = 0; + for (int iZ = 0; iZ < 4; iZ++) { + for (int iX = 0; iX < 2; iX++) { + TGeoCombiTrans* trans = new TGeoCombiTrans(); + trans->SetTranslation(chipX[iX], 0., chipZ[iZ]); + if (iX == 0) { // inner column rotated so its dead zone faces the outer module edge + TGeoRotation* rot = new TGeoRotation(); + rot->RotateY(180.); + trans->SetRotation(rot); + } + moduleVol->AddNode(createChip(), chipCopy++, trans); + } + } + + moduleVol->AddNode(createFPC(), 0, new TGeoTranslation(0, fpcMidY, 0)); + addConnector(moduleVol, connMidY); + addCapacitors(moduleVol, capMidY); + addBrackets(moduleVol, bracketMidY); + return moduleVol; +} + +TGeoVolume* TRKOTLayerRealistic::createHalfStave() +{ + std::string rowName = GeometryTGeo::getTRKHalfStavePattern() + std::to_string(mLayerNumber); + TGeoVolume* rowVol = new TGeoVolumeAssembly(rowName.c_str()); + + const int nModulesPerRow = mNumberOfModules / 2; + const double moduleLength = constants::OT::fpc::length; + const double step = moduleLength + constants::OT::interModuleGap; + const double rowHalfLen = getRowHalfLength(); + + for (int iModule = 0; iModule < nModulesPerRow; iModule++) { + double zPos = -rowHalfLen + moduleLength / 2 + iModule * step; + TGeoCombiTrans* trans = new TGeoCombiTrans(); + trans->SetTranslation(0, 0, zPos); + rowVol->AddNode(createModule(), iModule, trans); + } + + return rowVol; +} + +TGeoVolume* TRKOTLayerRealistic::createStave() +{ + std::string staveName = GeometryTGeo::getTRKStavePattern() + std::to_string(mLayerNumber); + TGeoVolume* staveVol = new TGeoVolumeAssembly(staveName.c_str()); + + // Two rows overlapping in phi and staggered in r. They straddle the stave origin, so the + // stave is tangent to the barrel circle at its centre and every row-to-row radial step, + // within a stave and between neighbours, is rowRadialStagger. + const double edgeDead = constants::moduleMLOT::gaps::outerEdgeLongSide + constants::moduleMLOT::chip::passiveEdgeReadOut; + const double inStaveOverlap = 2 * edgeDead + constants::OT::rowActiveOverlap; + const double rowOffset = constants::OT::fpc::width - inStaveOverlap; + + TGeoCombiTrans* tRow0 = new TGeoCombiTrans(); + tRow0->SetTranslation(-rowOffset / 2, 0, 0); + staveVol->AddNode(createHalfStave(), 0, tRow0); + TGeoCombiTrans* tRow1 = new TGeoCombiTrans(); + tRow1->SetTranslation(rowOffset / 2, constants::OT::rowRadialStagger, 0); + staveVol->AddNode(createHalfStave(), 1, tRow1); + + // Shortened at the mid-rapidity end for the support ring, hence off-centre. + TGeoCombiTrans* tPipe = new TGeoCombiTrans(); + tPipe->SetTranslation(0, constants::OT::coolingPipe::rLocalOffset, getPipeTrim() / 2); + staveVol->AddNode(createCoolingPipe(), 0, tPipe); + + // Past the last module at the outer z end (local +z in both eta half-barrels). + TGeoCombiTrans* tCard = new TGeoCombiTrans(); + tCard->SetTranslation(0, constants::OT::rowRadialStagger / 2, + getRowHalfLength() + constants::OT::eosCard::zGap + constants::OT::eosCard::length / 2); + staveVol->AddNode(createEndOfStaveCard(), 0, tCard); + return staveVol; +} + +void TRKOTLayerRealistic::createLayer(TGeoVolume* motherVolume) +{ + const double edgeDead = constants::moduleMLOT::gaps::outerEdgeLongSide + constants::moduleMLOT::chip::passiveEdgeReadOut; + const double inStaveOverlap = 2 * edgeDead + constants::OT::rowActiveOverlap; + const double staveWidth = 2 * constants::OT::fpc::width - inStaveOverlap; + + // One eta half-barrel = one row of modules, length set by the FPC. + const double lengthHalfBarrel = 2 * getRowHalfLength(); + + // The envelope reaches past the last module to hold the end-of-stave cards. + const double halfLength = lengthHalfBarrel + constants::OT::barrelHalvesZGap / 2 + constants::OT::eosCard::zGap + constants::OT::eosCard::length; + + // Cut on the vertical plane (x = 0) and at mid-rapidity into four quarter barrels. The + // envelope is slotted along both cuts so the separation walls run continuously in r; the + // slots clear the walls only, the staves stand back by barrelWallClearance. + const double wallThickness = TRKBaseParam::Instance().otBarrelWallThickness; + const bool hasWalls = wallThickness > 0.; + const double slotHalfWidth = wallThickness / 2 + constants::OT::barrelWallSlotMargin; + // The two mid-rapidity walls sit back to back, so the z slot must clear both. + const double zSlotHalfWidth = wallThickness + constants::OT::barrelWallSlotMargin; + if (hasWalls && zSlotHalfWidth >= constants::OT::barrelHalvesZGap / 2) { + LOGP(fatal, "TRKBase.otBarrelWallThickness = {} cm leaves no room for the staves in the {} cm gap between the eta half-barrels", + wallThickness, constants::OT::barrelHalvesZGap); + } + + auto [rMin, rMax] = getBoundingRadii(staveWidth); + TGeoMedium* medAir = gGeoManager->GetMedium("TRK_AIR$"); + TGeoShape* layer = nullptr; + if (hasWalls) { + const std::string tubeName = mLayerName + "_envelopesh"; + const std::string slotName = mLayerName + "_wallslotsh"; + const std::string zSlotName = mLayerName + "_midslotsh"; + new TGeoTube(tubeName.c_str(), rMin, rMax, halfLength); + new TGeoBBox(slotName.c_str(), slotHalfWidth, rMax + 1., halfLength + 1.); + new TGeoBBox(zSlotName.c_str(), rMax + 1., rMax + 1., zSlotHalfWidth); + layer = new TGeoCompositeShape((mLayerName + "sh").c_str(), (tubeName + "-" + slotName + "-" + zSlotName).c_str()); + } else { + layer = new TGeoTube(rMin, rMax, halfLength); + } + TGeoVolume* layerVol = new TGeoVolume(mLayerName.c_str(), layer, medAir); + layerVol->SetLineColor(kYellow); + + const double avgRadius = 0.5 * (mInnerRadius + mOuterRadius); + + // Arc lost at each of the two azimuthal cuts: the wall plus the passive stave edge, + // never less than the bare chip-to-chip gap. + const double accGap = std::max(constants::OT::halfBarrelChipGap + 2 * constants::moduleMLOT::chip::passiveEdgeReadOut, + 2 * (wallThickness / 2 + constants::OT::barrelWallClearance + edgeDead)); + + // Smallest even count still leaving rowActiveOverlap between neighbours: the two boundary + // staves take activeStaveWidth + accGap each, so only nStaves - 2 junctions share the rest. + const double activeStaveWidth = staveWidth - 2 * edgeDead; + int nStavesHalfBarrel = (int)std::ceil(2. + (avgRadius * 2 * TMath::Pi() - 2 * (activeStaveWidth + accGap)) / + (activeStaveWidth - constants::OT::rowActiveOverlap)); + nStavesHalfBarrel += nStavesHalfBarrel % 2; + + const int nHalf = nStavesHalfBarrel / 2; + const double thetaGap = (activeStaveWidth + accGap) / avgRadius; + const double thetaInt = (2. * TMath::Pi() - 2. * thetaGap) / (nStavesHalfBarrel - 2); + const double overlap = activeStaveWidth - avgRadius * thetaInt; + LOGP(info, "Creating realistic OT layer {}: {} staves/half-barrel, internal overlap {} mm, boundary gap {} mm, flipped={}", + mLayerNumber, nStavesHalfBarrel, overlap * 10, accGap * 10, mIsFlipped); + + const int nStaves = nStavesHalfBarrel * 2; + + for (int iStave = 0; iStave < nStaves; iStave++) { + int whichHalfBarrel = iStave / nStavesHalfBarrel; + int sInHB = iStave % nStavesHalfBarrel; + int azHalf = sInHB / nHalf; + int sInAz = sInHB % nHalf; + + // Stave centres placed so the boundary gaps land on the cut plane, keeping the + // region where the beam-pipe supports run clear of staves in both half-barrels. + const double phiCut = TMath::Pi() / 2; + double phi = phiCut + azHalf * TMath::Pi() + thetaGap / 2 + sInAz * thetaInt; + + TGeoRotation* rot = new TGeoRotation("rot"); + rot->RotateX(180.); // cooling pipe faces the larger-R side (inner for the flipped layer); keeps local phi + if (whichHalfBarrel == 1) { + rot->RotateY(180.); + } + if (mIsFlipped) { + rot->RotateZ(180.); + } + rot->RotateZ(phi * TMath::RadToDeg() + 90 + (whichHalfBarrel == 0 ? +1 : -1) * mTiltAngle); + + double zPos = (whichHalfBarrel == 0 ? -1 : 1) * (0.5 * lengthHalfBarrel + constants::OT::barrelHalvesZGap / 2); + TGeoCombiTrans* trans = new TGeoCombiTrans(); + trans->SetRotation(rot); + trans->SetTranslation(avgRadius * std::cos(phi), avgRadius * std::sin(phi), zPos); + layerVol->AddNode(createStave(), iStave, trans); + } + + // Support half-rings carrying the stave space frames, centred on the cooling pipe radius. + // One per quarter barrel per z end (mid-rapidity and under the end-of-stave cards): 8 per layer. + const double ringRMid = avgRadius + (mIsFlipped ? -1. : 1.) * constants::OT::coolingPipe::rLocalOffset; + const double ringRMin = ringRMid - constants::OT::supportRing::radialHeight / 2; + const double ringRMax = ringRMid + constants::OT::supportRing::radialHeight / 2; + // Stand off the cut plane by the same clearance the boundary staves keep. + const double ringDPhi = TMath::RadToDeg() * + std::asin((wallThickness / 2 + constants::OT::barrelWallClearance) / ringRMin); + const double zRingMid = wallThickness + constants::OT::supportRing::zClearance + + constants::OT::supportRing::zWidth / 2; + const double zRingEos = lengthHalfBarrel + constants::OT::barrelHalvesZGap / 2 + + constants::OT::eosCard::zGap + constants::OT::eosCard::length / 2; + + for (int azHalf = 0; azHalf < 2; ++azHalf) { + TGeoVolume* ringVol = createSupportRing(ringRMin, ringRMax, + 90. + 180. * azHalf + ringDPhi, + 270. + 180. * azHalf - ringDPhi, azHalf); + int iRing = 0; + for (int whichHalfBarrel = 0; whichHalfBarrel < 2; ++whichHalfBarrel) { + const double zSign = (whichHalfBarrel == 0) ? -1. : 1.; + for (double zAbs : {zRingMid, zRingEos}) { + layerVol->AddNode(ringVol, iRing++, new TGeoTranslation(0., 0., zSign * zAbs)); + } + } + } + + motherVolume->AddNode(layerVol, 1, nullptr); +} + +std::pair TRKOTLayerRealistic::getBoundingRadii(double staveWidth) const +{ + auto [radiusMin, radiusMax] = TRKSegmentedLayer::getBoundingRadii(staveWidth); + const float connectorReach = constants::OT::sensorThickness / 2 + constants::OT::fpc::thickness + constants::OT::connector::thickness; + const float pipeOuterReach = constants::OT::coolingPipe::rLocalOffset + constants::OT::coolingPipe::rOuter; + const float ringReach = constants::OT::coolingPipe::rLocalOffset + constants::OT::supportRing::radialHeight / 2; + const float outerReach = std::max(pipeOuterReach, ringReach); + const float margin = 0.1f; + if (!mIsFlipped) { + return {radiusMin - connectorReach - margin, radiusMax + outerReach + margin}; + } + return {radiusMin - outerReach - margin, radiusMax + connectorReach + margin}; +} // ClassImp(TRKLayer); } // namespace trk diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx index fb436cd473021..5e8e3d025c719 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx @@ -13,11 +13,14 @@ #include #include +#include #include #include #include #include +#include #include +#include #include #include @@ -25,7 +28,9 @@ #include +#include #include +#include namespace o2 { @@ -146,6 +151,7 @@ void TRKServices::createServices(TGeoVolume* motherVolume) } createMLServicesPeacock(vol); createOTServicesPeacock(vol); + createOTBarrelWalls(vol); } } @@ -615,9 +621,9 @@ void TRKServices::createMLServicesPeacock(TGeoVolume* motherVolume) // Carbon Fiber Cylinder support for the middle tracker // (from ICD_ALICE3_V3.b.3 drawing: 38.5 cm are allocated for staves and services, + 1 cm for the support; we assume less for the support - to be reconsidered if necessary) - float rMinMiddleCarbonSupport = 39.3f; // cm - float rMaxMiddleCarbonSupport = 39.5f; // cm, assume 2 mm of carbon fiber, ~0.88% X/X0 - const float zLengthMiddleCarbon = 282.f; // cm, to cover the full length of ML barrel and disks, from Corrado's drawing + float rMaxMiddleCarbonSupport = sMLOTShellRMax; // cm + float rMinMiddleCarbonSupport = sMLOTShellRMax - sMLOTShellThickness; // cm, 2 mm of carbon fiber, ~0.88% X/X0 + const float zLengthMiddleCarbon = 282.f; // cm, to cover the full length of ML barrel and disks, from Corrado's drawing TGeoTube* middleBarrelCarbonSupport = new TGeoTube("TRK_MID_CARBONSUPPORTsh", rMinMiddleCarbonSupport, rMaxMiddleCarbonSupport, zLengthMiddleCarbon / 2.); TGeoVolume* middleBarrelCarbonSupportVolume = new TGeoVolume("TRK_MID_CARBONSUPPORT", middleBarrelCarbonSupport, medCFiber); middleBarrelCarbonSupportVolume->SetLineColor(kGray); @@ -883,6 +889,67 @@ void TRKServices::createMLServicesPeacock(TGeoVolume* motherVolume) } } +void TRKServices::createOTBarrelWalls(TGeoVolume* motherVolume) +{ + // Closes each OT quarter barrel azimuthally (a wall in the cut plane) and at mid-rapidity + // (a half disk at z = 0); radially the two cylindrical shells already do it. Both run + // continuously between the shells, through the slots in the layer envelopes. + auto& matmgr = o2::base::MaterialManager::Instance(); + TGeoMedium* medCFiber = matmgr.getTGeoMedium("ALICE3_TRKSERVICES_CARBONFIBERM55J6K"); + + // Only the realistic OT barrel is built in quarters with slotted envelopes. + if (TRKBaseParam::Instance().getLayoutMLOT() != kSimplifiedRealistic) { + LOGP(info, "OT barrel separation walls skipped: they belong to the kSimplifiedRealistic OT barrel"); + return; + } + + const float thickness = TRKBaseParam::Instance().otBarrelWallThickness; + if (thickness <= 0.f) { + LOGP(info, "OT barrel separation walls disabled (TRKBase.otBarrelWallThickness = {})", thickness); + return; + } + + const float zWallOuter = 142.0f; // cm, up to the OT barrel service disk + const float phiCut = 90.f; // deg, the vertical cut plane, as in TRKOTLayerRealistic::createLayer + + // The wall is a box, so its outer edge is pulled in until the corners, not the face, + // sit on the outer shell. + const float zLength = zWallOuter - thickness; + const float rHiBox = std::sqrt(sOTShellRMin * sOTShellRMin - thickness * thickness / 4.f); + const float rMidWall = 0.5 * (sMLOTShellRMax + rHiBox); + LOGP(info, "Creating OT barrel separation walls, {} cm of carbon fibre, continuous over r = [{}, {}] cm", thickness, sMLOTShellRMax, rHiBox); + + for (auto& orientation : {Orientation::kASide, Orientation::kCSide}) { + const std::string orLabel = (orientation == Orientation::kASide) ? "A" : "C"; + const int zSign = (int)orientation; + + for (int iSide = 0; iSide < 2; ++iSide) { + const double phi = (phiCut + iSide * 180.f) * TMath::DegToRad(); + TGeoBBox* wallSh = new TGeoBBox(Form("TRK_OT_WALL_PHIsh_%s%d", orLabel.c_str(), iSide), + (rHiBox - sMLOTShellRMax) / 2., thickness / 2., zLength / 2.); + TGeoVolume* wallVol = new TGeoVolume(Form("TRK_OT_WALL_PHI_%s%d", orLabel.c_str(), iSide), wallSh, medCFiber); + wallVol->SetLineColor(kGray); + auto* rot = new TGeoRotation("", phiCut + iSide * 180.f, 0, 0); + motherVolume->AddNode(wallVol, 1, + new TGeoCombiTrans(rMidWall * std::cos(phi), rMidWall * std::sin(phi), + zSign * (thickness + zLength / 2.), rot)); + } + } + + // One per azimuthal half and per eta half-barrel, back to back in the gap between them. + for (auto& orientation : {Orientation::kASide, Orientation::kCSide}) { + const std::string orLabel = (orientation == Orientation::kASide) ? "A" : "C"; + const int zSign = (int)orientation; + for (int iHalf = 0; iHalf < 2; ++iHalf) { + TGeoTubeSeg* diskSh = new TGeoTubeSeg(Form("TRK_OT_WALL_Z0sh_%s%d", orLabel.c_str(), iHalf), + sMLOTShellRMax, sOTShellRMin, thickness / 2., phiCut + iHalf * 180.f, phiCut + (iHalf + 1) * 180.f); + TGeoVolume* diskVol = new TGeoVolume(Form("TRK_OT_WALL_Z0_%s%d", orLabel.c_str(), iHalf), diskSh, medCFiber); + diskVol->SetLineColor(kGray); + motherVolume->AddNode(diskVol, 1, new TGeoTranslation(0, 0, zSign * thickness / 2.)); + } + } +} + void TRKServices::createOTServicesPeacock(TGeoVolume* motherVolume) { // This implments the service barrels for power + data for the OT barrels and disks @@ -935,9 +1002,9 @@ void TRKServices::createOTServicesPeacock(TGeoVolume* motherVolume) float zLengthOuterDiskServices = 201.f; // cm // Carbon Fiber Cylinder support for the middle tracker - float rMinOuterCarbonSupport = 82.0f; // TODO: get more precise location - float rMaxOuterCarbonSupport = 82.4f; // 4 mm of carbon fiber - const float zLengthOuterCarbon = 280.0f; // Rough guess for now + float rMinOuterCarbonSupport = sOTShellRMin; // TODO: get more precise location + float rMaxOuterCarbonSupport = sOTShellRMin + sOTShellThickness; // 4 mm of carbon fiber, the only load-bearing wall + const float zLengthOuterCarbon = 280.0f; // Rough guess for now TGeoTube* outerBarrelCarbonSupport = new TGeoTube("TRK_OT_CARBONSUPPORTsh", rMinOuterCarbonSupport, rMaxOuterCarbonSupport, zLengthOuterCarbon / 2.); TGeoVolume* outerBarrelCarbonSupportVolume = new TGeoVolume("TRK_OT_CARBONSUPPORT", outerBarrelCarbonSupport, medCFiber); outerBarrelCarbonSupportVolume->SetLineColor(kGray); diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKSimulationLinkDef.h b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKSimulationLinkDef.h index 282fc72becc52..76b720f0ada98 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKSimulationLinkDef.h +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKSimulationLinkDef.h @@ -22,6 +22,7 @@ #pragma link C++ class o2::trk::TRKSegmentedLayer + ; #pragma link C++ class o2::trk::TRKMLLayer + ; #pragma link C++ class o2::trk::TRKOTLayer + ; +#pragma link C++ class o2::trk::TRKOTLayerRealistic + ; #pragma link C++ class o2::trk::VDLayer + ; #pragma link C++ class o2::trk::TRKServices + ; #pragma link C++ class o2::trk::Detector + ; diff --git a/Detectors/Upgrades/ALICE3/TRK/workflow/src/ClustererSpec.cxx b/Detectors/Upgrades/ALICE3/TRK/workflow/src/ClustererSpec.cxx index f91262e021a55..b0f0932be9bff 100644 --- a/Detectors/Upgrades/ALICE3/TRK/workflow/src/ClustererSpec.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/workflow/src/ClustererSpec.cxx @@ -32,7 +32,7 @@ void ClustererDPL::init(o2::framework::InitContext& ic) void ClustererDPL::run(o2::framework::ProcessingContext& pc) { - o2::base::GeometryManager::loadGeometry("sgn_geometry.root", false, true); + o2::base::GeometryManager::loadGeometry(); // default prefix -> o2sim_geometry[-aligned].root uint64_t totalClusters = 0; for (int iLayer = 0; iLayer < mLayers; ++iLayer) {