Skip to content

feat(ms)!: Add configurable iron-block muon shield (FairShip TRY_2026)#53

Open
matclim wants to merge 3 commits into
ShipSoft:mainfrom
matclim:MS
Open

feat(ms)!: Add configurable iron-block muon shield (FairShip TRY_2026)#53
matclim wants to merge 3 commits into
ShipSoft:mainfrom
matclim:MS

Conversation

@matclim

@matclim matclim commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Replace the GDML-derived 6-station stub with a configurable iron-block muon
shield. The blocks reproduce the FairShip TRY_2026 magnet layout (PR #1334) as a
solid-block approximation: each magnet is one iron block sized to its FairShip
outer envelope, dropping the aperture, yoke shape and field (field is handled
elsewhere). Magnets may be positioned, rotated, and tapered (GeoBox, or GeoTrd
when tapered). The FairShip absorber (the magnetised hadron stopper) stays in the
Target subsystem, so the shield is the 7 downstream magnets.

Geometry is driven by MS.toml via MuonShieldConfig / readMuonShieldConfig
(toml++), mirroring the calorimeter's calo.toml pattern: unknown-key warnings,
size/taper/envelope validation, and CWD / source-tree / install path resolution.
CMake stages and installs MS.toml and bakes the fallback paths. The container is
placed at the envelope centre reported by the factory rather than a hard-coded
GDML z position.

The muon shield and the Scattering & Neutrino Detector are defined independently.
The SND reservation envelope is declared in NeutrinoDetector/SD.toml (parsed by
SNDEnvelope) and carved from the shield iron at build time via Boolean
subtraction (MuonShieldFactory::reserveSpace -> GeoShapeSubtraction on every
magnet the box intersects); the SND is then embedded as a daughter of the shield
container, sitting in the resulting cavity. Which magnets are carved follows from
the reservation position, not a hard-coded list.

BREAKING CHANGE: MuonShieldFactory's public API and the muon-shield geometry and
placement have changed; the old station/piece structs are removed.

Summary by CodeRabbit

  • New Features

    • Added a configurable passive iron muon shield defined through TOML geometry files.
    • Added support for tapered and rotated shield blocks.
    • Embedded the neutrino detector within the muon-shield container with a reserved cavity.
    • Added configurable neutrino-detector envelope placement and sizing.
  • Bug Fixes

    • Improved geometry placement and overlap handling for the muon shield and neutrino detector.
  • Documentation

    • Updated subsystem documentation to describe the new geometry layout and configuration options.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

MuonShield now reads geometry from MS.toml, builds a passive iron shield envelope from that configuration, and embeds the neutrino detector as a daughter volume inside that container. Build wiring, tests, and documentation were updated to match the new layout.

Changes

Legacy MuonShield subsystem config-driven refactor

Layer / File(s) Summary
Config schema and TOML parser
subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h, subsystems/MuonShield/src/MuonShieldConfig.cpp, subsystems/MuonShield/MS.toml
MuonShieldConfig now defines shield geometry fields, and readMuonShieldConfig() parses and validates the TOML configuration.
Factory API and geometry rewrite
subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h, subsystems/MuonShield/src/MuonShieldFactory.cpp, src/SHiPGeometry.cpp
MuonShieldFactory resolves config paths, builds the envelope and iron blocks from configuration, supports reserved spaces and embedded daughters, and SHiPGeometryBuilder uses the computed center Z for placement.
Build rules, tests, and documentation
subsystems/MuonShield/CMakeLists.txt, subsystems/MuonShield/test_muonshield.cpp, subsystems/MuonShield/README.md, subsystems/NeutrinoDetector/..., tests/test_consistency.cpp
CMake integrates TOML configuration, tests cover geometry and parsing behavior, and documentation and consistency checks reflect the nested detector layout.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SHiPGeometryBuilder
  participant MuonShieldFactory
  participant NeutrinoDetector
  SHiPGeometryBuilder->>MuonShieldFactory: build configured shield
  SHiPGeometryBuilder->>NeutrinoDetector: build detector
  SHiPGeometryBuilder->>MuonShieldFactory: embed daughter detector
  MuonShieldFactory-->>SHiPGeometryBuilder: shield volume and centreZ_mm()
Loading

Possibly related PRs

Suggested reviewers: olantwin

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a breaking, configurable iron-block muon shield based on the FairShip TRY_2026 layout.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (19)
stage/tests/test_consistency.cpp-13-16 (1)

13-16: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add <cmath> for std::abs(double). tests/test_consistency.cpp calls std::abs on a double but doesn’t include <cmath>, so this depends on transitive includes from other headers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/tests/test_consistency.cpp` around lines 13 - 16,
tests/test_consistency.cpp uses std::abs on a double without including the
proper header, so add the missing <cmath> include alongside the existing
includes to make the dependency explicit and avoid relying on transitive
headers. Update the include list near the top of the file where std::abs is used
in the consistency test code.
stage/tests/test_consistency.cpp-48-69 (1)

48-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the unreachable cavern skip. /SHiP/cavern never appears among world’s children here, so this branch is dead; if the cavern should be excluded from ExpectedSubsystemCount, the test needs to match the actual child being iterated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/tests/test_consistency.cpp` around lines 48 - 69, The cavern filter in
collectSubsystems is dead code because the loop over world children never
encounters "/SHiP/cavern"; remove that unreachable skip, or if
ExpectedSubsystemCount is meant to exclude the cavern then update
collectSubsystems to iterate the actual child that represents it and filter that
symbol instead. Keep the logic anchored in collectSubsystems, getChildVol, and
getLogVol so the test matches the real world-child ordering.
subsystems/MuonShield/src/MuonShieldConfig.cpp-36-40 (1)

36-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run clang-format to fix CI lint failure.

Pipeline logs report clang-format reformatting the kKnownKeys array spacing/alignment; current formatting fails the lint job.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp` around lines 36 - 40, The
kKnownKeys array formatting in MuonShieldConfig needs to be normalized to match
clang-format, as the current spacing/alignment is causing the lint failure.
Update the declaration in MuonShieldConfig.cpp by applying the project’s
standard formatting to the static constexpr std::array kKnownKeys initializer so
it matches clang-format output exactly.

Source: Pipeline failures

subsystems/MuonShield/src/MuonShieldConfig.cpp-120-139 (1)

120-139: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run clang-format to fix CI lint failure.

Pipeline logs report clang-format reformatting the block_width_x_mm/block_height_y_mm std::runtime_error throw messages (wrapping/indentation); current formatting fails the lint job.

🔧 Suggested fix
clang-format -i subsystems/MuonShield/src/MuonShieldConfig.cpp
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp` around lines 120 - 139, Run
clang-format on MuonShieldConfig.cpp to match the repository style and fix the
CI lint failure. The affected code is the validation block in
MuonShieldConfigConfig parsing where the std::runtime_error messages for
block_width_x_mm, block_height_y_mm, envelope_half_x_mm, and envelope_half_y_mm
are wrapped differently than expected; reformat that block so the throw
statements and concatenated strings follow clang-format output consistently.

Source: Pipeline failures

subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h-41-49 (1)

41-49: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run clang-format to fix CI lint failure.

Pipeline logs report clang-format reformatting envelopeLengthZ_mm() from a multi-line body to a single line here; the committed formatting doesn't match the project's clang-format style and is failing the prek/lint CI job.

🔧 Suggested fix
clang-format -i subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h` around lines 41
- 49, The MuonShieldConfig helper formatting does not match the project’s
clang-format style, causing the lint CI failure. Update the formatting in
MuonShieldConfig so the `envelopeLengthZ_mm()`, `envelopeCentreZ_mm()`, and
`blockThicknessZ_mm()` accessors match clang-format’s preferred
single-line/multi-line layout, using the existing `MuonShieldConfig` class
definitions as the reference point, then run clang-format on the header to keep
the committed style consistent.

Source: Pipeline failures

stage/README.md-41-43 (1)

41-43: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Documented compiler requirement conflicts with actual CMake config.

README states "C++20 compatible compiler" as a prerequisite, but stage/CMakeLists.txt sets CMAKE_CXX_STANDARD 23 (required by Phlex per its comment). Update the README to match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/README.md` around lines 41 - 43, The README prerequisite list is out of
sync with the build configuration because it says “C++20 compatible compiler”
while the CMake setup in stage/CMakeLists.txt requires C++23 for Phlex. Update
the compiler requirement text in stage/README.md to match the actual
CMAKE_CXX_STANDARD setting and keep the rest of the prerequisite list unchanged.
stage/REUSE.toml-4-4 (1)

4-4: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Download location URL inconsistent with README badge.

This file lists SPDX-PackageDownloadLocation = "https://github.com/SHiP-Collaboration/geometry", while stage/README.md's badge links to https://github.com/ShipSoft/Geometry. Confirm which org/repo path is canonical and align both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/REUSE.toml` at line 4, The SPDX-PackageDownloadLocation in REUSE.toml
is inconsistent with the repository URL used in the stage/README.md badge.
Confirm the canonical org/repo path and update the SPDX-PackageDownloadLocation
value to match it, keeping the README badge and the REUSE.toml metadata aligned.
Use the existing SPDX-PackageDownloadLocation symbol in REUSE.toml and the badge
target in stage/README.md to verify both point to the same canonical location.
stage/subsystems/NeutrinoDetector/README.md-73-79 (1)

73-79: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Documented test name doesn't match the actual test case.

The README lists NeutrinoDetectorIsBoxContainer, but the referenced test file defines the case as TEST_CASE("NeutrinoDetectorWithinEnvelope", ...).

Proposed fix
-- `NeutrinoDetectorIsBoxContainer` - the container is a `GeoBox` large enough to
+- `NeutrinoDetectorWithinEnvelope` - the container is a `GeoBox` large enough to
   hold the contents.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/NeutrinoDetector/README.md` around lines 73 - 79, The README
references the wrong test case name for the container check: it says
NeutrinoDetectorIsBoxContainer, but the actual test in test_neutrinodetector.cpp
is NeutrinoDetectorWithinEnvelope. Update the documentation entry in the
NeutrinoDetector README to use the exact test name from the TEST_CASE
definition, and keep the other listed test names unchanged.
stage/subsystems/NeutrinoDetector/README.md-38-38 (1)

38-38: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Geometry-tree box dimensions don't match the factory constants.

The container is documented as 640 x 640 x 4100 mm, but NeutrinoDetectorFactory.h defines s_halfX = 400.0, s_halfY = 400.0, s_halfZ = 2550.0 (full box 800 x 800 x 5100 mm), and the test in test_neutrinodetector.cpp checks against these same 400/400/2550 half-length bounds. The documented dimensions appear stale relative to the current header.

Proposed fix
-/SHiP/neutrino_detector (Air, 640 x 640 x 4100 mm box)
+/SHiP/neutrino_detector (Air, 800 x 800 x 5100 mm box)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/NeutrinoDetector/README.md` at line 38, The NeutrinoDetector
container dimensions in the README are stale and should match the current
geometry constants used by NeutrinoDetectorFactory and the tests. Update the
documented box size to align with the half-lengths defined in
NeutrinoDetectorFactory.h (and the expectations in test_neutrinodetector.cpp),
or explicitly note that the README was outdated if the factory values are the
source of truth. Keep the geometry description consistent with the symbols
s_halfX, s_halfY, and s_halfZ so the docs reflect the actual detector bounds.
stage/subsystems/Trackers/src/TrackersFactory.cpp-253-280 (1)

253-280: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Straw gas GeoNameTag isn't unique per straw.

Every straw's gas daughter is tagged with the literal "/SHiP/trackers/straw_gas" (Line 274), unlike the log-volume names (straw_wall_<uid>, straw_gas_<uid>) which correctly embed uid for uniqueness. With 9600 straws, all gas children under different wall parents share the identical NameTag string, so any downstream code that resolves volumes via cumulative NameTag/IdentifierTag path (rather than log-volume name) cannot distinguish individual straws' gas volumes.

Proposed fix
-    wallPhys->add(new GeoNameTag("/SHiP/trackers/straw_gas"));
+    wallPhys->add(new GeoNameTag("/SHiP/trackers/straw_gas_" + std::to_string(uid)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Trackers/src/TrackersFactory.cpp` around lines 253 - 280,
The straw gas child in TrackersFactory::buildStraw is using a shared GeoNameTag
string, so each straw’s gas volume is indistinguishable from the others. Update
the GeoNameTag for the gas daughter to include the uid, matching the unique
naming pattern already used for the GeoLogVol objects in buildStraw
(straw_wall_<uid> and straw_gas_<uid>). Keep the rest of the wall/gas
construction and GeoIdentifierTag flow unchanged.
stage/subsystems/Target/README.md-13-13 (1)

13-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language to fenced code block.

Static analysis flags this fence as missing a language identifier.

📝 Proposed fix
-```
+```text
 target_vacuum_box (Vacuum, 160×227.1×300 cm)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Target/README.md` at line 13, The fenced code block in the
README for the vacuum box entry is missing a language identifier, which triggers
static analysis. Update the Markdown fence for the target_vacuum_box snippet to
use a text-language fenced block, keeping the existing content unchanged, so the
block is explicitly labeled in the README.

Source: Linters/SAST tools

stage/subsystems/MuonShield/README.md-19-19 (1)

19-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language to fenced code block.

Static analysis flags this fence as missing a language identifier.

📝 Proposed fix
-```
+```text
 muon_shield (Air envelope, 4200 × 4600 × 27480 mm)   centre z = 18.48 m
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/MuonShield/README.md` at line 19, The fenced block in the
MuonShield README is missing a language tag, which triggers the static analysis
warning. Update the Markdown fence around the muon_shield description to use an
explicit language identifier, using the fenced code block in the README so the
rendered content stays the same while satisfying linting.

Source: Linters/SAST tools

stage/subsystems/MuonShield/src/MuonShieldConfig.cpp-34-40 (1)

34-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

clang-format check failing on kKnownKeys array.

CI reports clang-format would reformat this array's spacing/alignment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/MuonShield/src/MuonShieldConfig.cpp` around lines 34 - 40,
The kKnownKeys initializer in MuonShieldConfig should be reformatted to match
clang-format’s preferred spacing and alignment. Update the std::array
declaration so the string literals are laid out in the formatter’s canonical
style, keeping the kKnownKeys symbol and its sorted key list intact while
removing manual alignment that causes CI diffs.

Source: Pipeline failures

stage/subsystems/MuonShield/src/MuonShieldConfig.cpp-120-140 (1)

120-140: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

clang-format check failing on validation throw messages.

CI reports clang-format would reformat the wrapped std::runtime_error string concatenation/indentation for the block_width_x_mm/block_height_y_mm checks. Run clang-format locally to match CI expectations before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/MuonShield/src/MuonShieldConfig.cpp` around lines 120 - 140,
The validation throws in MuonShieldConfig are formatted in a way that
clang-format will rewrap, so the current `std::runtime_error` string
concatenation/indentation for the block and envelope size checks needs to match
the formatter’s style. Update the exception message construction in
`MuonShieldConfig` so the `block_width_x_mm`, `block_height_y_mm`,
`envelope_half_x_mm`, and `envelope_half_y_mm` checks follow the same
wrapping/indentation pattern clang-format expects, then run clang-format on the
file and keep the resulting formatting.

Source: Pipeline failures

stage/subsystems/Calorimeter/README.md-25-25 (1)

25-25: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Z placement value conflicts with CalorimeterFactory.h.

This README states z = 96970 mm, matching calo.toml's detector_offset_z_mm, but CalorimeterFactory.h's class doc says the container is "centred at Z = 98 320 mm". One of the two is stale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Calorimeter/README.md` at line 25, The Calorimeter placement
documentation is inconsistent between the README and CalorimeterFactory.h.
Verify the correct Z position used by the calorimeter container, then update the
stale docstring in CalorimeterFactory and/or the README so both reference the
same value as detector_offset_z_mm in calo.toml. Make the wording consistent
around the container being centered at Z and ensure the single source of truth
is reflected in the relevant comments.
stage/subsystems/Calorimeter/src/CaloFibreHPLayer.cpp-61-70 (1)

61-70: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Sublayers 0 and 2 collapse into duplicate overlapping fibres when casingZ ≤ 2×rOuter.

dz = std::max(0.0, 0.5 * casingZ - rOuter) becomes 0 whenever hpl_thickness_mm is small relative to the fibre diameter. With dz == 0, sublayers 0 and 2 share identical dx (both 0.0) and zLocal (-0 == 0 == +0), so they place fibres at exactly the same positions — perfectly overlapping duplicate physical volumes rather than the intended 3-sublayer stagger. This doesn't trigger with the current default calo.toml values, but nothing prevents a future config from hitting it.

🔧 Proposed guard
-    const double dz = std::max(0.0, 0.5 * casingZ - rOuter);
+    const double dz = 0.5 * casingZ - rOuter;
+    if (dz <= 0.0)
+        throw std::runtime_error(
+            "CaloFibreHP::buildLayer: casing Z is too thin to stagger 3 fibre sublayers "
+            "(hpl_thickness_mm must exceed roughly 2x fiber_diameter_mm)");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Calorimeter/src/CaloFibreHPLayer.cpp` around lines 61 - 70,
In CaloFibreHPLayer’s fibre tiling setup, the three-sublayer layout collapses
when dz becomes zero, causing sublayers 0 and 2 to generate duplicate
overlapping fibres. Update the sublayer placement logic so the staggered pattern
is only used when casingZ is large enough, or otherwise fall back to a reduced
layout/adjusted offsets that guarantee each sublayer has distinct x/z positions.
Use the existing symbols pitch, dx, dz, zLocal, and the tiling block in
CaloFibreHPLayer to apply the guard cleanly.
stage/subsystems/Calorimeter/src/CalorimeterConfig.cpp-99-123 (1)

99-123: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Legacy comma-string parsing can throw an undocumented exception type.

std::stoi(token.substr(...)) in the legacy string branch isn't wrapped in a try/catch. A malformed token (e.g. non-numeric text, or a token that's just ;/whitespace-adjacent junk left after trimming) will let std::invalid_argument/std::out_of_range escape uncaught, whereas the documented contract for readCaloConfig only promises std::runtime_error for malformed input. Callers that catch std::runtime_error specifically (per the API contract) won't catch this.

🔧 Proposed fix
             if (first == std::string::npos)
                 continue;
-            out.push_back(std::stoi(token.substr(first, last - first + 1)));
+            try {
+                out.push_back(std::stoi(token.substr(first, last - first + 1)));
+            } catch (const std::exception&) {
+                throw std::runtime_error("CalorimeterConfig: '" + key +
+                                         "' contains a non-integer token: '" + token + "'");
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Calorimeter/src/CalorimeterConfig.cpp` around lines 99 -
123, The legacy comma-string parsing in readIntList can leak
std::invalid_argument or std::out_of_range from std::stoi, which breaks the
expected error contract for readCaloConfig. Update the string branch in
CalorimeterConfig::readIntList to catch parsing failures around the token
conversion and rethrow them as std::runtime_error with a clear
CalorimeterConfig/key-specific message, so malformed legacy list values are
handled consistently with the array branch.
stage/subsystems/TimingDetector/data/timing_detector.gmx-26-27 (1)

26-27: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Outer bar row touches the container boundary with zero margin.

The outermost bar rows are at Y=±3220.000mm; with the 30mm bar half-width (Line 42 in the header/shape def) this puts the bar face at exactly ±3250mm — precisely equal to s_containerHalfY (3250.0, see TimingDetectorFactory.h Line 39). This is a coincident-face condition. The sibling UpstreamTaggerFactory.cpp explicitly avoids this exact scenario via s_env_z_margin ("coincident faces trigger GeomNav stuck-track warnings"), but no equivalent margin exists here. Since the container size is fixed by the envelope contract (test comment: halfY ≤ 3250), the fix should shrink the Y grid slightly (e.g., a few mm) rather than grow the container.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/TimingDetector/data/timing_detector.gmx` around lines 26 -
27, The outer bar rows in the timing detector grid are exactly touching the
container boundary, creating a coincident-face condition. Update the Y positions
in timDety_even and timDety_odd in timing_detector.gmx so the outermost bar
faces sit slightly inside s_containerHalfY, similar to the margin approach used
in UpstreamTaggerFactory.cpp, rather than changing the container envelope.
Ensure the adjusted grid still preserves symmetry around zero while avoiding
exact contact with the 3250mm boundary.
stage/subsystems/TimingDetector/src/TimingDetectorFactory.cpp-31-51 (1)

31-51: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add an explicit failure when the GMX file cannot be found. resolveGmxPath() still returns timing_detector.gmx if neither fallback exists, so build() hands a missing file to Gmx2Geo and defers the error to a lower-level parser. Throw a descriptive exception before constructing Gmx2Geo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/TimingDetector/src/TimingDetectorFactory.cpp` around lines
31 - 51, The GMX path resolution in resolveGmxPath still falls back to the
default filename even when neither candidate exists, so
TimingDetectorFactory::build ends up passing a missing file into Gmx2Geo. Update
resolveGmxPath to explicitly detect the “not found” case and throw a descriptive
exception before constructing Gmx2Geo, keeping the failure at
TimingDetectorFactory/resolveGmxPath rather than deferring it to the parser.
🧹 Nitpick comments (19)
subsystems/MuonShield/README.md (1)

19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language to the fenced code block.

markdownlint flags this block (MD040) for missing a language specifier.

📝 Proposed fix
-```
+```text
 muon_shield (Air envelope, 4200 × 4600 × 27480 mm)   centre z = 18.48 m
  ├─ block_0 (Iron, 3000 × 2000 × 2748 mm)
  ├─ block_1 (Iron, 3000 × 2000 × 2748 mm)
  ├─ …
  └─ block_9 (Iron, 3000 × 2000 × 2748 mm)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @subsystems/MuonShield/README.md around lines 19 - 25, Add a language
specifier to the fenced block in the MuonShield README so markdownlint MD040 is
satisfied; update the code fence around the muon_shield diagram to use a
descriptive language tag such as text, keeping the block contents unchanged.


</details>

<!-- cr-comment:v1:718f8e202038c1c176d9ea6a -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>subsystems/MuonShield/CMakeLists.txt (1)</summary><blockquote>

`45-58`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Baked `MS_TOML_INSTALL_PATH` won't survive staged/relocated installs.**

`${CMAKE_INSTALL_PREFIX}` is embedded as an absolute compile-time string. This breaks for `DESTDIR`-staged packaging (rpm/deb) or if the install tree is later relocated — the binary will still look for the config at the original build-time prefix. This mirrors the same pattern presumably used for `calo.toml`/`sbt.toml`, so it's a pre-existing project convention rather than something unique to this file.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @subsystems/MuonShield/CMakeLists.txt around lines 45 - 58, The
MS_TOML_INSTALL_PATH definition in MuonShield is baking in CMAKE_INSTALL_PREFIX
as an absolute path, which will fail for staged or relocated installs. Update
the install-time lookup logic so it uses an install-root-relative path or
another relocatable mechanism consistent with the existing calo.toml/sbt.toml
pattern, and keep the fallback aligned with the MuonShield target’s config
loading behavior.


</details>

<!-- cr-comment:v1:cdef84e3ca18e656014ee3ef -->

</blockquote></details>
<details>
<summary>subsystems/MuonShield/src/MuonShieldFactory.cpp (1)</summary><blockquote>

`33-47`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚡ Quick win_

**Use `std::filesystem::path::is_absolute()` here.** `path[0] == '/'` is POSIX-only, so absolute Windows paths will miss the fast path and fall through to fallback lookup. The same resolver also appears in `CalorimeterFactory.cpp` and `DecayVolumeFactory.cpp`, so a shared helper would keep the behavior aligned.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @subsystems/MuonShield/src/MuonShieldFactory.cpp around lines 33 - 47, The
resolveTomlPath helper is using a POSIX-only slash check to detect absolute
paths, which breaks Windows path handling and should be replaced with
std::filesystem::path::is_absolute(). Update the absolute-path branch in
resolveTomlPath in MuonShieldFactory.cpp, and apply the same change to the
matching resolver logic in CalorimeterFactory.cpp and DecayVolumeFactory.cpp so
all factories behave consistently; if possible, factor the shared
path-resolution logic into one common helper.


</details>

<!-- cr-comment:v1:2733782706d5e83281bc6e8c -->

</blockquote></details>
<details>
<summary>subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h (1)</summary><blockquote>

`40-45`: _🩺 Stability & Availability_ | _🔵 Trivial_ | _💤 Low value_

**Consider guarding against calling `centreZ_mm()` before `build()`.**

The docs say the value is "valid only after build()", but nothing enforces it — a future caller invoking `centreZ_mm()` before `build()` would silently get `0.0` instead of an error. Not currently an issue since the one call site (`stage/src/SHiPGeometry.cpp`) calls `build()` first, but a `built_` flag with an assert/throw would make misuse fail loudly.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h around lines 40

  • 45, Guard MuonShieldFactory::centreZ_mm() against being called before
    MuonShieldFactory::build() has completed, since it currently returns
    m_centreZ_mm silently even when the value is not yet valid. Add a built-state
    check in MuonShieldFactory (for example a built_ flag set by build()) and make
    centreZ_mm() fail loudly via an assert or exception when accessed too early,
    while keeping the existing valid-after-build behavior for normal callers.

</details>

<!-- cr-comment:v1:e94fc92e440b8e481aeedd10 -->

</blockquote></details>
<details>
<summary>stage/README.md (1)</summary><blockquote>

`133-150`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Add a language hint to the fenced code block.**

Static analysis flags this ASCII-tree block as missing a language identifier (MD040).

<details>
<summary>📝 Proposed fix</summary>

```diff
-```
+```text
 geometry/
 ├── include/SHiPGeometry/   # Public headers (SHiPGeometry, SHiPMaterials)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/README.md` around lines 133 - 150, The ASCII-tree block in the README
is missing a fenced code language tag, triggering MD040. Update the fenced block
in the geometry section to use a text-oriented language hint so the markdown
parser recognizes it, and keep the existing tree content unchanged.

Source: Linters/SAST tools

stage/CMakeLists.txt (1)

84-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate install rule for build_geometry/validate_geometry.

These two targets are already installed by stage/apps/CMakeLists.txt (lines 17-20). Having the same install(TARGETS ...) rule in two places is redundant and a maintenance hazard if the two diverge (e.g., differing EXPORT sets or destinations).

♻️ Proposed fix — remove one of the duplicate rules
-install(
-    TARGETS build_geometry validate_geometry
-    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
-)
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/CMakeLists.txt` around lines 84 - 87, The install rule for
build_geometry and validate_geometry is duplicated, so remove the redundant
TARGETS install from stage/CMakeLists and keep the single source of truth in
stage/apps/CMakeLists. Verify the remaining install(TARGETS ...) block still
covers both targets with the intended RUNTIME DESTINATION and any other
install/export settings used by those symbols.
stage/src/SHiPMaterials.cpp (1)

85-268: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider .at() instead of operator[] for element lookups.

All current element keys resolve correctly, but m_elements["Name"] silently inserts a null entry on a typo instead of failing fast. .at() would throw immediately, catching future maintenance errors at construction rather than passing a null GeoElement* into GeoMaterial::add().

♻️ Example diff (Air material)
-    air->add(m_elements["Nitrogen"], 0.755);
-    air->add(m_elements["Oxygen"], 0.231);
-    air->add(m_elements["Argon"], 0.014);
+    air->add(m_elements.at("Nitrogen"), 0.755);
+    air->add(m_elements.at("Oxygen"), 0.231);
+    air->add(m_elements.at("Argon"), 0.014);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/src/SHiPMaterials.cpp` around lines 85 - 268, The element lookups in
SHiPMaterials::createMaterials() use m_elements["Name"], which can silently
insert null entries if a key is misspelled. Replace these operator[] accesses
with m_elements.at(...) throughout createMaterials() so missing elements fail
fast before being passed into GeoMaterial::add(), using the existing
material-building blocks like the Air, Concrete, and ArCO2_70_30 sections as
reference points.
stage/subsystems/DecayVolume/include/DecayVolume/SBTConfig.h (1)

35-54: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Unenforced invariant: container_thickness_mm should equal webHeight().

Docs state container_thickness_mm must equal the H-beam clear web height (hbeam_height_mm - 2*hbeam_flange_thickness_mm), and the default (225 mm) satisfies it, but nothing in readSBTConfig enforces this relationship. Editing either value independently would silently produce sensor containers that overlap or gap from the H-beam structure, with no build/test failure to catch it.

Consider either deriving the container thickness from webHeight() (dropping the duplicate field) or adding a tolerance check in readSBTConfig, e.g.:

if (std::abs(cfg.container_thickness_mm - cfg.webHeight()) > 1e-6)
    throw std::runtime_error(
        "SBTConfig: 'container_thickness_mm' must equal hbeam_height_mm - "
        "2*hbeam_flange_thickness_mm (the H-beam clear web height)");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/DecayVolume/include/DecayVolume/SBTConfig.h` around lines 35
- 54, The SBTConfig invariant between container_thickness_mm and webHeight() is
only documented, not enforced. Update readSBTConfig to validate that
cfg.container_thickness_mm matches cfg.webHeight() within a small tolerance, or
derive container_thickness_mm directly from webHeight() in SBTConfig to avoid
duplicate sources of truth. Use the existing SBTConfig::webHeight() helper and
the container_thickness_mm field to locate the check.
stage/subsystems/Magnet/README.md (1)

11-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language tag to the fenced geometry-tree block.

Flagged by markdownlint (MD040): the fenced block is missing a language identifier.

Proposed fix
-```
+```text
 SHiPMagnet (Air, 6500×8600×5000 mm)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Magnet/README.md` around lines 11 - 22, The fenced
geometry-tree block in the Magnet README is missing a language identifier,
triggering markdownlint MD040. Update the opening fence for the geometry-tree
snippet to include an appropriate tag such as text, and keep the content
otherwise unchanged. Use the fenced block near the SHiPMagnet diagram to make
the fix easy to locate.

Source: Linters/SAST tools

stage/subsystems/DecayVolume/src/SBTStructureBuilder.cpp (1)

112-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated rotation-frame construction between placeHBeamInclined and the inline lambda.

The orthonormal-frame computation (n/b axis derivation, degenerate-axis fallback, RotationMatrix3D assembly) at Lines 138-176 is re-implemented nearly verbatim inside the placeLongBeam lambda at Lines 313-332. Extracting a shared helper (e.g., computeBeamFrame(x0,y0,z0,x1,y1,z1) -> {rotMat, halfLen}) would remove ~35 lines of duplicated vector math and reduce the risk of the two copies drifting apart.

Also applies to: 306-343

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/DecayVolume/src/SBTStructureBuilder.cpp` around lines 112 -
194, The orthonormal beam-frame construction is duplicated between
placeHBeamInclined and the placeLongBeam lambda, including the degenerate-axis
fallback and RotationMatrix3D setup. Extract that shared vector math into a
helper such as computeBeamFrame (returning the rotation matrix and half-length),
then have both placeHBeamInclined and placeLongBeam call it so the frame logic
stays consistent and easier to maintain.
stage/subsystems/Trackers/README.md (1)

21-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language hint to the fenced geometry-tree block.

Static analysis flags this fence for missing a language identifier (MD040).

Proposed fix
-```
+```text
 /SHiP/trackers (Air, 3000 × 3430 × 6000 mm half-extents)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Trackers/README.md` around lines 21 - 33, The geometry-tree
fenced block in the Trackers README is missing a language identifier, triggering
the MD040 markdown lint warning. Update the fence that wraps the /SHiP/trackers
tree to include a text language hint, keeping the content unchanged; this is the
only fenced block in this snippet and should be easy to locate by the
/SHiP/trackers heading and nested station_<n>/view_<v>/envelope entries.

Source: Linters/SAST tools

stage/subsystems/Cavern/test_cavern.cpp (1)

15-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test only covers the outer world box; cavern shape/cavities are unverified.

The subtraction chain producing m_cavern (5 boolean operations) has no assertions — a regression there (e.g., wrong material, or a cavity subtraction silently failing) wouldn't be caught.

♻️ Suggested additional coverage
     CHECK(box->getXHalfLength() == 200000.0);
     CHECK(box->getYHalfLength() == 200000.0);
     CHECK(box->getZHalfLength() == 200000.0);
+
+    // Verify the cavern (rock) volume was added and uses Concrete
+    REQUIRE(world->getNChildVols() > 0);
+    GeoPhysVol* cavern = dynamic_cast<GeoPhysVol*>(world->getChildVol(world->getNChildVols() - 1));
+    REQUIRE(cavern != nullptr);
+    CHECK(cavern->getLogVol()->getMaterial()->getName() == "Concrete");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Cavern/test_cavern.cpp` around lines 15 - 25, The
CavernFactory test only verifies the outer world box and misses the boolean
subtraction chain that builds m_cavern, so add assertions that exercise the
actual cavern/cavity geometry. Extend test_cavern.cpp in
TEST_CASE("CavernWorldVolume", ...) to inspect the returned world volume and
validate the nested GeoShape/boolean structure produced by
SHiPGeometry::CavernFactory::build(), including the expected cavity subtractions
and resulting material/shape configuration so a broken subtraction step is
caught.
stage/subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h (1)

20-50: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider guarding blockThicknessZ_mm() against n_sections == 0.

envelopeLengthZ_mm() / n_sections divides by n_sections unconditionally. This is currently safe only because readMuonShieldConfig validates n_sections > 0 before any caller can use this struct meaningfully — but a MuonShieldConfig can also be default- or hand-constructed without going through the parser, in which case nothing prevents n_sections from being set to 0 before calling this helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h` around
lines 20 - 50, Guard MuonShieldConfig::blockThicknessZ_mm() against n_sections
being zero, since it currently divides unconditionally by n_sections and can be
called on a default- or manually-constructed MuonShieldConfig outside
readMuonShieldConfig validation. Update the helper to handle the invalid zero
case safely, and keep the fix localized to the MuonShieldConfig struct so
callers of envelopeLengthZ_mm() and blockThicknessZ_mm() do not need to change.
stage/subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h (1)

38-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stateful ordering between build() and centreZ_mm() isn't enforced.

centreZ_mm() silently returns the default 0.0 (line 55) if called before build(), rather than failing loudly. Since correct world placement depends on this value, consider bundling the physical volume and the centre offset into a single return type from build() (or asserting/throwing in centreZ_mm() if not yet populated) to remove the temporal coupling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h` around
lines 38 - 49, The MuonShieldFactory API currently allows centreZ_mm() to return
a default 0.0 before build(), which hides incorrect call ordering. Update
MuonShieldFactory so the build()/centreZ_mm() temporal coupling is enforced by
either returning the center offset together with the GeoPhysVol from build() via
a single result type, or by making centreZ_mm() assert/throw until the value has
been populated. Use the existing MuonShieldFactory, build(), and centreZ_mm()
symbols to keep the contract explicit and prevent silent fallback values.
stage/subsystems/MuonShield/src/MuonShieldFactory.cpp (1)

54-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate path-resolution call.

build() (Line 61) calls resolveTomlPath(m_configPath) directly instead of reusing resolvedConfigPath() (Line 54-56), duplicating the resolution logic path.

♻️ Proposed fix
 GeoPhysVol* MuonShieldFactory::build() {
-    const MuonShieldConfig cfg = readMuonShieldConfig(resolveTomlPath(m_configPath));
+    const MuonShieldConfig cfg = readMuonShieldConfig(resolvedConfigPath());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/MuonShield/src/MuonShieldFactory.cpp` around lines 54 - 61,
`MuonShieldFactory::build()` is re-resolving `m_configPath` instead of reusing
the existing `resolvedConfigPath()` helper, which duplicates the same path
logic. Update `build()` to call `resolvedConfigPath()` when passing the config
path into `readMuonShieldConfig`, so the path resolution is centralized in
`MuonShieldFactory::resolvedConfigPath()` and not repeated.
stage/subsystems/Target/test_target.cpp (1)

15-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test only covers the outer vacuum box; sub-component placements are untested.

This test verifies the top-level vacuum_box dimensions but doesn't exercise any of the manually re-derived offsets for the proximity/top/bottom shielding, vessel, caps, or the 19 target slabs in TargetFactory.cpp (Lines 34-108) — the parts most likely to contain an error during a GDML→code translation. Consider adding assertions on a few child transform positions (e.g. vessel, front/back caps) to catch regressions in the offset math.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Target/test_target.cpp` around lines 15 - 26, The
TargetBuilds test only checks the outer vacuum_box dimensions and leaves the
manually computed placement offsets in TargetFactory unverified. Update the test
around SHiPMaterials and SHiPMGeometry::TargetFactory::build() to also inspect
selected child placements from the returned GeoPhysVol, especially the vessel
and front/back cap transforms, so regressions in the offset math for the
shielding, vessel, caps, and target slabs are caught even if the top-level box
still passes.
stage/subsystems/Calorimeter/README.md (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mislabeled dimensions: these are full extents, not half-extents.

CalorimeterFactory.h defines s_containerHalfX/Y/Z = 3000/3500/1450 mm — the values documented here (6000×7000×2900) are the corresponding full box sizes, not half-extents.

✏️ Suggested fix
-/SHiP/calorimeter (Air, 6000 × 7000 × 2900 mm half-extents fixed by SHiPGeometry)
+/SHiP/calorimeter (Air, 6000 × 7000 × 2900 mm full extents / 3000 × 3500 × 1450 mm half-extents, fixed by SHiPGeometry)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/Calorimeter/README.md` at line 18, The Calorimeter README
currently labels the container dimensions as half-extents, but the documented
6000 × 7000 × 2900 mm values are actually the full sizes derived from
CalorimeterFactory.h constants like s_containerHalfX, s_containerHalfY, and
s_containerHalfZ. Update the description in the calorimeter section to say full
extents or otherwise clarify that these are the full box dimensions, so it
matches the definitions used by CalorimeterFactory.
stage/subsystems/TimingDetector/README.md (1)

11-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add language identifier to fenced code block.

markdownlint flags the geometry-tree block for missing a language hint.

📝 Proposed fix
-```
+```text
 Timing_Detector (Air, 5500×6500×500 mm)
  └─ 330 × scintillator bars (1400×60×10 mm each)
      built from GeoModelXML prototype
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @stage/subsystems/TimingDetector/README.md around lines 11 - 15, The fenced
geometry-tree block in the TimingDetector README is missing a language
identifier, which triggers markdownlint. Update the fenced block to use an
appropriate language hint such as text while keeping the existing content and
formatting intact.


</details>

<!-- cr-comment:v1:2031c3e9f8f905a3d9feeb4e -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>stage/subsystems/TimingDetector/CMakeLists.txt (1)</summary><blockquote>

`31-41`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Fragile assumption on `GeoModelTools_DIR` layout.**

`_gt_prefix` is derived by walking three levels up from `GeoModelTools_DIR`, assuming the standard `<prefix>/lib/cmake/GeoModelTools` layout. This holds for typical CMake config-package installs, but breaks silently (degrading to just the warning at Lines 37-40) for `lib64`, multi-level, or non-standard install layouts, since there is no fallback search via `find_file`.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @stage/subsystems/TimingDetector/CMakeLists.txt around lines 31 - 41, The
geomodel.dtd lookup in the TimingDetector CMake logic is too dependent on
walking up from GeoModelTools_DIR, so replace the hardcoded prefix derivation in
the geomodel.dtd copy block with a proper search using find_file or an
equivalent fallback search path. Keep the existing configure_file copy step in
place, but make it consume the discovered DTD path from the search result
instead of assuming the /share/GeoModelXml layout, and preserve the
warning path only when the search truly fails.


</details>

<!-- cr-comment:v1:1086fc4fd1da579c13889b46 -->

</blockquote></details>

</blockquote></details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `95d2a274-90e0-42d0-8d32-93be225a4c61`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 203b9d2f0ed89ccec49c68f37601c39285982de7 and 1fff4ce63422edf9e7be42f8802abe777effef22.

</details>

<details>
<summary>⛔ Files ignored due to path filters (4)</summary>

* `geometry.tar.gz` is excluded by `!**/*.gz`
* `geometry_muonshield.tar.gz` is excluded by `!**/*.gz`
* `stage/pixi.lock` is excluded by `!**/*.lock`
* `stage/subsystem_envelopes.csv` is excluded by `!**/*.csv`

</details>

<details>
<summary>📒 Files selected for processing (101)</summary>

* `src/SHiPGeometry.cpp`
* `stage/CMakeLists.txt`
* `stage/LICENSES/LGPL-3.0-or-later.txt`
* `stage/README.md`
* `stage/REUSE.toml`
* `stage/apps/CMakeLists.txt`
* `stage/cliff.toml`
* `stage/cmake/SHiPGeometryConfig.cmake.in`
* `stage/committed.toml`
* `stage/doxygen/Doxyfile`
* `stage/include/SHiPGeometry/SHiPGeometry.h`
* `stage/include/SHiPGeometry/SHiPMaterials.h`
* `stage/pixi.toml`
* `stage/renovate.json`
* `stage/scripts/release.sh`
* `stage/src/CMakeLists.txt`
* `stage/src/SHiPGeometry.cpp`
* `stage/src/SHiPMaterials.cpp`
* `stage/subsystems/CMakeLists.txt`
* `stage/subsystems/Calorimeter/CMakeLists.txt`
* `stage/subsystems/Calorimeter/README.md`
* `stage/subsystems/Calorimeter/calo.toml`
* `stage/subsystems/Calorimeter/include/Calorimeter/CaloBarLayer.h`
* `stage/subsystems/Calorimeter/include/Calorimeter/CaloFibreHPLayer.h`
* `stage/subsystems/Calorimeter/include/Calorimeter/CalorimeterConfig.h`
* `stage/subsystems/Calorimeter/include/Calorimeter/CalorimeterFactory.h`
* `stage/subsystems/Calorimeter/src/CaloBarLayer.cpp`
* `stage/subsystems/Calorimeter/src/CaloFibreHPLayer.cpp`
* `stage/subsystems/Calorimeter/src/CalorimeterConfig.cpp`
* `stage/subsystems/Calorimeter/src/CalorimeterFactory.cpp`
* `stage/subsystems/Calorimeter/test_calorimeter.cpp`
* `stage/subsystems/Cavern/CMakeLists.txt`
* `stage/subsystems/Cavern/README.md`
* `stage/subsystems/Cavern/include/Cavern/CavernFactory.h`
* `stage/subsystems/Cavern/src/CavernFactory.cpp`
* `stage/subsystems/Cavern/test_cavern.cpp`
* `stage/subsystems/DecayVolume/CMakeLists.txt`
* `stage/subsystems/DecayVolume/README.md`
* `stage/subsystems/DecayVolume/include/DecayVolume/DecayVolumeFactory.h`
* `stage/subsystems/DecayVolume/include/DecayVolume/SBTConfig.h`
* `stage/subsystems/DecayVolume/include/DecayVolume/SBTSensorBuilder.h`
* `stage/subsystems/DecayVolume/include/DecayVolume/SBTStructureBuilder.h`
* `stage/subsystems/DecayVolume/sbt.toml`
* `stage/subsystems/DecayVolume/src/DecayVolumeFactory.cpp`
* `stage/subsystems/DecayVolume/src/SBTConfig.cpp`
* `stage/subsystems/DecayVolume/src/SBTSensorBuilder.cpp`
* `stage/subsystems/DecayVolume/src/SBTStructureBuilder.cpp`
* `stage/subsystems/DecayVolume/test_decayvolume.cpp`
* `stage/subsystems/Magnet/CMakeLists.txt`
* `stage/subsystems/Magnet/README.md`
* `stage/subsystems/Magnet/include/Magnet/MagnetFactory.h`
* `stage/subsystems/Magnet/src/MagnetFactory.cpp`
* `stage/subsystems/Magnet/test_magnet.cpp`
* `stage/subsystems/MuonShield/CMakeLists.txt`
* `stage/subsystems/MuonShield/MS.toml`
* `stage/subsystems/MuonShield/README.md`
* `stage/subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h`
* `stage/subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h`
* `stage/subsystems/MuonShield/src/MuonShieldConfig.cpp`
* `stage/subsystems/MuonShield/src/MuonShieldFactory.cpp`
* `stage/subsystems/MuonShield/test_muonshield.cpp`
* `stage/subsystems/NeutrinoDetector/CMakeLists.txt`
* `stage/subsystems/NeutrinoDetector/README.md`
* `stage/subsystems/NeutrinoDetector/include/NeutrinoDetector/NeutrinoDetectorFactory.h`
* `stage/subsystems/NeutrinoDetector/src/NeutrinoDetectorFactory.cpp`
* `stage/subsystems/NeutrinoDetector/test_neutrinodetector.cpp`
* `stage/subsystems/Target/CMakeLists.txt`
* `stage/subsystems/Target/README.md`
* `stage/subsystems/Target/include/Target/TargetFactory.h`
* `stage/subsystems/Target/src/TargetFactory.cpp`
* `stage/subsystems/Target/test_target.cpp`
* `stage/subsystems/TimingDetector/CMakeLists.txt`
* `stage/subsystems/TimingDetector/README.md`
* `stage/subsystems/TimingDetector/data/timing_detector.gmx`
* `stage/subsystems/TimingDetector/include/TimingDetector/SHiPTimingDetInterface.h`
* `stage/subsystems/TimingDetector/include/TimingDetector/TimingDetectorFactory.h`
* `stage/subsystems/TimingDetector/src/TimingDetectorFactory.cpp`
* `stage/subsystems/TimingDetector/test_timingdetector.cpp`
* `stage/subsystems/Trackers/CMakeLists.txt`
* `stage/subsystems/Trackers/README.md`
* `stage/subsystems/Trackers/include/Trackers/TrackersFactory.h`
* `stage/subsystems/Trackers/src/TrackersFactory.cpp`
* `stage/subsystems/Trackers/test_trackers.cpp`
* `stage/subsystems/UpstreamTagger/CMakeLists.txt`
* `stage/subsystems/UpstreamTagger/README.md`
* `stage/subsystems/UpstreamTagger/include/UpstreamTagger/SHiPUBTManager.h`
* `stage/subsystems/UpstreamTagger/include/UpstreamTagger/UpstreamTaggerFactory.h`
* `stage/subsystems/UpstreamTagger/src/UpstreamTaggerFactory.cpp`
* `stage/subsystems/UpstreamTagger/test_upstreamtagger.cpp`
* `stage/tests/CMakeLists.txt`
* `stage/tests/test_builder.cpp`
* `stage/tests/test_consistency.cpp`
* `stage/tests/test_materials.cpp`
* `subsystems/MuonShield/CMakeLists.txt`
* `subsystems/MuonShield/MS.toml`
* `subsystems/MuonShield/README.md`
* `subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h`
* `subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h`
* `subsystems/MuonShield/src/MuonShieldConfig.cpp`
* `subsystems/MuonShield/src/MuonShieldFactory.cpp`
* `subsystems/MuonShield/test_muonshield.cpp`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread stage/CMakeLists.txt Outdated
Comment on lines +4 to +16
cmake_minimum_required(VERSION 3.16)

project(
SHiPGeometry
VERSION 0.2.1
DESCRIPTION "SHiP detector geometry implementation using GeoModel"
LANGUAGES CXX
)

# C++23: required by Phlex
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does CMake 3.16 support CMAKE_CXX_STANDARD 23?

💡 Result:

No, CMake 3.16 does not support CMAKE_CXX_STANDARD 23 [1]. Support for C++23 in the CMAKE_CXX_STANDARD variable and the CXX_STANDARD target property was introduced in CMake 3.20 [2][3]. In CMake 3.16, the supported values for CXX_STANDARD are limited to 98, 11, 14, 17, and 20 [1].

Citations:


Raise the CMake minimum to 3.20 CMAKE_CXX_STANDARD 23 is only supported starting in CMake 3.20, so 3.16–3.19 can leave the project configured without C++23.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/CMakeLists.txt` around lines 4 - 16, The stage/CMakeLists.txt
configuration currently sets the minimum CMake version too low for the
CMAKE_CXX_STANDARD 23 setting used in the top-level project block. Update the
cmake_minimum_required declaration in stage/CMakeLists.txt to require CMake 3.20
or newer so the CXX standard setting is reliably honored during configuration.

Comment thread stage/subsystems/Calorimeter/src/CalorimeterFactory.cpp Outdated
Comment on lines +78 to +111
// ── Air container ────────────────────────────────────────────────────
// The container is the experiment's fixed envelope allocation for the
// decay region (s_halfX/Y/Z). The SBT geometry comes from sbt.toml, so a
// future edit there could in principle outgrow it. Guard against that:
// bound the outermost structure reach from the config and fail loudly if
// it would pierce the envelope, rather than silently overlapping.
//
// Reference description: rectangular frustum, 2.0 x 3.0 m entrance ->
// 4.0 x 6.0 m exit, 50 m long. With the default config the reaches are
// ~2130 / ~3259 / ~25130 mm, inside the 2200 / 3300 / 25200 envelope.
{
const double growthY = (cfg.y_half_exit_mm - cfg.y_half_entrance_mm) / cfg.total_length_mm;
// X: vertical columns sit at x_half_exit with a half-flange overhang.
const double reachX = cfg.x_half_exit_mm + 0.5 * cfg.hbeam_flange_width_mm;
// Y: top/bottom cross-beams are shifted a full beam-height above
// y_half_exit (plus a small frustum-growth term); +10 pads the
// assembly standoff and is a safe upper bound on the real ~3259 mm.
const double reachY = cfg.y_half_exit_mm + cfg.hbeam_height_mm +
0.5 * cfg.hbeam_flange_width_mm * growthY + 10.0;
// Z: the 50 m structure spans [z_entrance, z_entrance + total_length];
// cross-beam flanges extend half a flange-width beyond the end rows.
const double reachZ = std::max(std::abs(cfg.z_entrance_mm),
std::abs(cfg.z_entrance_mm + cfg.total_length_mm)) +
0.5 * cfg.hbeam_flange_width_mm;

if (reachX > s_halfX || reachY > s_halfY || reachZ > s_halfZ) {
throw std::runtime_error(
"DecayVolumeFactory: configured SBT (half-reach " + std::to_string(reachX) + " x " +
std::to_string(reachY) + " x " + std::to_string(reachZ) +
" mm) does not fit the decay-volume envelope (" + std::to_string(s_halfX) + " x " +
std::to_string(s_halfY) + " x " + std::to_string(s_halfZ) +
" mm); adjust sbt.toml or the container size.");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Envelope guard doesn't validate entrance/exit half-widths against the helium inset.

The guard at Line 103 checks that the structure's outer reach fits the fixed envelope, but nothing validates that cfg.x_half_entrance_mm/y_half_entrance_mm/x_half_exit_mm/y_half_exit_mm exceed inset = container_thickness_mm + helium_clearance_mm (Line 120). SBTConfig::readSBTConfig only checks each of these fields individually for > 0, not their relationship to the inset. If a future sbt.toml edit sets a small entrance/exit half-width, dx1/dy1/dx2/dy2 (Lines 122-125) go negative, producing a degenerate/invalid GeoTrap.

🛡️ Proposed fix: add an inset-consistency check
     const double inset = cfg.container_thickness_mm + cfg.helium_clearance_mm;
+    if (cfg.x_half_entrance_mm <= inset || cfg.y_half_entrance_mm <= inset ||
+        cfg.x_half_exit_mm <= inset || cfg.y_half_exit_mm <= inset) {
+        throw std::runtime_error(
+            "DecayVolumeFactory: entrance/exit half-widths must exceed container_thickness_mm + "
+            "helium_clearance_mm");
+    }
     const double dz = 0.5 * cfg.total_length_mm * mm;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/DecayVolume/src/DecayVolumeFactory.cpp` around lines 78 -
111, The envelope check in DecayVolumeFactory only verifies the outer reach
against s_halfX/s_halfY/s_halfZ, but it still allows invalid trap dimensions if
the entrance/exit half-widths are smaller than the helium inset. Add a
consistency guard near the existing reach check in DecayVolumeFactory::build
(before computing dx1/dy1/dx2/dy2) that compares cfg.x_half_entrance_mm,
cfg.y_half_entrance_mm, cfg.x_half_exit_mm, and cfg.y_half_exit_mm against inset
= container_thickness_mm + helium_clearance_mm, and throw if any are too small.
Keep the error message specific so future sbt.toml edits that would create a
degenerate GeoTrap are caught early.

Comment thread stage/subsystems/Magnet/src/MagnetFactory.cpp Outdated
Comment thread stage/subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h Outdated
Comment thread stage/subsystems/NeutrinoDetector/src/NeutrinoDetectorFactory.cpp Outdated
Comment on lines +6 to +18
target_include_directories(
TimingDetector
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)

target_link_libraries(
TimingDetector
PUBLIC GeoModelCore::GeoModelKernel
PRIVATE GeoModelTools::GeoModelXml
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Missing SHiPGeometry link on the TimingDetector target.

target_include_directories (Line 10) pulls in ${CMAKE_SOURCE_DIR}/include specifically so TimingDetectorFactory.cpp can #include "SHiPGeometry/SHiPMaterials.h", but target_link_libraries (Lines 14-18) never links the SHiPGeometry library that implements SHiPMaterials. The test target below (Line 73) has to add SHiPGeometry explicitly to link successfully, confirming the main library is missing this dependency. Any other consumer that links only TimingDetector (without independently adding SHiPGeometry) risks unresolved-symbol link errors.

🔧 Proposed fix
 target_link_libraries(
     TimingDetector
-    PUBLIC GeoModelCore::GeoModelKernel
+    PUBLIC GeoModelCore::GeoModelKernel SHiPGeometry
     PRIVATE GeoModelTools::GeoModelXml
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
target_include_directories(
TimingDetector
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(
TimingDetector
PUBLIC GeoModelCore::GeoModelKernel
PRIVATE GeoModelTools::GeoModelXml
)
target_include_directories(
TimingDetector
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(
TimingDetector
PUBLIC GeoModelCore::GeoModelKernel SHiPGeometry
PRIVATE GeoModelTools::GeoModelXml
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/subsystems/TimingDetector/CMakeLists.txt` around lines 6 - 18, The
TimingDetector target is missing its direct dependency on SHiPGeometry, which is
needed for TimingDetectorFactory.cpp to use SHiPMaterials. Update the
target_link_libraries block for TimingDetector to link SHiPGeometry alongside
the existing GeoModelCore::GeoModelKernel and GeoModelTools::GeoModelXml
entries, so consumers of TimingDetector do not need to add that library
separately.

Comment thread stage/tests/CMakeLists.txt Outdated
Comment on lines +24 to +32
# Integration test: build geometry into SQLite then run overlap check
add_test(
NAME OverlapCheck
COMMAND
bash -c
"$<TARGET_FILE:build_geometry> ship_test_overlap.db &&
$<TARGET_FILE:validate_geometry> ship_test_overlap.db"
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a CMakeLists.txt stage/apps | xargs grep -n "build_geometry\|validate_geometry"

Repository: ShipSoft/Geometry

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Top-level directories:\n'
find . -maxdepth 2 -type d | sort | sed -n '1,120p'

printf '\nCMakeLists files mentioning build_geometry/validate_geometry:\n'
rg -n --hidden --glob 'CMakeLists.txt' 'build_geometry|validate_geometry' .

printf '\nFiles named CMakeLists.txt under stage:\n'
fd -a CMakeLists.txt stage .

Repository: ShipSoft/Geometry

Length of output: 2129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'apps/CMakeLists.txt:\n'
cat -n apps/CMakeLists.txt

printf '\nvalidate_geometry.cpp:\n'
cat -n apps/validate_geometry.cpp

printf '\ntests/CMakeLists.txt around OverlapCheck:\n'
sed -n '20,40p' tests/CMakeLists.txt

printf '\nCI workflows mentioning tests:\n'
rg -n "ctest|bash|ubuntu|windows|macos" .github/workflows -S

Repository: ShipSoft/Geometry

Length of output: 11163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'build_geometry.cpp:\n'
cat -n apps/build_geometry.cpp

printf '\nRoot CMakeLists relevant lines:\n'
sed -n '70,95p' CMakeLists.txt

printf '\nWorkflow files:\n'
fd -a .github/workflows -e yml -e yaml .

printf '\nWorkflow excerpts mentioning runs-on / bash / ctest:\n'
rg -n "runs-on|bash|ctest|ubuntu|windows|macos" .github/workflows -S

Repository: ShipSoft/Geometry

Length of output: 2714


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '.github/workflows files:\n'
fd . .github/workflows -t f

printf '\nWorkflow excerpts mentioning shells / runners / ctest:\n'
rg -n "runs-on|shell:|bash|ctest|ubuntu|windows|macos" .github/workflows -S

printf '\nWhere validate_geometry decides its exit code:\n'
rg -n "return 0|return 1|overlap|validateGeometry|isValid" apps/validate_geometry.cpp -n -S

Repository: ShipSoft/Geometry

Length of output: 396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'build-test.yml:\n'
cat -n .github/workflows/build-test.yml

printf '\nrelease.yml:\n'
cat -n .github/workflows/release.yml | sed -n '1,220p'

Repository: ShipSoft/Geometry

Length of output: 1162


Make validate_geometry fail on invalid geometry

build_geometry and validate_geometry both exist, but validate_geometry.cpp only prints a report and main() always returns 0. As written, OverlapCheck can pass even when the geometry is invalid or overlaps are present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/tests/CMakeLists.txt` around lines 24 - 32, `validate_geometry`
currently reports issues but never fails the test because `main()` always
returns 0. Update `validate_geometry.cpp` so `main()` returns a non-zero exit
code whenever invalid geometry or overlaps are detected, and keep 0 only for the
all-clear case. Use the existing validation/reporting logic in
`validate_geometry` to drive the exit status so the `OverlapCheck` test
correctly fails on bad geometry.

Comment thread stage/tests/test_consistency.cpp Outdated
Comment on lines +34 to +45
double getHalfZ(const GeoVPhysVol* vol) {
const auto* box = dynamic_cast<const GeoBox*>(vol->getLogVol()->getShape());
if (box) {
return box->getZHalfLength();
}
// For non-box shapes (e.g. GeoFullPhysVol with GeoBox), try the tube
const auto* tube = dynamic_cast<const GeoTube*>(vol->getLogVol()->getShape());
if (tube) {
return tube->getZHalfLength();
}
return 0.0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Silent 0.0 fallback in getHalfZ can mask real overlaps.

Returning 0.0 for any shape that isn't GeoBox/GeoTube silently disables the overlap/order checks for that subsystem rather than failing loudly. If any top-level subsystem shape (e.g. a boolean/union shape) isn't one of these two, NoUnexpectedZOverlaps and Z-order checks will pass vacuously instead of catching real problems.

Consider failing the test (e.g. FAIL/REQUIRE) or logging a clear diagnostic when the shape type is unrecognized, so silent false negatives don't creep in as subsystems evolve.

Proposed fix
 double getHalfZ(const GeoVPhysVol* vol) {
     const auto* box = dynamic_cast<const GeoBox*>(vol->getLogVol()->getShape());
     if (box) {
         return box->getZHalfLength();
     }
     // For non-box shapes (e.g. GeoFullPhysVol with GeoBox), try the tube
     const auto* tube = dynamic_cast<const GeoTube*>(vol->getLogVol()->getShape());
     if (tube) {
         return tube->getZHalfLength();
     }
-    return 0.0;
+    FAIL("Unsupported shape type for subsystem " << vol->getLogVol()->getName()
+         << "; extend getHalfZ to handle it.");
+    return 0.0;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
double getHalfZ(const GeoVPhysVol* vol) {
const auto* box = dynamic_cast<const GeoBox*>(vol->getLogVol()->getShape());
if (box) {
return box->getZHalfLength();
}
// For non-box shapes (e.g. GeoFullPhysVol with GeoBox), try the tube
const auto* tube = dynamic_cast<const GeoTube*>(vol->getLogVol()->getShape());
if (tube) {
return tube->getZHalfLength();
}
return 0.0;
}
double getHalfZ(const GeoVPhysVol* vol) {
const auto* box = dynamic_cast<const GeoBox*>(vol->getLogVol()->getShape());
if (box) {
return box->getZHalfLength();
}
// For non-box shapes (e.g. GeoFullPhysVol with GeoBox), try the tube
const auto* tube = dynamic_cast<const GeoTube*>(vol->getLogVol()->getShape());
if (tube) {
return tube->getZHalfLength();
}
FAIL("Unsupported shape type for subsystem " << vol->getLogVol()->getName()
<< "; extend getHalfZ to handle it.");
return 0.0;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stage/tests/test_consistency.cpp` around lines 34 - 45, The fallback in
getHalfZ currently returns 0.0 for unrecognized shapes, which can make the
Z-overlap and ordering checks in NoUnexpectedZOverlaps pass silently. Update
getHalfZ to explicitly handle only the supported shape types and fail loudly or
emit a clear diagnostic when vol->getLogVol()->getShape() is neither GeoBox nor
GeoTube, so unexpected subsystem shapes do not bypass the test logic.

@matclim

matclim commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@matclim
matclim requested a review from olantwin July 2, 2026 11:45
@olantwin

olantwin commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Could you please reply to coderabbit where it did not identify the comment as resolved and also check the minor and nitpick comments that weren't posted as inline comments?

@olantwin

olantwin commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

I'll review this myself ASAP

@matclim

matclim commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@olantwin

olantwin commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Please remove the two tarballs from this PR.

@matclim
matclim force-pushed the MS branch 3 times, most recently from 040dc39 to 9b4b44a Compare July 3, 2026 22:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
subsystems/MuonShield/README.md (1)

29-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language identifier to the fenced code block.

Flagged by markdownlint (MD040).

📝 Proposed fix
-```
+```text
 muon_shield (Air envelope)
  ├─ block_0 (Iron, upstream of the gap)
  └─ block_1 (Iron, downstream of the gap)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @subsystems/MuonShield/README.md around lines 29 - 33, Add a language
identifier to the fenced Markdown block in the MuonShield README to satisfy
MD040; update the existing fenced snippet around the muon_shield diagram so it
uses a text-style fence rather than an unlabeled code block.


</details>

<!-- cr-comment:v1:41270a496e0eab1dfcd98e2a -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>subsystems/MuonShield/src/MuonShieldFactory.cpp (1)</summary><blockquote>

`38-50`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Prefer `std::filesystem::path::is_absolute()` over manual `'/'` check.**

More idiomatic and future-proof than a manual first-character check, since `<filesystem>` is already included.




<details>
<summary>♻️ Proposed refactor</summary>

```diff
 static std::string resolveTomlPath(const std::string& path) {
-    if (!path.empty() && path[0] == '/')
+    if (!path.empty() && std::filesystem::path(path).is_absolute())
         return path;  // already absolute
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp` around lines 38 - 50, The
path resolution helper in resolveTomlPath is using a manual first-character
check to detect absolute paths, which is less idiomatic than the filesystem API
already available. Update the absolute-path branch in resolveTomlPath to use
std::filesystem::path::is_absolute() on the input path, while keeping the
existing fallback lookup behavior for the relative, source-tree, and install
paths unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@subsystems/MuonShield/README.md`:
- Around line 29-33: Add a language identifier to the fenced Markdown block in
the MuonShield README to satisfy MD040; update the existing fenced snippet
around the muon_shield diagram so it uses a text-style fence rather than an
unlabeled code block.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 38-50: The path resolution helper in resolveTomlPath is using a
manual first-character check to detect absolute paths, which is less idiomatic
than the filesystem API already available. Update the absolute-path branch in
resolveTomlPath to use std::filesystem::path::is_absolute() on the input path,
while keeping the existing fallback lookup behavior for the relative,
source-tree, and install paths unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58ad5b46-a262-47cf-af63-cc7c1d2a2d15

📥 Commits

Reviewing files that changed from the base of the PR and between 75a7271 and 040dc39.

📒 Files selected for processing (10)
  • src/SHiPGeometry.cpp
  • subsystems/MuonShield/MS.toml
  • subsystems/MuonShield/README.md
  • subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h
  • subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h
  • subsystems/MuonShield/src/MuonShieldConfig.cpp
  • subsystems/MuonShield/src/MuonShieldFactory.cpp
  • subsystems/MuonShield/test_muonshield.cpp
  • subsystems/NeutrinoDetector/README.md
  • tests/test_consistency.cpp
✅ Files skipped from review due to trivial changes (1)
  • subsystems/NeutrinoDetector/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h

@matclim
matclim requested a review from Gfrisella July 7, 2026 08:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
subsystems/MuonShield/test_muonshield.cpp (1)

86-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test exercises the rotation block field.

Rotation is a documented feature (README Lines 68–75) and parsed by readFixedArray<3> in MuonShieldConfig.cpp, but no test verifies that a rotated block produces the expected GeoBox/GeoTrd orientation. Consider adding a test with a non-zero rotation to validate the extrinsic X→Y→Z transform is applied correctly about start.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/test_muonshield.cpp` around lines 86 - 103, The
MuonShield test coverage is missing validation for the block-level rotation
field, so add a new case in test_muonshield.cpp alongside
MuonShieldTaperMakesTrd that configures a non-zero rotation and asserts the
resulting GeoBox or GeoTrd is oriented correctly. Use MuonShieldFactory::build,
the parsed rotation from MuonShieldConfig.cpp, and the block’s start point as
the pivot, then verify the extrinsic X→Y→Z transform is reflected in the child
volume’s shape/orientation.
subsystems/MuonShield/src/MuonShieldConfig.cpp (2)

54-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

readNumeric and nodeToNumber duplicate the same int/float-to-double logic.

Both functions accept either a TOML float or integer and return double, differing only in input type (toml::node_view<toml::node> vs const toml::node*). A single templated helper or having readNumeric delegate to nodeToNumber would eliminate the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp` around lines 54 - 72, The
int/float-to-double conversion logic is duplicated between readNumeric and
nodeToNumber in MuonShieldConfig.cpp. Refactor so there is a single helper that
performs the TOML numeric extraction and conversion, then have the other
function delegate to it; keep the existing error text behavior intact and
preserve the current signatures of readNumeric and nodeToNumber if they’re used
elsewhere.

162-163: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

kDegToRad duplicates the degToRad constant in MuonShieldFactory.cpp line 78.

Both files define the same 3.14159265358979323846 / 180.0 constant with different names. A shared constant (e.g., in MuonShieldConfig.h or a common header) would prevent drift and improve consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp` around lines 162 - 163, The
degree-to-radian constant is duplicated between MuonShieldConfig.cpp and
MuonShieldFactory.cpp under different names, so centralize it in a shared place
such as MuonShieldConfig.h or a common MuonShield header and update both
MuonShieldConfig and MuonShieldFactory to use the same symbol. Keep the existing
kEps local if needed, but remove the duplicate degToRad/kDegToRad definition so
there is a single source of truth for the conversion constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@subsystems/MuonShield/README.md`:
- Around line 45-52: The MuonShield config defaults in the README do not match
the actual auto-sized geometry produced by the envelope factory, so update the
table to reflect that these are parser fallbacks rather than the built
dimensions. In the MuonShield README section with the config table, add a clear
note near the `[[block]]`/auto-sizing description that the factory auto-sizes
the envelope to contain all blocks, and make the `envelope_half_x_mm`,
`envelope_half_y_mm`, `envelope_z_start_m`, and `envelope_z_end_m` entries
consistent with the geometry tree/test values or explicitly label them as
overridden defaults.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp`:
- Around line 178-183: The MuonShieldConfig block Z validation only checks the
upstream face, so a block can still extend past the envelope end. Update the
validation in MuonShieldConfig::... around the existing b.start[2] sanity check
to also verify the downstream edge using b.start[2] + b.size[2] against
envEndMm, and throw a similar std::runtime_error when that end lies outside the
envelope.

---

Nitpick comments:
In `@subsystems/MuonShield/src/MuonShieldConfig.cpp`:
- Around line 54-72: The int/float-to-double conversion logic is duplicated
between readNumeric and nodeToNumber in MuonShieldConfig.cpp. Refactor so there
is a single helper that performs the TOML numeric extraction and conversion,
then have the other function delegate to it; keep the existing error text
behavior intact and preserve the current signatures of readNumeric and
nodeToNumber if they’re used elsewhere.
- Around line 162-163: The degree-to-radian constant is duplicated between
MuonShieldConfig.cpp and MuonShieldFactory.cpp under different names, so
centralize it in a shared place such as MuonShieldConfig.h or a common
MuonShield header and update both MuonShieldConfig and MuonShieldFactory to use
the same symbol. Keep the existing kEps local if needed, but remove the
duplicate degToRad/kDegToRad definition so there is a single source of truth for
the conversion constant.

In `@subsystems/MuonShield/test_muonshield.cpp`:
- Around line 86-103: The MuonShield test coverage is missing validation for the
block-level rotation field, so add a new case in test_muonshield.cpp alongside
MuonShieldTaperMakesTrd that configures a non-zero rotation and asserts the
resulting GeoBox or GeoTrd is oriented correctly. Use MuonShieldFactory::build,
the parsed rotation from MuonShieldConfig.cpp, and the block’s start point as
the pivot, then verify the extrinsic X→Y→Z transform is reflected in the child
volume’s shape/orientation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 943c8557-3dcd-4c43-9a91-07f663968af5

📥 Commits

Reviewing files that changed from the base of the PR and between 040dc39 and fced812.

📒 Files selected for processing (10)
  • src/SHiPGeometry.cpp
  • subsystems/MuonShield/MS.toml
  • subsystems/MuonShield/README.md
  • subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h
  • subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h
  • subsystems/MuonShield/src/MuonShieldConfig.cpp
  • subsystems/MuonShield/src/MuonShieldFactory.cpp
  • subsystems/MuonShield/test_muonshield.cpp
  • subsystems/NeutrinoDetector/README.md
  • tests/test_consistency.cpp
✅ Files skipped from review due to trivial changes (1)
  • subsystems/NeutrinoDetector/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_consistency.cpp

Comment thread subsystems/MuonShield/README.md Outdated
Comment thread subsystems/MuonShield/src/MuonShieldConfig.cpp Outdated
@matclim

matclim commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

In principle everything should be in order now.

@Gfrisella the config (subsystems/MuonShield/MS.toml) looks like this:

# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) CERN for the benefit of the SHiP Collaboration
#
# Muon shield geometry — solid-block approximation of the FairShip TRY_2026
# magnet layout (FairShip PR #1334). Generated by fairship_to_mstoml.py.
#
# Each FairShip magnet is reproduced as a solid iron block from its OUTER
# envelope (dX*(1+ratio)+midGap+ceil(max(100/dY,gap)) x dY+dY_yoke). The central
# aperture, yoke shape and field are dropped (field handled elsewhere).
#
# The FairShip absorber (row 0) is the magnetised hadron stopper and lives in
# the Target subsystem (2.14-4.44 m), so the shield front is 2.14 m and magnet 1
# begins at 4.59 m.
#
# The SND is a daughter of the shield (own subsystem, embedded by
# SHiPGeometryBuilder) and passes through the last two magnets (6 and 7). As in
# FairShip's SetSNDSpace, those magnets carry a central rectangular aperture for
# it: each is built as a 4-block frame (L/R/T/B) around the 800 x 800 mm SND
# opening, so the SND sits in real iron apertures with no overlap. Taper is
# dropped for these two magnets (near-face dims).
block_material = "Iron"
envelope_half_x_mm = 1760.0
envelope_half_y_mm = 1320.0
envelope_z_start_m = 4.5400
envelope_z_end_m   = 32.0800

# magnet 1
[[block]]
start = [0.0, 0.0, 4590.0]
size  = [2720.0, 1600.0, 3000.0]

# magnet 2
[[block]]
start = [0.0, 0.0, 7740.0]
size  = [2980.0, 1780.0, 4500.0]

# magnet 3
[[block]]
start = [0.0, 0.0, 12400.0]
size  = [3104.0, 1700.0, 4500.0]

# magnet 4
[[block]]
start = [0.0, 0.0, 17050.0]
size  = [3184.0, 1200.0, 4500.0]

# magnet 5
[[block]]
start = [0.0, 0.0, 21710.0]
size  = [2680.0, 1040.0, 2620.0]

# magnet 6 (SND aperture) R
[[block]]
start = [950.0, 0.0, 24480.0]
size  = [1100.0, 1060.0, 3360.0]

# magnet 6 (SND aperture) L
[[block]]
start = [-950.0, 0.0, 24480.0]
size  = [1100.0, 1060.0, 3360.0]

# magnet 6 (SND aperture) T
[[block]]
start = [0.0, 465.0, 24480.0]
size  = [800.0, 130.0, 3360.0]

# magnet 6 (SND aperture) B
[[block]]
start = [0.0, -465.0, 24480.0]
size  = [800.0, 130.0, 3360.0]

# magnet 7 (SND aperture) R
[[block]]
start = [1055.0, 0.0, 28030.0]
size  = [1310.0, 2540.0, 4000.0]

# magnet 7 (SND aperture) L
[[block]]
start = [-1055.0, 0.0, 28030.0]
size  = [1310.0, 2540.0, 4000.0]

# magnet 7 (SND aperture) T
[[block]]
start = [0.0, 835.0, 28030.0]
size  = [800.0, 870.0, 4000.0]

# magnet 7 (SND aperture) B
[[block]]
start = [0.0, -835.0, 28030.0]
size  = [800.0, 870.0, 4000.0]

The iron blocks around the SD/SND (will fix name inconsistency in a future PR) are segmented into 4 blocks as it is preferable to give space to the SD.

The current version should reflect the geometry changes you made in ShipSoft/FairShip#1334 exactly.

@olantwin

olantwin commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Could you please update the PR title, PR text, and clean up the commit history?

@Gfrisella

Copy link
Copy Markdown

Thanks Matei for the amazing job!
I just want to point out that the SND will be stored in magnets 5 and 6.
Magnet 7 will be normal.

@Gfrisella

Copy link
Copy Markdown

Moreover, would it be possible to allocate the space afterwards?
My idea is to define the MS and the SND as two independent detectors, and then create the space for the SND inside the MS by applying a Boolean subtraction (A − B). This way, the MS can be defined independently, while the SND space reservation is handled in a separate file and merged afterwards using the A − B operation.

@matclim

matclim commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Will implement, thank you for the comments!

@matclim matclim changed the title feat(ms)!: Add configurable iron-block muon shield feat(ms)!: Add configurable iron-block muon shield (FairShip TRY_2026) Jul 9, 2026
@matclim

matclim commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

I think we should be clean now @Gfrisella

@Gfrisella

Copy link
Copy Markdown

I think we should be clean now @Gfrisella

Yepppp

@matclim

matclim commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
subsystems/MuonShield/src/MuonShieldFactory.cpp (1)

41-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing resolveConfigPath instead of duplicating the resolution logic.

resolveTomlPath duplicates the same fallback-chain logic already implemented in resolveConfigPath (include/SHiPGeometry/ConfigPath.h). Delegating to the shared helper would eliminate the duplication and automatically pick up the more robust is_regular_file check.

♻️ Proposed refactor
-#ifndef MS_TOML_DEFAULT_PATH
-#define MS_TOML_DEFAULT_PATH "MS.toml"
-#endif
-#ifndef MS_TOML_INSTALL_PATH
-#define MS_TOML_INSTALL_PATH ""
-#endif
-
-static std::string resolveTomlPath(const std::string& path) {
-    if (std::filesystem::path(path).is_absolute())
-        return path;
-    if (std::filesystem::is_regular_file(path))
-        return path;
-    const std::string srcFallback = MS_TOML_DEFAULT_PATH;
-    if (std::filesystem::is_regular_file(srcFallback))
-        return srcFallback;
-    const std::string installFallback = MS_TOML_INSTALL_PATH;
-    if (!installFallback.empty() && std::filesystem::is_regular_file(installFallback))
-        return installFallback;
-    return path;
-}
+// Keep the MS_TOML_DEFAULT_PATH / MS_TOML_INSTALL_PATH macros for the call site.
+static std::string resolveTomlPath(const std::string& path) {
+    return resolveConfigPath(path, MS_TOML_DEFAULT_PATH, MS_TOML_INSTALL_PATH);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp` around lines 41 - 53,
Replace the duplicated fallback logic in resolveTomlPath with a call to the
shared resolveConfigPath helper from ConfigPath.h, preserving the existing input
and return behavior. Ensure the helper’s regular-file validation is used and
remove the local filesystem checks and fallback constants that are no longer
needed.
subsystems/NeutrinoDetector/src/SNDEnvelope.cpp (1)

47-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unknown-key warnings for consistency with readMuonShieldConfig.

readMuonShieldConfig performs a first pass that warns about unrecognized top-level keys, catching typos and stale fields. readSNDEnvelope silently ignores unknown keys — a typo in the optional rotation field (e.g. rotaton) would fall back to the default {0,0,0} with no diagnostic, which could be hard to debug in production geometry.

♻️ Proposed addition
 SNDEnvelope readSNDEnvelope(const std::string& path) {
     toml::table table;
     try {
         table = toml::parse_file(path);
     } catch (const toml::parse_error& e) {
         throw std::runtime_error("SNDEnvelope: failed to parse " + path + ": " +
                                  std::string(e.description()));
     }

+    // Warn about unknown keys (typo / stale-field guard).
+    static constexpr std::string_view kKnownKeys[] = {"centre", "size", "rotation"};
+    for (const auto& [k, _] : table) {
+        bool known = false;
+        for (const auto& kk : kKnownKeys)
+            if (k == kk) { known = true; break; }
+        if (!known)
+            std::cerr << "SNDEnvelope: warning: unknown key '" << k << "' in " << path
+                      << " (typo? stale field? — value will be ignored)\n";
+    }
+
     SNDEnvelope env;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/NeutrinoDetector/src/SNDEnvelope.cpp` around lines 47 - 64, Add a
top-level unknown-key validation pass in readSNDEnvelope, matching
readMuonShieldConfig: iterate over the parsed table keys, allow only centre,
size, and rotation, and emit warnings for any unrecognized keys before reading
the fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/SHiPGeometry.cpp`:
- Around line 51-68: Register the neutrino detector cavity with
MuonShieldFactory before building the shield: call reserveSpace(...) using the
SND dimensions and placement defined by subsystems/NeutrinoDetector/SD.toml,
centered at 28.95 m. Add this reservation after constructing muonShieldFactory
and before muonShieldFactory.build(), while retaining the existing
embedDaughter(...) call.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 41-53: Replace the std::filesystem::exists checks in
resolveTomlPath with std::filesystem::is_regular_file for the input path and
both fallback paths, preserving the existing absolute-path handling and fallback
order.
- Around line 77-175: Register the SND cavity reservation before constructing
the muon shield: update the SHiP geometry build flow that embeds the neutrino
detector to call MuonShieldFactory::reserveSpace() with the detector’s world
position, dimensions, and rotation before invoking MuonShieldFactory::build().
Ensure this reservation is added before block subtraction in
MuonShieldFactory::build() is reached.

In `@subsystems/NeutrinoDetector/README.md`:
- Around line 51-57: Correct the README description of
MuonShieldFactory::embedDaughter: state that it only nests the pre-built
daughter volume in the muon-shield volume tree, while
MuonShieldFactory::reserveSpace registers and carves the matching air gap from
the shield iron. Preserve the explanation that the SND remains an independent
subsystem.

---

Nitpick comments:
In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 41-53: Replace the duplicated fallback logic in resolveTomlPath
with a call to the shared resolveConfigPath helper from ConfigPath.h, preserving
the existing input and return behavior. Ensure the helper’s regular-file
validation is used and remove the local filesystem checks and fallback constants
that are no longer needed.

In `@subsystems/NeutrinoDetector/src/SNDEnvelope.cpp`:
- Around line 47-64: Add a top-level unknown-key validation pass in
readSNDEnvelope, matching readMuonShieldConfig: iterate over the parsed table
keys, allow only centre, size, and rotation, and emit warnings for any
unrecognized keys before reading the fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a518f180-fba2-496f-be49-b6e0a94c6b92

📥 Commits

Reviewing files that changed from the base of the PR and between 754fa69 and af92028.

📒 Files selected for processing (15)
  • src/SHiPGeometry.cpp
  • subsystems/MuonShield/CMakeLists.txt
  • subsystems/MuonShield/MS.toml
  • subsystems/MuonShield/README.md
  • subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h
  • subsystems/MuonShield/include/MuonShield/MuonShieldFactory.h
  • subsystems/MuonShield/src/MuonShieldConfig.cpp
  • subsystems/MuonShield/src/MuonShieldFactory.cpp
  • subsystems/MuonShield/test_muonshield.cpp
  • subsystems/NeutrinoDetector/CMakeLists.txt
  • subsystems/NeutrinoDetector/README.md
  • subsystems/NeutrinoDetector/SD.toml
  • subsystems/NeutrinoDetector/include/NeutrinoDetector/SNDEnvelope.h
  • subsystems/NeutrinoDetector/src/SNDEnvelope.cpp
  • tests/test_consistency.cpp

Comment thread src/SHiPGeometry.cpp
Comment on lines +51 to +68
// Build the muon shield, with the neutrino detector embedded inside it.
//
// The SND is an independent subsystem, but in volume terms it is a daughter
// of the muon-shield container: it sits within the shield region (WARM SND
// slot 26.40–31.50 m → centre 28.95 m). We build it first, then nest it in
// the shield container. The muon-shield block list must leave this slot
// free of iron.
MuonShieldFactory muonShieldFactory(materials);
GeoPhysVol* muonShield = muonShieldFactory.build();
placeChild(world, muonShield, "/SHiP/muon_shield", 2,
GeoTrf::Translate3D(0.0, 0.0, 16763.3 * mm));

// Build and place the Scattering and Neutrino Detector (SND).
// Z: 26.40 to 31.50 m (WARM muon-shield configuration) → centre 28.95 m.
// The SND sits within the downstream end of the muon-shield region, so its
// envelope overlaps the muon-shield container by design (see test_consistency).
NeutrinoDetectorFactory neutrinoDetectorFactory(materials);
GeoPhysVol* neutrinoDetector = neutrinoDetectorFactory.build();
placeChild(world, neutrinoDetector, "/SHiP/neutrino_detector", 9,
GeoTrf::Translate3D(0.0, 0.0, 28.95 * m));
muonShieldFactory.embedDaughter(neutrinoDetector, 28.95 * 1000.0, "/SHiP/neutrino_detector");

// The container is built centred on its own origin, so it is placed at the
// envelope centre reported by the factory after build().
GeoPhysVol* muonShield = muonShieldFactory.build();
placeChild(world, muonShield, "/SHiP/muon_shield", 2,
GeoTrf::Translate3D(0.0, 0.0, muonShieldFactory.centreZ_mm()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all calls to reserveSpace across the codebase.
rg -nP -C3 '\breservespace\s*\(' --type=cpp -g '!**/build/**'

# Also check if SD.toml is read anywhere outside the provided files.
rg -nP -C3 'SD\.toml' --type=cpp -g '!**/build/**'

# Check the actual MS.toml block list for blocks overlapping the SND Z range (26400-31500 mm).
fd 'MS.toml' --exec sh -c 'echo "=== {} ===" && cat "{}"'

Repository: ShipSoft/Geometry

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any reserveSpace implementations/calls.
rg -n --hidden --glob '!**/build/**' '\breserveSpace\s*\(' .

# Find any references to SD.toml.
rg -n --hidden --glob '!**/build/**' 'SD\.toml' .

# Locate geometry-related TOML files.
fd -a 'MS\.toml|SD\.toml|.*\.toml$' .

# Inspect the SHiP geometry source and related factory implementation.
fd -a 'SHiPGeometry.cpp|MuonShieldFactory.*|NeutrinoDetectorFactory.*' src .

Repository: ShipSoft/Geometry

Length of output: 3045


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== src/SHiPGeometry.cpp ==='
nl -ba src/SHiPGeometry.cpp | sed -n '1,120p'

echo
echo '=== subsystems/MuonShield/src/MuonShieldFactory.cpp ==='
nl -ba subsystems/MuonShield/src/MuonShieldFactory.cpp | sed -n '1,260p'

echo
echo '=== subsystems/MuonShield/MS.toml ==='
nl -ba subsystems/MuonShield/MS.toml | sed -n '1,220p'

echo
echo '=== subsystems/NeutrinoDetector/SD.toml ==='
nl -ba subsystems/NeutrinoDetector/SD.toml | sed -n '1,220p'

Repository: ShipSoft/Geometry

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path

files = [
    "src/SHiPGeometry.cpp",
    "subsystems/MuonShield/src/MuonShieldFactory.cpp",
    "subsystems/MuonShield/MS.toml",
    "subsystems/NeutrinoDetector/SD.toml",
]
for fp in files:
    p = Path(fp)
    print(f"=== {fp} ===")
    if not p.exists():
        print("<missing>")
        print()
        continue
    for i, line in enumerate(p.read_text().splitlines(), 1):
        print(f"{i:4}: {line}")
    print()
PY

Repository: ShipSoft/Geometry

Length of output: 18351


Add the SND reservation before building the muon shield in src/SHiPGeometry.cpp:51-62. MuonShieldFactory::build() only subtracts boxes registered via reserveSpace(...); this code embeds the SND but never registers the subsystems/NeutrinoDetector/SD.toml cavity, while subsystems/MuonShield/MS.toml blocks 6–7 already span the 26.4–31.5 m slot. Without that call, the detector overlaps solid iron.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SHiPGeometry.cpp` around lines 51 - 68, Register the neutrino detector
cavity with MuonShieldFactory before building the shield: call reserveSpace(...)
using the SND dimensions and placement defined by
subsystems/NeutrinoDetector/SD.toml, centered at 28.95 m. Add this reservation
after constructing muonShieldFactory and before muonShieldFactory.build(), while
retaining the existing embedDaughter(...) call.

Comment thread subsystems/MuonShield/src/MuonShieldFactory.cpp Outdated
Comment on lines 77 to 175
GeoPhysVol* MuonShieldFactory::build() {
const GeoMaterial* air = m_materials.requireMaterial("Air");

// Overall MuonShieldArea container (Air)
auto* areaBox = new GeoBox(s_areaHalfX, s_areaHalfY, s_areaHalfZ);
auto* areaLog = new GeoLogVol("/SHiP/muon_shield", areaBox, air);
auto* areaPhys = new GeoPhysVol(areaLog);

// Build and place 6 stations
for (const StationData& station : k_stations) {
GeoPhysVol* stationPhys = buildStation(station);
std::string stationName = "/SHiP/muon_shield/" + std::string(station.name);
GeoTrf::Transform3D trf = GeoTrf::Translate3D(0.0, 0.0, station.stationZ);
areaPhys->add(new GeoNameTag(stationName));
areaPhys->add(new GeoIdentifierTag(static_cast<int>(&station - &k_stations[0])));
areaPhys->add(new GeoTransform(trf));
areaPhys->add(stationPhys);
const MuonShieldConfig cfg = readMuonShieldConfig(resolveTomlPath(m_configPath));

GeoMaterial* air = m_materials.requireMaterial("Air");
GeoMaterial* absorber = m_materials.requireMaterial(cfg.block_material);

// Envelope-derived dimensions (mm).
const double halfZ = 0.5 * cfg.envelopeLengthZ_mm();
m_centreZ_mm = cfg.envelopeCentreZ_mm();

// Air container spanning the full muon-shield envelope, centred on its own
// origin (SHiPGeometryBuilder places it at centreZ_mm()).
auto* containerBox = new GeoBox(cfg.envelope_half_x_mm, cfg.envelope_half_y_mm, halfZ);
auto* containerLog = new GeoLogVol("/SHiP/muon_shield", containerBox, air);
auto* containerPhys = new GeoPhysVol(containerLog);

// Place each iron block. A block is anchored by its upstream (−z) face at
// its `start`, extends downstream by size[2], is rotated about `start`
// (extrinsic X→Y→Z), and may taper symmetrically into a GeoTrd.
int childId = 0;
for (const auto& b : cfg.blocks) {
const double halfX = 0.5 * b.size[0];
const double halfY = 0.5 * b.size[1];
const double halfLenZ = 0.5 * b.size[2];

const double farHalfX = halfX + b.size[2] * std::tan(b.taper_deg[0] * kDegToRad);
const double farHalfY = halfY + b.size[2] * std::tan(b.taper_deg[1] * kDegToRad);

// Placement transform: shift the solid (centred at its origin) so its
// upstream face sits at `start`, rotate about `start` (extrinsic
// X→Y→Z), then translate into place. Container is at m_centreZ_mm in Z.
const double anchorX = b.start[0];
const double anchorY = b.start[1];
const double anchorZ = b.start[2] - m_centreZ_mm;
const GeoTrf::Transform3D rotation = GeoTrf::RotateZ3D(b.rotation_deg[2] * kDegToRad) *
GeoTrf::RotateY3D(b.rotation_deg[1] * kDegToRad) *
GeoTrf::RotateX3D(b.rotation_deg[0] * kDegToRad);
const GeoTrf::Transform3D trf = GeoTrf::Translate3D(anchorX, anchorY, anchorZ) * rotation *
GeoTrf::Translate3D(0.0, 0.0, halfLenZ);

GeoShape* shape = nullptr;
if (b.taper_deg[0] == 0.0 && b.taper_deg[1] == 0.0)
shape = new GeoBox(halfX, halfY, halfLenZ);
else
shape = new GeoTrd(halfX, farHalfX, halfY, farHalfY, halfLenZ);

// Carve out any reserved box that intersects this magnet (A - B). The
// reserved box, given in world coords, is expressed in the block's own
// (shape) frame before subtracting.
const GeoTrf::Vector3D blockCentre = trf.translation();
const double blockMaxHX = std::max(halfX, farHalfX);
const double blockMaxHY = std::max(halfY, farHalfY);
for (const auto& r : m_reservations) {
const double rcx = r.centre_mm[0];
const double rcy = r.centre_mm[1];
const double rcz = r.centre_mm[2] - m_centreZ_mm;
const double rhx = 0.5 * r.size_mm[0];
const double rhy = 0.5 * r.size_mm[1];
const double rhz = 0.5 * r.size_mm[2];
const bool intersects = std::abs(blockCentre.x() - rcx) < blockMaxHX + rhx &&
std::abs(blockCentre.y() - rcy) < blockMaxHY + rhy &&
std::abs(blockCentre.z() - rcz) < halfLenZ + rhz;
if (!intersects)
continue;
const GeoTrf::Transform3D resRot = GeoTrf::RotateZ3D(r.rotation_deg[2] * kDegToRad) *
GeoTrf::RotateY3D(r.rotation_deg[1] * kDegToRad) *
GeoTrf::RotateX3D(r.rotation_deg[0] * kDegToRad);
const GeoTrf::Transform3D resInBlock =
trf.inverse() * GeoTrf::Translate3D(rcx, rcy, rcz) * resRot;
auto* voidBox = new GeoBox(rhx, rhy, rhz);
shape = new GeoShapeSubtraction(shape, new GeoShapeShift(voidBox, resInBlock));
}

const std::string blockName = "/SHiP/muon_shield/block_" + std::to_string(childId);
auto* blockLog = new GeoLogVol(blockName, shape, absorber);
auto* blockPhys = new GeoPhysVol(blockLog);

containerPhys->add(new GeoNameTag(blockName));
containerPhys->add(new GeoIdentifierTag(childId++));
containerPhys->add(new GeoTransform(trf));
containerPhys->add(blockPhys);
}

// Place the embedded daughters inside the container, keeping them as
// independent, named subsystems. (Callers must not list iron blocks over a
// daughter's footprint — the block list is the sole source of iron.)
for (const auto& d : m_daughters) {
const double localZ = d.worldCentreZ_mm - m_centreZ_mm;
if (std::abs(localZ) > halfZ)
throw std::runtime_error("MuonShieldFactory: embedded daughter '" + d.name +
"' lies outside the shield envelope in Z");
containerPhys->add(new GeoNameTag(d.name));
containerPhys->add(new GeoIdentifierTag(childId++));
containerPhys->add(new GeoTransform(GeoTrf::Translate3D(0.0, 0.0, localZ)));
containerPhys->add(d.volume);
}

return areaPhys;
return containerPhys;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all calls to reserveSpace in the codebase
rg -nP '\breserveSpace\s*\(' --type=cpp -C3

Repository: ShipSoft/Geometry

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## reserveSpace references (all files)'
rg -n -S '\breserveSpace\s*\(' .

echo
echo '## embedDaughter references'
rg -n -S '\bembedDaughter\s*\(' .

echo
echo '## MuonShieldFactory references'
rg -n -S '\bMuonShieldFactory\b' .

echo
echo '## SHiPGeometryBuilder references'
rg -n -S '\bSHiPGeometryBuilder\b' .

Repository: ShipSoft/Geometry

Length of output: 6295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## src/SHiPGeometry.cpp'
sed -n '1,180p' src/SHiPGeometry.cpp

echo
echo '## subsystems/MuonShield/README.md around reserveSpace'
sed -n '80,110p' subsystems/MuonShield/README.md

echo
echo '## subsystems/MuonShield/test_muonshield.cpp around reserveSpace/embedDaughter'
sed -n '70,170p' subsystems/MuonShield/test_muonshield.cpp

Repository: ShipSoft/Geometry

Length of output: 11843


Call reserveSpace() before building the muon shield src/SHiPGeometry.cpp embeds the neutrino detector, but never registers the SND cavity reservation, so the subtraction in MuonShieldFactory::build() never runs and the detector can overlap iron.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp` around lines 77 - 175,
Register the SND cavity reservation before constructing the muon shield: update
the SHiP geometry build flow that embeds the neutrino detector to call
MuonShieldFactory::reserveSpace() with the detector’s world position,
dimensions, and rotation before invoking MuonShieldFactory::build(). Ensure this
reservation is added before block subtraction in MuonShieldFactory::build() is
reached.

Comment thread subsystems/NeutrinoDetector/README.md Outdated
@matclim

matclim commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@olantwin I think we are good now

@olantwin

Copy link
Copy Markdown
Contributor

@olantwin I think we are good now

I'm review Monday. Could you please clean up the commit history?

@olantwin

Copy link
Copy Markdown
Contributor

For the config path resolution, please reuse the version we already have, we don't need to reimplement this for every detector.

Matei Climescu added 2 commits July 11, 2026 12:11
Replace the GDML-derived station stub with a configurable block list driven
by MS.toml (MuonShieldConfig / readMuonShieldConfig, toml++), mirroring the
calorimeter's calo.toml pattern. Each block is positioned by its upstream
face and may be rotated or tapered (GeoBox, or GeoTrd when tapered).

The shipped MS.toml reproduces the FairShip TRY_2026 magnets (FairShip
PR #1334) as a solid-block approximation: each magnet is one iron block
sized to its FairShip outer envelope, dropping the aperture, yoke shape and
field (field is handled elsewhere). The FairShip absorber is the magnetised
hadron stopper and stays in the Target subsystem.

Place the container at the envelope centre reported by the factory instead
of the hard-coded GDML z, and nest the neutrino detector inside the shield
container as an embedded daughter while keeping it an independent subsystem.

BREAKING CHANGE: MuonShieldFactory's public API and the muon-shield geometry
and placement have changed; the old station/piece structs are removed.
Define the muon shield and the neutrino detector independently and carve the
SND cavity out of the iron at build time. MuonShieldFactory::reserveSpace()
registers a box; build() subtracts it (GeoShapeSubtraction) from every magnet
it intersects and leaves the rest untouched, so which magnets are carved
follows from the reservation rather than a hard-coded list.

The reservation envelope is declared in NeutrinoDetector/SD.toml, parsed by
SNDEnvelope via the shared resolveConfigPath helper, and wired in by
SHiPGeometryBuilder before the shield is built.
@matclim

matclim commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Worth noting that commit 084c088 will likely not compile standalone as a result of the rebase. I'm not sure whether this is worth working around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants