diff --git a/README.md b/README.md index a4f1c22..1d2b35e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The SHiP geometry is described using GeoModel and is used by the simulation and | DecayVolume | Approximate | Rectangular vessel (should be frustum) | | TimingDetector | Complete | 330 scintillator bars via GeoModelXML | | UpstreamTagger | Approximate | Monolithic slab (needs bar segmentation) | -| Trackers | Envelope only | 4 empty station boxes | +| Trackers | Complete | 4 stations, 4 stereo views each, 9600 straw tubes | | Calorimeter | Simulation-ready | ECAL + HCAL sampling layers driven by `calo.toml` (Pb/PVT/HPL + Fe/PVT) | ## Building diff --git a/src/SHiPMaterials.cpp b/src/SHiPMaterials.cpp index 8af7a7f..fe808b0 100644 --- a/src/SHiPMaterials.cpp +++ b/src/SHiPMaterials.cpp @@ -218,6 +218,38 @@ void SHiPMaterials::createMaterials() { polystyrene->lock(); m_materials["Polystyrene"] = polystyrene; } + + // Mylar / PET (density 1.39 g/cm³): C10H8O4 — straw tube walls + // MW = 10*12.011 + 8*1.008 + 4*15.999 = 192.166 g/mol + { + const double awC = 12.011; + const double awH = 1.008; + const double awO = 15.999; + const double mw = 10.0 * awC + 8.0 * awH + 4.0 * awO; + GeoMaterial* mylar = + new GeoMaterial("Mylar", 1.39 * GeoModelKernelUnits::g / GeoModelKernelUnits::cm3); + mylar->add(m_elements["Carbon"], 10.0 * awC / mw); + mylar->add(m_elements["Hydrogen"], 8.0 * awH / mw); + mylar->add(m_elements["Oxygen"], 4.0 * awO / mw); + mylar->lock(); + m_materials["Mylar"] = mylar; + } + + // ArCO2_70_30 (density 1.56e-3 g/cm³): 70% Ar + 30% CO2 by mass — + // straw tube gas fill. The CO2 mass fraction is split into its C and O + // constituents (C: 12.011/44.009, O: 2*15.999/44.009 of the CO2 mass). + { + const double mwCO2 = 44.009; + const double fracAr = 0.70; + const double fracCO2 = 0.30; + GeoMaterial* arco2 = new GeoMaterial( + "ArCO2_70_30", 1.56e-3 * GeoModelKernelUnits::g / GeoModelKernelUnits::cm3); + arco2->add(m_elements["Argon"], fracAr); + arco2->add(m_elements["Carbon"], fracCO2 * 12.011 / mwCO2); + arco2->add(m_elements["Oxygen"], fracCO2 * 2.0 * 15.999 / mwCO2); + arco2->lock(); + m_materials["ArCO2_70_30"] = arco2; + } } } // namespace SHiPGeometry diff --git a/subsystems/Trackers/README.md b/subsystems/Trackers/README.md index ce08c1f..064f93d 100644 --- a/subsystems/Trackers/README.md +++ b/subsystems/Trackers/README.md @@ -4,40 +4,110 @@ Straw tube tracking stations. ## Description -The Trackers subsystem implements 4 tracking stations for the SHiP spectrometer. Each station currently consists of an empty air box at the correct z-position. The full implementation requires straw tube modules with individual straws, stereo views, and support structures. +The Trackers subsystem implements 4 straw tracking stations for the SHiP +spectrometer. Each station envelope is filled with 4 stereo views; each +view is a material frame (FairShip-style hollow rectangle) enclosing a +staggered double sub-layer of straw tubes. -## Geometry Tree +A straw is a Mylar-walled cylinder filled with an Ar/CO₂ gas mixture. The +four views per station are rotated about the beam axis by alternating +stereo angles so that crossing straws give a space point. + +The spectrometer dipole between stations 2 and 3 is a separate subsystem +(see `subsystems/Magnet`) and is not described here. + +## Geometry tree ``` -TrackersContainer (Air, 6000×6860×6000 mm) - ├─ TrackerStation_1 (Air, 6000×6860×1000 mm) z = 84070 mm - ├─ TrackerStation_2 (Air) z = 86070 mm - ├─ TrackerStation_3 (Air) z = 93070 mm - └─ TrackerStation_4 (Air) z = 95070 mm +/SHiP/trackers (Air, 3000 × 3430 × 6000 mm half-extents) + ├─ /SHiP/trackers/station_ (Air, 3000 × 3430 × 500 mm) n = 1..4 + │ └─ /SHiP/trackers/station_/view_/envelope (Air) v = 0..3 + │ (rotated about Z by the view stereo angle) + │ ├─ .../frame_body (Aluminium, hollow rectangle = outer − aperture) + │ ├─ .../sublayer_0_body (Air slab, 300 straws) + │ └─ .../sublayer_1_body (Air slab, 300 straws, half-pitch staggered) + │ └─ .../straw_ + │ └─ straw_wall_ (Mylar tube) + │ └─ straw_gas_ (ArCO2_70_30 tube) + └─ /SHiP/trackers/tracker_magnet (Air, inert marker box) ``` +Station Z positions (centres): 84070, 86070, 93070, 95070 mm. Position in world: centred at z = 89570 mm (average of stations 1 and 4). Stations 1-2 are upstream of the magnet, stations 3-4 downstream. +## Tracker magnet + +`/SHiP/trackers/tracker_magnet` is an inert, air-filled marker volume for +the tracker magnet, centred at z = 86820 mm with a 460 mm Z extent. + +It is **not** a physically-scaled dipole. The spectrometer dipole proper is +the separate `Magnet` subsystem (iron yoke + coils) occupying z = 87.07 to +92.07 m. The only space inside the trackers container that is clear of both +the straw stations and that yoke is the ~0.5 m gap between station 2 and the +yoke, so the marker is sized and placed to fit there without overlap. + +The marker exists so the tracker magnet has a named placeholder in the +geometry; simulation/field code can locate it by its log-volume name. A +physically-scaled magnet, if required, belongs in the `Magnet` subsystem. + +## Parameters + +| Parameter | Value | +| --- | --- | +| Stations | 4 | +| Stereo views per station | 4 | +| Sub-layers per view | 2 (half-pitch staggered) | +| Straws per sub-layer | 300 | +| Straws total | 4 × 4 × 2 × 300 = 9600 | +| Straw outer diameter | 20 mm | +| Straw length | 4000 mm (along X) | +| Straw wall | 30 µm Mylar | +| Straw fill | Ar/CO₂ 70/30 by mass | +| View aperture | 4000 × 6000 mm (X × Y) | +| View stereo angles | +2.3°, −2.3°, +2.3°, −2.3° about Z | +| Frame bar width | 100 mm (X and Y) | +| Frame material | Aluminium | + ## Materials -| Material | Density | Usage | -|----------|-------------|----------------------| -| Air | 1.29 mg/cm³ | Container & stations | +| Material | Density | Composition | Notes | +| --- | --- | --- | --- | +| Air | 1.29 mg/cm³ | already in catalog | container, station, view, sub-layer envelopes | +| Aluminium | 2.70 g/cm³ | already in catalog | view frames | +| Mylar | 1.39 g/cm³ | C₁₀H₈O₄, mass-fraction-normalised | straw walls | +| ArCO2_70_30 | 1.56 mg/cm³ | 70/30 Ar/CO₂ by mass | straw gas fill | + +Mylar and ArCO2_70_30 are added by this subsystem to the central +`SHiPMaterials` catalog; Air and Aluminium were already present. All +elements required (C, H, O, Ar) were already in the element catalog. + +## Tests + +`test_trackers.cpp` exercises: + +- `TrackersWithinEnvelope` — station 1 exists and its box stays within the + CSV envelope limits (≤ 3000 × 3500 × 500 mm half-extents). +- `TrackersHasFourStations` — all 4 station volumes are present. +- `TrackersStationHasViews` — each station holds 4 stereo views. +- `TrackersViewHasFrameAndSubLayers` — a view holds a frame plus two + sub-layers, and each sub-layer carries the full straw count. +- `TrackersHasTrackerMagnet` — the inert `tracker_magnet` marker exists and + fits in the gap before the spectrometer-magnet yoke (no overlap). ## Status -- [x] C++ implementation (envelope only) -- [ ] Implement straw tube geometry -- [ ] Add stereo views and support structures -- [ ] Verification against GDML +- [x] C++ implementation (straw-level geometry) +- [x] 4 stations × 4 stereo views +- [x] Straw tubes (Mylar wall + Ar/CO₂ gas) +- [x] Staggered double sub-layers +- [x] FairShip-style view frames +- [x] Inert TrackerMagnet marker volume +- [x] Mylar and ArCO2_70_30 materials +- [ ] Verification against a reference GDML ## TODO -- Implement straw tube modules within each station (major work) - - Individual straw tubes (mylar + gas) - - 4 views per station (Y, U, V, Y') with stereo angles - - Support frames and service volumes -- Add straw tube gas material (Ar/CO2 mixture) to SHiPMaterials -- Add mylar material to SHiPMaterials -- Verify station positions against GDML reference +- Verify straw pitch, view spacing and station positions against the GDML + reference once the tracker design is fixed. +- Add support frames and service volumes beyond the per-view frame. diff --git a/subsystems/Trackers/include/Trackers/TrackersFactory.h b/subsystems/Trackers/include/Trackers/TrackersFactory.h index 51551ea..e72cabe 100644 --- a/subsystems/Trackers/include/Trackers/TrackersFactory.h +++ b/subsystems/Trackers/include/Trackers/TrackersFactory.h @@ -3,6 +3,9 @@ #pragma once +#include +#include + class GeoPhysVol; namespace SHiPGeometry { @@ -10,14 +13,25 @@ namespace SHiPGeometry { class SHiPMaterials; /** - * @brief Factory for the Trackers (straw tube tracking stations) geometry + * @brief Factory for the Trackers (straw tube tracking stations) geometry. * - * Creates 4 tracking stations from GDML reference (statbox solid): + * Builds 4 straw tracking stations for the SHiP spectrometer. Station + * envelopes and z-positions follow the GDML reference / subsystem_envelopes.csv: * - Station 1: Z 83.57-84.57 m → centre 84.07 m * - Station 2: Z 85.57-86.57 m → centre 86.07 m * - Station 3: Z 92.57-93.57 m → centre 93.07 m * - Station 4: Z 94.57-95.57 m → centre 95.07 m - * GDML statbox: x=600 cm, y=686 cm, z=100 cm → half: 300×343×50 cm + * Station envelope (GDML statbox): half 3000 × 3430 × 500 mm. + * + * Each station envelope is filled with 4 stereo views. A view is a material + * frame (FairShip-style hollow rectangle) enclosing a staggered double + * sub-layer of straw tubes: + * - Straw: 20 mm outer diameter, 4 m long, horizontal (along X). + * - Wall: 30 µm Mylar; fill: Ar/CO₂ 70/30 by mass. + * - View stereo angles about the beam axis Z: +2.3°, -2.3°, +2.3°, -2.3°. + * + * The spectrometer dipole between stations 2 and 3 is a separate subsystem + * (see subsystems/Magnet) and is intentionally not described here. */ class TrackersFactory { public: @@ -25,28 +39,115 @@ class TrackersFactory { ~TrackersFactory() = default; /** - * @brief Build the Trackers geometry - * @return Pointer to container volume with all 4 stations + * @brief Build the Trackers geometry. + * @return Pointer to the container volume holding all 4 stations. */ GeoPhysVol* build(); + // ── Straw / view geometry constants (mm) ──────────────────────────── + static constexpr int s_nStations = 4; + static constexpr int s_nViews = 4; ///< stereo views per station + static constexpr int s_nSubLayers = 2; ///< staggered straw layers per view + + static constexpr double s_strawRadius = 10.0; ///< 1 cm radius (2 cm diam) + static constexpr double s_strawLength = 4000.0; ///< 4 m, along X + static constexpr double s_wallThickness = 0.030; ///< 30 µm Mylar wall + + /// Active aperture inside a view frame (X = straw length region, Y = pitch). + static constexpr double s_apertureX = 4000.0; + static constexpr double s_apertureY = 6000.0; + + /// Straws per sub-layer (aperture height / straw diameter). + static constexpr int s_nStraws = static_cast(s_apertureY / (2.0 * s_strawRadius)); + + static constexpr double s_stereoAngleDeg = 2.3; ///< |stereo angle| per view + + // View frame (FairShip-style hollow rectangle). + static constexpr double s_frameBarX = 100.0; ///< frame bar width in X + static constexpr double s_frameBarY = 100.0; ///< frame bar width in Y + static constexpr double s_frameHalfZ = 22.0; ///< frame half-thickness in Z + + // ── Tracker magnet ────────────────────────────────────────────────── + // An inert, air-filled marker volume named "TrackerMagnet", placed in + // the clear gap between station 2 and the spectrometer-magnet yoke. + // + // NOTE: this is NOT a physically-scaled dipole. The spectrometer dipole + // proper is the separate Magnet subsystem (iron yoke + coils) occupying + // z = 87.07-92.07 m. The only free space inside the trackers container + // and clear of that yoke is a ~0.5 m gap, so this marker is sized to fit + // there. It exists so the tracker magnet has a named placeholder in the + // geometry; simulation/field code can locate it by the name + // "/SHiP/trackers/tracker_magnet". + static constexpr double s_trackerMagnetZ = 86820.0; ///< centre, mm + static constexpr double s_trackerMagnetHalfZ = 230.0; ///< half-depth, mm + private: SHiPMaterials& m_materials; - // Station dimensions from GDML statbox (mm) + /// Frame material name in the central SHiPMaterials catalogue. + std::string m_frameMaterialName = "Aluminium"; + + // ── Station envelope from GDML statbox (mm) ───────────────────────── static constexpr double s_halfX = 3000.0; // 300 cm - static constexpr double s_halfY = 3430.0; // 343 cm (GDML y=686 cm) + static constexpr double s_halfY = 3430.0; // 343 cm (GDML y = 686 cm) static constexpr double s_halfZ = 500.0; // 50 cm - // Station Z positions (centres, in mm from origin) + // Station Z positions (centres, mm from origin). static constexpr double s_station1Z = 84070.0; // 84.07 m static constexpr double s_station2Z = 86070.0; // 86.07 m static constexpr double s_station3Z = 93070.0; // 93.07 m static constexpr double s_station4Z = 95070.0; // 95.07 m - // Container dimensions (spans all stations) + // Container dimensions (spans all stations). static constexpr double s_containerHalfZ = (s_station4Z - s_station1Z) / 2.0 + s_halfZ; static constexpr double s_containerCentreZ = (s_station1Z + s_station4Z) / 2.0; + + // ── Internal builders ─────────────────────────────────────────────── + + /** + * @brief Build one station envelope and place its 4 stereo views. + * @param stationIndex 0-based station index (0..3). + * @return The station envelope physical volume. + */ + GeoPhysVol* buildStation(int stationIndex); + + /** + * @brief Build one stereo view: a frame plus two staggered sub-layers. + * @param stationIndex 0-based parent station index, used for unique names. + * @param viewIndex 0-based view index within the station (0..3). + * @return The (unrotated) view physical volume. + */ + GeoPhysVol* buildView(int stationIndex, int viewIndex); + + /** + * @brief Build the FairShip-style hollow-rectangle frame for one view. + * @param stationIndex 0-based parent station index, used for unique names. + * @param viewIndex 0-based view index within the station. + */ + GeoPhysVol* buildFrame(int stationIndex, int viewIndex); + + /** + * @brief Build one sub-layer of s_nStraws parallel straws. + * @param stationIndex 0-based parent station index, used for unique names. + * @param viewIndex 0-based parent view index. + * @param shifted true for the half-pitch-staggered sub-layer. + */ + GeoPhysVol* buildSubLayer(int stationIndex, int viewIndex, bool shifted); + + /** + * @brief Build a single straw: a Mylar wall cylinder with a gas core. + * @param uid Globally unique straw identifier, used for unique names. + */ + GeoPhysVol* buildStraw(int uid); + + /** + * @brief Build the inert TrackerMagnet marker volume (air-filled box). + * + * Placed in the gap between station 2 and the spectrometer-magnet yoke. + * See the note on s_trackerMagnetZ for why this is a marker rather than + * a physically-scaled dipole. + */ + GeoPhysVol* buildTrackerMagnet(); }; } // namespace SHiPGeometry diff --git a/subsystems/Trackers/src/TrackersFactory.cpp b/subsystems/Trackers/src/TrackersFactory.cpp index 6171857..dfbed54 100644 --- a/subsystems/Trackers/src/TrackersFactory.cpp +++ b/subsystems/Trackers/src/TrackersFactory.cpp @@ -11,42 +11,290 @@ #include #include #include +#include #include +#include +#include +#include #include namespace SHiPGeometry { +using namespace GeoModelKernelUnits; + +// ── file-scope geometry helpers ────────────────────────────────────────────── +namespace { + +// Clearances (mm). The view aperture is a little larger than the nominal +// straw pattern so the staggered sub-layer and the straw outer radius fit +// inside; the view envelope is a little larger than the frame outer box. +constexpr double kApertureClearX = 5.0; +constexpr double kApertureClearY = 15.0; +constexpr double kEnvClearance = 5.0; + +// View aperture (inner frame hole) half-sizes. +constexpr double kApertureHalfX = TrackersFactory::s_apertureX / 2.0 + kApertureClearX; // 2005 +constexpr double kApertureHalfY = TrackersFactory::s_apertureY / 2.0 + kApertureClearY; // 3015 + +// Frame outer half-sizes. +constexpr double kFrameHalfX = kApertureHalfX + TrackersFactory::s_frameBarX; // 2105 +constexpr double kFrameHalfY = kApertureHalfY + TrackersFactory::s_frameBarY; // 3115 + +// View envelope half-sizes. +constexpr double kViewHalfX = kFrameHalfX + kEnvClearance; // 2110 +constexpr double kViewHalfY = kFrameHalfY + kEnvClearance; // 3120 +constexpr double kViewHalfZ = TrackersFactory::s_frameHalfZ + kEnvClearance; // 27 + +// Small gap between the frame aperture and the sub-layer envelope. +constexpr double kFrameClearance = 0.5; + +// Signed stereo angle for a view: views 0,2 → +, views 1,3 → -. +double stereoSignedDeg(int viewIndex) { + const double sign = (viewIndex % 2 == 0) ? +1.0 : -1.0; + return sign * TrackersFactory::s_stereoAngleDeg; +} + +} // namespace + +// ── constructor ────────────────────────────────────────────────────────────── + TrackersFactory::TrackersFactory(SHiPMaterials& materials) : m_materials(materials) {} +// ── build ──────────────────────────────────────────────────────────────────── + GeoPhysVol* TrackersFactory::build() { const GeoMaterial* air = m_materials.requireMaterial("Air"); - // Create container volume that spans all 4 stations + // Container volume spanning all 4 stations. auto* containerBox = new GeoBox(s_halfX, s_halfY, s_containerHalfZ); auto* containerLog = new GeoLogVol("/SHiP/trackers", containerBox, air); auto* containerPhys = new GeoPhysVol(containerLog); - // Create and place individual stations - const double stationZ[4] = {s_station1Z, s_station2Z, s_station3Z, s_station4Z}; + const std::array stationZ = {s_station1Z, s_station2Z, s_station3Z, + s_station4Z}; - for (int i = 0; i < 4; ++i) { - auto* stationBox = new GeoBox(s_halfX, s_halfY, s_halfZ); - std::string stationName = "/SHiP/trackers/station_" + std::to_string(i + 1); - auto* stationLog = new GeoLogVol(stationName, stationBox, air); - auto* stationPhys = new GeoPhysVol(stationLog); + for (int i = 0; i < s_nStations; ++i) { + GeoPhysVol* stationPhys = buildStation(i); - // Position relative to container centre - double relativeZ = stationZ[i] - s_containerCentreZ; - GeoTrf::Transform3D stationTrf = GeoTrf::Translate3D(0.0, 0.0, relativeZ); + // Place the station relative to the container centre. + const double relativeZ = stationZ[i] - s_containerCentreZ; + const GeoTrf::Transform3D stationTrf = GeoTrf::Translate3D(0.0, 0.0, relativeZ); + // Name kept as "/SHiP/trackers/station_" for downstream lookups. + const std::string stationName = "/SHiP/trackers/station_" + std::to_string(i + 1); containerPhys->add(new GeoNameTag(stationName)); containerPhys->add(new GeoIdentifierTag(i)); containerPhys->add(new GeoTransform(stationTrf)); containerPhys->add(stationPhys); } + // Inert tracker-magnet marker, in the gap between station 2 and the + // spectrometer-magnet yoke (see buildTrackerMagnet / s_trackerMagnetZ). + { + GeoPhysVol* trackerMagnet = buildTrackerMagnet(); + const double relativeZ = s_trackerMagnetZ - s_containerCentreZ; + containerPhys->add(new GeoNameTag("/SHiP/trackers/tracker_magnet")); + containerPhys->add(new GeoIdentifierTag(s_nStations)); + containerPhys->add(new GeoTransform(GeoTrf::Translate3D(0.0, 0.0, relativeZ))); + containerPhys->add(trackerMagnet); + } + return containerPhys; } +// ── buildStation ───────────────────────────────────────────────────────────── + +GeoPhysVol* TrackersFactory::buildStation(int stationIndex) { + const GeoMaterial* air = m_materials.requireMaterial("Air"); + + // Station envelope: fixed GDML statbox size. Kept exactly so the geometry + // consistency test (station box <= 3000 x 3500 x 500 mm) still passes. + const std::string stationName = "/SHiP/trackers/station_" + std::to_string(stationIndex + 1); + auto* stationBox = new GeoBox(s_halfX, s_halfY, s_halfZ); + auto* stationLog = new GeoLogVol(stationName, stationBox, air); + auto* stationPhys = new GeoPhysVol(stationLog); + + // Four stereo views, stacked along Z within the station, each rotated + // about the beam axis by its signed stereo angle. + const double viewGap = 5.0; + const double viewPitch = 2.0 * kViewHalfZ + viewGap; + + for (int v = 0; v < s_nViews; ++v) { + GeoPhysVol* viewPhys = buildView(stationIndex, v); + + const double zView = -0.5 * (s_nViews - 1) * viewPitch + v * viewPitch; + const double angleRad = stereoSignedDeg(v) * M_PI / 180.0; + const GeoTrf::Transform3D viewTrf = + GeoTrf::Translate3D(0.0, 0.0, zView) * GeoTrf::RotateZ3D(angleRad); + + const std::string viewName = stationName + "/view_" + std::to_string(v); + stationPhys->add(new GeoNameTag(viewName)); + stationPhys->add(new GeoIdentifierTag(v)); + stationPhys->add(new GeoTransform(viewTrf)); + stationPhys->add(viewPhys); + } + + return stationPhys; +} + +// ── buildView ──────────────────────────────────────────────────────────────── + +GeoPhysVol* TrackersFactory::buildView(int stationIndex, int viewIndex) { + // The stereo rotation is applied by the parent station; the view is built + // unrotated here. It holds a material frame plus two staggered sub-layers. + const GeoMaterial* air = m_materials.requireMaterial("Air"); + + const std::string viewName = "/SHiP/trackers/station_" + std::to_string(stationIndex + 1) + + "/view_" + std::to_string(viewIndex); + + auto* viewBox = new GeoBox(kViewHalfX, kViewHalfY, kViewHalfZ); + auto* viewLog = new GeoLogVol(viewName + "/envelope", viewBox, air); + auto* viewPhys = new GeoPhysVol(viewLog); + + // Material frame. + { + GeoPhysVol* framePhys = buildFrame(stationIndex, viewIndex); + viewPhys->add(new GeoNameTag(viewName + "/frame")); + viewPhys->add(new GeoIdentifierTag(0)); + viewPhys->add(new GeoTransform(GeoTrf::Transform3D::Identity())); + viewPhys->add(framePhys); + } + + // Two sub-layers of straws. Centres at z = ±(strawRadius + 0.55) mm so the + // two sub-layer envelopes do not overlap each other at z = 0. + const double dz = s_strawRadius + 0.55; + + { + GeoPhysVol* sub0 = buildSubLayer(stationIndex, viewIndex, false); + viewPhys->add(new GeoNameTag(viewName + "/sublayer_0")); + viewPhys->add(new GeoIdentifierTag(0)); + viewPhys->add(new GeoTransform(GeoTrf::Translate3D(0.0, 0.0, -dz))); + viewPhys->add(sub0); + } + { + GeoPhysVol* sub1 = buildSubLayer(stationIndex, viewIndex, true); + viewPhys->add(new GeoNameTag(viewName + "/sublayer_1")); + viewPhys->add(new GeoIdentifierTag(1)); + viewPhys->add(new GeoTransform(GeoTrf::Translate3D(0.0, 0.0, +dz))); + viewPhys->add(sub1); + } + + return viewPhys; +} + +// ── buildFrame ─────────────────────────────────────────────────────────────── + +GeoPhysVol* TrackersFactory::buildFrame(int stationIndex, int viewIndex) { + const GeoMaterial* frameMat = m_materials.requireMaterial(m_frameMaterialName); + + // Hollow rectangle = outer box minus aperture box. The inner box is made + // slightly thicker in Z so the subtraction punches cleanly through. + auto* outerBox = new GeoBox(kFrameHalfX, kFrameHalfY, s_frameHalfZ); + auto* innerBox = new GeoBox(kApertureHalfX, kApertureHalfY, s_frameHalfZ + 1.0); + auto* frameShape = new GeoShapeSubtraction(outerBox, innerBox); + + const std::string frameName = "/SHiP/trackers/station_" + std::to_string(stationIndex + 1) + + "/view_" + std::to_string(viewIndex) + "/frame_body"; + auto* frameLog = new GeoLogVol(frameName, frameShape, frameMat); + auto* framePhys = new GeoPhysVol(frameLog); + + return framePhys; +} + +// ── buildSubLayer ──────────────────────────────────────────────────────────── + +GeoPhysVol* TrackersFactory::buildSubLayer(int stationIndex, int viewIndex, bool shifted) { + // A sub-layer is a thin air slab holding s_nStraws straws. Both the + // nominal and the shifted sub-layer share the same symmetric XY footprint; + // the half-pitch Y stagger is applied here, per straw, via `shifted`. + const GeoMaterial* air = m_materials.requireMaterial("Air"); + + const double pitch = 2.0 * s_strawRadius; // 20 mm + const double yStagger = shifted ? s_strawRadius : 0.0; // +10 mm if shifted + + const double subHalfX = kApertureHalfX - kFrameClearance; + const double subHalfY = kApertureHalfY - kFrameClearance; + const double subHalfZ = s_strawRadius + 0.5; + + const std::string subName = "/SHiP/trackers/station_" + std::to_string(stationIndex + 1) + + "/view_" + std::to_string(viewIndex) + "/sublayer_" + + (shifted ? "1" : "0") + "_body"; + auto* subBox = new GeoBox(subHalfX, subHalfY, subHalfZ); + auto* subLog = new GeoLogVol(subName, subBox, air); + auto* subPhys = new GeoPhysVol(subLog); + + // Globally-unique straw id seed: keeps every straw log-volume name unique + // across the whole subsystem (station, view, sub-layer, straw). + const int subLayerOrdinal = + ((stationIndex * s_nViews) + viewIndex) * s_nSubLayers + (shifted ? 1 : 0); + int strawUid = subLayerOrdinal * s_nStraws; + + const double yStart = -(s_nStraws - 1) * 0.5 * pitch; + + for (int i = 0; i < s_nStraws; ++i) { + const double yStraw = yStart + i * pitch + yStagger; + + // GeoTube axis is local Z; rotate it to lie along X (the straw axis). + const GeoTrf::Transform3D strawTrf = + GeoTrf::Translate3D(0.0, yStraw, 0.0) * GeoTrf::RotateY3D(M_PI / 2.0); + + subPhys->add(new GeoNameTag(subName + "/straw_" + std::to_string(i))); + subPhys->add(new GeoIdentifierTag(i)); + subPhys->add(new GeoTransform(strawTrf)); + subPhys->add(buildStraw(strawUid++)); + } + + return subPhys; +} + +// ── buildStraw ─────────────────────────────────────────────────────────────── + +GeoPhysVol* TrackersFactory::buildStraw(int uid) { + // A straw is a solid Mylar cylinder (the wall) with a gas daughter that + // fills the interior. Modelling the wall as a solid cylinder rather than a + // hollow tube keeps the gas fully contained, with no mother-daughter + // overlap. GeoTube axis is local Z; the parent sub-layer rotates it to X. + const GeoMaterial* mylar = m_materials.requireMaterial("Mylar"); + const GeoMaterial* gas = m_materials.requireMaterial("ArCO2_70_30"); + + const double rGas = s_strawRadius - s_wallThickness; + const double rWall = s_strawRadius; + const double halfLength = s_strawLength / 2.0; + + auto* wallTube = new GeoTube(0.0, rWall, halfLength); + auto* wallLog = + new GeoLogVol("/SHiP/trackers/straw_wall_" + std::to_string(uid), wallTube, mylar); + auto* wallPhys = new GeoPhysVol(wallLog); + + auto* gasTube = new GeoTube(0.0, rGas, halfLength); + auto* gasLog = new GeoLogVol("/SHiP/trackers/straw_gas_" + std::to_string(uid), gasTube, gas); + auto* gasPhys = new GeoPhysVol(gasLog); + + wallPhys->add(new GeoNameTag("/SHiP/trackers/straw_gas")); + wallPhys->add(new GeoIdentifierTag(0)); + wallPhys->add(new GeoTransform(GeoTrf::Transform3D::Identity())); + wallPhys->add(gasPhys); + + return wallPhys; +} + +// ── buildTrackerMagnet ─────────────────────────────────────────────────────── + +GeoPhysVol* TrackersFactory::buildTrackerMagnet() { + // Inert, air-filled marker for the tracker magnet. It is deliberately + // sized to fit the ~0.5 m gap between station 2 and the spectrometer + // magnet yoke, so it does not overlap the separate Magnet subsystem. + // It is NOT a physically-scaled dipole — it gives the tracker magnet a + // named placeholder volume that simulation/field code can locate by the + // log-volume name "/SHiP/trackers/tracker_magnet". + const GeoMaterial* air = m_materials.requireMaterial("Air"); + + auto* box = new GeoBox(s_halfX, s_halfY, s_trackerMagnetHalfZ); + auto* log = new GeoLogVol("/SHiP/trackers/tracker_magnet", box, air); + auto* phys = new GeoPhysVol(log); + + return phys; +} + } // namespace SHiPGeometry diff --git a/subsystems/Trackers/test_trackers.cpp b/subsystems/Trackers/test_trackers.cpp index f0d1c11..5d5d687 100644 --- a/subsystems/Trackers/test_trackers.cpp +++ b/subsystems/Trackers/test_trackers.cpp @@ -13,6 +13,7 @@ #include using SHiPGeometry::SHiPMaterials; +using SHiPGeometry::TrackersFactory; static const GeoVPhysVol* findChild(const GeoVPhysVol* parent, const std::string& name) { for (unsigned int i = 0; i < parent->getNChildVols(); ++i) { @@ -27,7 +28,7 @@ static const GeoVPhysVol* findChild(const GeoVPhysVol* parent, const std::string // CSV limits: Trackers per-station halfX ≤ 3000, halfY ≤ 3500, halfZ ≤ 500 TEST_CASE("TrackersWithinEnvelope", "[trackers]") { SHiPMaterials materials; - SHiPGeometry::TrackersFactory factory(materials); + TrackersFactory factory(materials); GeoPhysVol* tc = factory.build(); REQUIRE(tc != nullptr); const GeoVPhysVol* st1 = findChild(tc, "/SHiP/trackers/station_1"); @@ -39,3 +40,65 @@ TEST_CASE("TrackersWithinEnvelope", "[trackers]") { CHECK(box->getYHalfLength() <= 3500.0); CHECK(box->getZHalfLength() <= 500.0); } + +// The container holds all 4 stations. +TEST_CASE("TrackersHasFourStations", "[trackers]") { + SHiPMaterials materials; + TrackersFactory factory(materials); + GeoPhysVol* tc = factory.build(); + REQUIRE(tc != nullptr); + for (int i = 1; i <= 4; ++i) { + const std::string name = "/SHiP/trackers/station_" + std::to_string(i); + INFO("missing station: " << name); + CHECK(findChild(tc, name) != nullptr); + } +} + +// Each station is now populated with 4 stereo views (no longer an empty box). +TEST_CASE("TrackersStationHasViews", "[trackers]") { + SHiPMaterials materials; + TrackersFactory factory(materials); + GeoPhysVol* tc = factory.build(); + REQUIRE(tc != nullptr); + const GeoVPhysVol* st1 = findChild(tc, "/SHiP/trackers/station_1"); + REQUIRE(st1 != nullptr); + CHECK(st1->getNChildVols() == static_cast(TrackersFactory::s_nViews)); +} + +// A view contains a frame plus two straw sub-layers. +TEST_CASE("TrackersViewHasFrameAndSubLayers", "[trackers]") { + SHiPMaterials materials; + TrackersFactory factory(materials); + GeoPhysVol* tc = factory.build(); + REQUIRE(tc != nullptr); + const GeoVPhysVol* st1 = findChild(tc, "/SHiP/trackers/station_1"); + REQUIRE(st1 != nullptr); + const GeoVPhysVol* view0 = findChild(st1, "/SHiP/trackers/station_1/view_0/envelope"); + REQUIRE(view0 != nullptr); + // 1 frame + 2 sub-layers + CHECK(view0->getNChildVols() == 3u); // NOLINT(readability/check) + const GeoVPhysVol* sub0 = findChild(view0, "/SHiP/trackers/station_1/view_0/sublayer_0_body"); + REQUIRE(sub0 != nullptr); + // Each sub-layer carries the full straw count. + CHECK(sub0->getNChildVols() == static_cast(TrackersFactory::s_nStraws)); +} + +// The inert TrackerMagnet marker is present and fits in the gap before the +// spectrometer-magnet yoke (i.e. it does not overlap station 2 or the yoke). +TEST_CASE("TrackersHasTrackerMagnet", "[trackers]") { + SHiPMaterials materials; + TrackersFactory factory(materials); + GeoPhysVol* tc = factory.build(); + REQUIRE(tc != nullptr); + const GeoVPhysVol* tm = findChild(tc, "/SHiP/trackers/tracker_magnet"); + INFO("tracker_magnet not found"); + REQUIRE(tm != nullptr); + auto* box = dynamic_cast(tm->getLogVol()->getShape()); + REQUIRE(box != nullptr); + // Span must stay clear of station 2 (ends 86570 mm) and the Magnet yoke + // (starts 87070 mm): 86570 <= centre ± halfZ <= 87070. + const double centre = TrackersFactory::s_trackerMagnetZ; + const double halfZ = box->getZHalfLength(); + CHECK(centre - halfZ >= 86570.0); + CHECK(centre + halfZ <= 87070.0); +}