Skip to content

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

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

feat(ms)!: Add configurable iron-block muon shield (FairShip TRY_2026)#53
matclim wants to merge 5 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 passive, config-driven muon shield defined by an MS.toml iron block list, with support for per-block rotation and optional taper.
    • Embedded the neutrino detector inside the muon-shield container, using SD.toml to reserve and carve out a clearance cavity to avoid overlaps.
    • Added install-time packaging and build-time path resolution for MS.toml/SD.toml.
  • Bug Fixes
    • Updated the geometry hierarchy and consistency checks so the neutrino detector is nested under the muon shield.
  • Documentation
    • Refreshed the MuonShield and NeutrinoDetector READMEs for the new config-driven, passive block model.
  • Tests
    • Expanded MuonShield regression/config tests to cover taper/rotation, carving behavior, embedded daughters, and TOML validation.

@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 geometry is now read from MS.toml, built as configurable passive blocks, and integrated with a TOML-defined NeutrinoDetector cavity and embedded daughter volume. Shared TOML infrastructure, tests, consistency checks, packaging, and documentation were updated.

Changes

Configured MuonShield geometry and nested detector

Layer / File(s) Summary
Shared TOML infrastructure
CMakeLists.txt, cmake/ShipAddTomlConfig.cmake, include/SHiPGeometry/TomlConfig.h, subsystems/Calorimeter/...
Shared CMake and parsing helpers provide TOML dependency wiring, staging, installation, numeric conversion, and fixed-size array validation.
MuonShield and SND configuration
subsystems/MuonShield/..., subsystems/NeutrinoDetector/...
TOML schemas and parsers define validated shield blocks, envelope geometry, taper, rotation, and SND reservation data.
Factory API and shield construction
subsystems/MuonShield/include/..., subsystems/MuonShield/src/MuonShieldFactory.cpp
MuonShieldFactory builds Air and iron geometry, applies transforms, subtracts reserved spaces, and embeds daughter volumes.
Detector hierarchy wiring
src/SHiPGeometry.cpp
The SND is embedded under the muon shield, while shield placement uses the computed center Z.
Packaging, regression coverage, and documentation
subsystems/*/CMakeLists.txt, subsystems/MuonShield/test_muonshield.cpp, tests/test_consistency.cpp, subsystems/*/README.md
Configuration packaging, geometry and parser tests, hierarchy expectations, and geometry-model documentation are updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant SHiPGeometryBuilder
  participant SNDEnvelope
  participant MuonShieldFactory
  participant NeutrinoDetector
  SHiPGeometryBuilder->>SNDEnvelope: read SD.toml
  SHiPGeometryBuilder->>MuonShieldFactory: reserveSpace(envelope)
  SHiPGeometryBuilder->>NeutrinoDetector: build detector
  SHiPGeometryBuilder->>MuonShieldFactory: embedDaughter(detector)
  MuonShieldFactory-->>SHiPGeometryBuilder: built shield and centreZ_mm()
Loading

Possibly related PRs

Suggested reviewers: gfrisella, olantwin

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% 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 is clear, specific, and matches the main change: a configurable iron-block muon shield based on FairShip TRY_2026.
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 thread subsystems/MuonShield/src/MuonShieldFactory.cpp Outdated
Comment thread subsystems/MuonShield/src/MuonShieldFactory.cpp
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.

@olantwin olantwin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly questions to understand better how everything works.

Comment thread src/SHiPGeometry.cpp
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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This duplicates the number from the SD toml. Does the embed create the gap in the iron?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, embedDaughter only nests the pre-built volume in the shield's tree; it doesn't touch the iron. The gap is carved by reserveSpace → GeoShapeSubtraction in build(). That call was missing, which is the bug in comment 2. Both the reservation and the embed now derive from the same SNDEnvelope read from SD.toml, so the 28.95 literal is gone and the duplicate world-placement is removed.

Comment thread src/SHiPGeometry.cpp
// 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
Contributor

Choose a reason for hiding this comment

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

I'd concur with coderabbit that it seems that the space is never reserved. Should the reserveSpace function be called first?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct, thank you, it wasn't being called at all. The builder now does reserveSpace(snd.centre_mm, snd.size_mm, snd.rotation_deg) before build(). OverlapCheck confirms the SND now sits in a carved cavity rather than solid iron.

{"/SHiP/muon_shield", 18310.0, 500.0},
// The neutrino detector is nested inside the muon shield (see
// MuonShieldFactory::embedDaughter), not a direct world child, so it is
// not among collectSubsystems(world).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we test this in another way? Maybe check the placement within the muon shield?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, added ConsistencyTest.NeutrinoDetectorNestedInMuonShield, which locates /SHiP/muon_shield among the world's children, asserts /SHiP/neutrino_detector is among its children, and checks it's not a direct world child. So it's a positive assertion of the nesting rather than an omission.

throw std::runtime_error("MuonShieldConfig: " + where +
" has a taper that collapses its downstream face");

// The block must sit within the envelope in Z (light sanity check;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we also check the envelope in x/y? For collisions with TCC8 this might be relevant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added. Each block's transverse extent (|start| + half-size, taper-aware) is now checked against envelope_half_x/y_mm and throws if it pokes out. Good point regarding TCC8.

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 &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The half-widths here are the unrotated ones, right? In that case, the actual bounding box might be different if rotations are included.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right. The intersection test now computes the block's true axis-aligned bounding box in the container frame by transforming its 8 corners through the placement transform, so it's correct under rotation. (The subtraction geometry itself was always exact — this only governs whether it's applied, so no change to the current geometry, but it's correct now for rotated blocks.)

// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again, we should probably also check x and y.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

embedDaughter takes only a Z centre, so daughters are placed on the beam axis (x = y = 0) by construction, the Z fit is the only bound to check here. I've documented that in the code. If we ever support off-axis daughters, a transverse-extent check against the envelope (from the daughter's bounding box) should be added, happy to do it then, or now if you'd prefer. I left it out to avoid special-casing GeoBox.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok, since it's documented we can leave it for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be good if we can enforce (by design ideally, otherwise via error messages/documentation) that the different operations happen in the right order.

E.g. reserving space or embedding daughter after build() has not effect, it should not fail silently.
Maybe we can read the config in the constructor so that certain variables can not be initialized? Or check that a config is loaded before certain operations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added an m_built guard: reserveSpace/embedDaughter after build(), and a second build(), now throw with a clear message instead of no-oping. On reading the config in the constructor: I like the direction, but it moves file I/O and its error handling into construction, which changes when/where failures surface; I'd prefer to keep the guard for now and do constructor-loading as a separate change if you feel it's worth it.

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 +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should include

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added (and for std::max, which was also being used transitively).

CHECK_THAT(ms->getXToChildVol(0).translation().z(),
Catch::Matchers::WithinAbs(blockCentreLocalZ, 1e-3));

// Magnet 6 (index 5) widens in Y → GeoTrd.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this still true?

@matclim matclim Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not anymore now that M6 is no longer tapered. It's been updated accordingly so the check looks for a regular GeoBox.

```text
muon_shield (Air envelope, 3520 × 2640 × 27540 mm) centre z = 18.31 m
├─ block_0 magnet 1 (Iron box)
├─ … magnets 2–5 (Iron boxes; magnet 6 is a GeoTrd, widens in Y)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is magnet 6 still tapered?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No longer FairShip #1342 (fix: SND magnet configuration) made magnet 6's upstream and downstream faces equal, so it's a plain box now, not a GeoTrd. Updated both the config and the assertion; taper so GeoTrd stays covered by MuonShieldTaperMakesTrd.

@matclim

matclim commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, comments 1 and 2 caught a real bug: reserveSpace() was never called, so no cavity was carved and the SND was overlapping solid iron. It is fixed, plus the hardening you flagged. All building with tests green (incl. OverlapCheck, which now genuinely exercises the carved cavity).

@olantwin

Copy link
Copy Markdown
Contributor

Thanks for the responses. I'd suggest adding additional commits now addressing them, we can then squash things later. Please leave the threads unresolved for now, I'll resolve them while I review the new version.

@matclim

matclim commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Done, I thought I had pushed beforehand but apparently not.

@olantwin

Copy link
Copy Markdown
Contributor

Thanks, I'll review ASAP.

@olantwin

Copy link
Copy Markdown
Contributor

While I review, could you please also ask coderabbit for a full review?

@matclim

matclim commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 59 minutes.

@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 (2)
subsystems/MuonShield/src/MuonShieldConfig.cpp (1)

174-207: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider validating that blocks don't overlap each other in Z.

Validation covers per-block bounds (size, taper, envelope Z/X/Y) but never checks that two blocks' Z ranges don't overlap. A future MS.toml edit that accidentally overlaps two blocks would only surface later via OverlapCheck/geometry tests instead of failing fast here, right next to the other geometric sanity checks.

♻️ Proposed addition (after the existing per-block validation loop)
+    // Blocks must not overlap each other in Z (unrotated check, same caveat as above).
+    for (std::size_t i = 0; i < cfg.blocks.size(); ++i) {
+        for (std::size_t j = i + 1; j < cfg.blocks.size(); ++j) {
+            const auto& a = cfg.blocks[i];
+            const auto& b = cfg.blocks[j];
+            const bool overlap = a.start[2] < b.start[2] + b.size[2] - kEps &&
+                                 b.start[2] < a.start[2] + a.size[2] - kEps;
+            if (overlap)
+                throw std::runtime_error("MuonShieldConfig: blocks " + std::to_string(i) + " and " +
+                                         std::to_string(j) + " overlap in Z in " + 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/MuonShieldConfig.cpp` around lines 174 - 207,
Extend the validation around the per-block checks to detect overlapping Z
intervals between distinct blocks. Compare each block’s upstream and downstream
Z bounds against every later block, and throw a descriptive runtime error
identifying both block indices when their ranges overlap, while allowing merely
touching boundaries.
subsystems/NeutrinoDetector/SD.toml (1)

14-15: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Confirm whether magnet 7 should remain uncarved before accepting this reservation envelope.

centre.z = 28950 with size.z = 5100 produces a reservation range of [26400, 31500], which overlaps magnet 6 by 1440 mm and magnet 7 by 3470 mm, but does not overlap magnet 5. If the intended SND cavity is still “magnets 5 and 6 only,” the reservation needs to move upstream rather than carve magnet 7.

🤖 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/SD.toml` around lines 14 - 15, Confirm the
intended SND cavity magnet coverage for the reservation envelope defined by
centre and size in SD.toml. If it must cover only magnets 5 and 6, adjust
centre.z and/or size.z so the resulting z-range excludes magnet 7 while
retaining the required magnet 5–6 coverage; otherwise explicitly preserve the
current envelope for magnet 7.
🤖 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/CMakeLists.txt`:
- Around line 32-38: Update MS_TOML_INSTALL_PATH in
subsystems/MuonShield/CMakeLists.txt (lines 32-38) to use
CMAKE_INSTALL_FULL_DATADIR. Apply the same change to SD_TOML_INSTALL_PATH in
subsystems/NeutrinoDetector/CMakeLists.txt (lines 38-45), preserving the
existing SHiPGeometry subdirectory and TOML filenames.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 127-160: Update the bounding-box validation around the block
construction to account for both rotation gaps. Compute each reservation’s
world-axis-aligned bounds from all corners transformed by its rotation before
the intersection test, and use those bounds instead of unrotated rhx/rhy/rhz in
the checks preceding GeoShapeSubtraction. Also validate bLoX..bHiZ against the
configured envelope half-extents and halfZ, rejecting any rotated block whose
AABB exceeds the Air container.

---

Nitpick comments:
In `@subsystems/MuonShield/src/MuonShieldConfig.cpp`:
- Around line 174-207: Extend the validation around the per-block checks to
detect overlapping Z intervals between distinct blocks. Compare each block’s
upstream and downstream Z bounds against every later block, and throw a
descriptive runtime error identifying both block indices when their ranges
overlap, while allowing merely touching boundaries.

In `@subsystems/NeutrinoDetector/SD.toml`:
- Around line 14-15: Confirm the intended SND cavity magnet coverage for the
reservation envelope defined by centre and size in SD.toml. If it must cover
only magnets 5 and 6, adjust centre.z and/or size.z so the resulting z-range
excludes magnet 7 while retaining the required magnet 5–6 coverage; otherwise
explicitly preserve the current envelope for magnet 7.
🪄 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 Plus

Run ID: 1ed3be33-b454-49d3-a20e-3faabddbbef8

📥 Commits

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

📒 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 subsystems/MuonShield/CMakeLists.txt Outdated
Comment thread subsystems/MuonShield/src/MuonShieldFactory.cpp
@olantwin

Copy link
Copy Markdown
Contributor

Actionable comments posted: 2
🧹 Nitpick comments (2)

subsystems/MuonShield/src/MuonShieldConfig.cpp (1)> 174-207: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider validating that blocks don't overlap each other in Z.
Validation covers per-block bounds (size, taper, envelope Z/X/Y) but never checks that two blocks' Z ranges don't overlap. A future MS.toml edit that accidentally overlaps two blocks would only surface later via OverlapCheck/geometry tests instead of failing fast here, right next to the other geometric sanity checks.

♻️ Proposed addition (after the existing per-block validation loop)

+    // Blocks must not overlap each other in Z (unrotated check, same caveat as above).
+    for (std::size_t i = 0; i < cfg.blocks.size(); ++i) {
+        for (std::size_t j = i + 1; j < cfg.blocks.size(); ++j) {
+            const auto& a = cfg.blocks[i];
+            const auto& b = cfg.blocks[j];
+            const bool overlap = a.start[2] < b.start[2] + b.size[2] - kEps &&
+                                 b.start[2] < a.start[2] + a.size[2] - kEps;
+            if (overlap)
+                throw std::runtime_error("MuonShieldConfig: blocks " + std::to_string(i) + " and " +
+                                         std::to_string(j) + " overlap in Z in " + 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/MuonShieldConfig.cpp` around lines 174 - 207,
Extend the validation around the per-block checks to detect overlapping Z
intervals between distinct blocks. Compare each block’s upstream and downstream
Z bounds against every later block, and throw a descriptive runtime error
identifying both block indices when their ranges overlap, while allowing merely
touching boundaries.

subsystems/NeutrinoDetector/SD.toml (1)> 14-15: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Confirm whether magnet 7 should remain uncarved before accepting this reservation envelope.
centre.z = 28950 with size.z = 5100 produces a reservation range of [26400, 31500], which overlaps magnet 6 by 1440 mm and magnet 7 by 3470 mm, but does not overlap magnet 5. If the intended SND cavity is still “magnets 5 and 6 only,” the reservation needs to move upstream rather than carve magnet 7.

🤖 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/SD.toml` around lines 14 - 15, Confirm the
intended SND cavity magnet coverage for the reservation envelope defined by
centre and size in SD.toml. If it must cover only magnets 5 and 6, adjust
centre.z and/or size.z so the resulting z-range excludes magnet 7 while
retaining the required magnet 5–6 coverage; otherwise explicitly preserve the
current envelope for magnet 7.

🤖 Prompt for all review comments with AI agents

🪄 Autofix (Beta)

ℹ️ Review info

@matclim these two nitpicks seem worth addressing.

@matclim

matclim commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Update underway, have to finish with another PR first

@matclim

matclim commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 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: 1

🧹 Nitpick comments (8)
subsystems/MuonShield/src/MuonShieldConfig.cpp (2)

174-207: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Blocks aren't checked for mutual Z overlap.

Only the envelope bounds are validated per block; nothing detects two blocks whose [start_z, start_z+size_z] ranges overlap. This is the outstanding nitpick called out in the latest review (per PR objectives) and isn't addressed in this version.

🛡️ Suggested overlap check (after the per-block loop)
     }
+
+    // Distinct blocks must not overlap in Z (order-independent check).
+    std::vector<std::size_t> order(cfg.blocks.size());
+    for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
+    std::sort(order.begin(), order.end(),
+              [&](std::size_t a, std::size_t b) { return cfg.blocks[a].start[2] < cfg.blocks[b].start[2]; });
+    for (std::size_t k = 1; k < order.size(); ++k) {
+        const auto& prev = cfg.blocks[order[k - 1]];
+        const auto& cur = cfg.blocks[order[k]];
+        if (prev.start[2] + prev.size[2] > cur.start[2] + kEps)
+            throw std::runtime_error("MuonShieldConfig: block " + std::to_string(order[k - 1]) +
+                                     " overlaps block " + std::to_string(order[k]) + " in Z in " + path);
+    }

     return cfg;
🤖 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 174 - 207, Add
mutual Z-overlap validation after the per-block checks in the configuration
validation routine. Compare each pair of blocks using their [start[2], start[2]
+ size[2]] intervals, reject any overlapping ranges with a runtime_error
identifying both block indices, and allow merely touching endpoints.

119-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Unknown-key warning only covers top-level keys, not per-block keys.

Typos inside [[block]] tables (e.g. rotaton instead of rotation) are silently ignored — the intended "typo? stale field?" protection doesn't reach the part of the schema most likely to be hand-edited per magnet.

🔍 Suggested extension
+static constexpr std::array kKnownBlockKeys = {"rotation"sv, "size"sv, "start"sv, "taper"sv};
+
 // First pass: warn about unknown keys.
 for (const auto& [k, _] : table) {
     if (!std::ranges::binary_search(kKnownKeys, std::string_view{k})) {
         std::cerr << "MuonShieldConfig: warning: unknown key '" << k << "' in " << path
                   << " (typo? stale field? — value will be ignored)\n";
     }
 }
+// ... inside the block loop, after `const toml::table* bt = elem.as_table();`
+for (const auto& [k, _] : *bt) {
+    if (!std::ranges::binary_search(kKnownBlockKeys, std::string_view{k}))
+        std::cerr << "MuonShieldConfig: warning: unknown block key '" << k << "' in " << path
+                  << "\n";
+}
🤖 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 119 - 158,
Extend unknown-key validation to each [[block]] table in the block-parsing loop,
using the block schema keys start, size, rotation, and taper. Emit the same
warning format for unrecognized per-block keys before reading the known fields,
while preserving the existing parsing and validation behavior.
subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h (1)

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

Docstring is out of date vs. the validation actually implemented.

readMuonShieldConfig()'s @throws doc only mentions non-positive/inverted Z envelope, non-positive block size, taper collapse, and upstream-face-out-of-envelope. MuonShieldConfig.cpp also throws for the downstream face extending past the envelope end and for blocks extending beyond the envelope in x/y — neither is mentioned here.

📝 Proposed doc update
  * `@throws` std::runtime_error if the file cannot be opened, contains malformed
  *         TOML, has a non-positive or inverted Z envelope, or a block with a
  *         non-positive size, a taper that collapses the far face, or an
- *         upstream face outside the envelope in Z.
+ *         upstream or downstream face outside the envelope in Z, or a
+ *         transverse (x/y) extent exceeding the envelope half-widths.
  */
🤖 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 76
- 88, Update the `@throws` documentation for readMuonShieldConfig to include
validation failures when a downstream face exceeds the envelope end and when a
block extends beyond the envelope in x or y. Keep the existing documented
failure cases unchanged and do not modify implementation behavior.
subsystems/NeutrinoDetector/CMakeLists.txt (1)

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

Same TOML staging/install/compile-definitions boilerplate duplicated from MuonShield's CMakeLists.txt.

See the consolidated comment for a suggested shared-function extraction covering both files (and the existing Calorimeter copy).

🤖 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/CMakeLists.txt` around lines 11 - 45, Extract the
duplicated TOML dependency, SD.toml staging/install, and resolveConfigPath()
compile-definition setup from this CMakeLists.txt and MuonShield’s
CMakeLists.txt into the shared CMake helper proposed by the consolidated review
comment. Replace the local boilerplate with the helper invocation, preserving
the existing target, source file, install destination, and generated-path
behavior; also reuse that helper for the existing Calorimeter equivalent.
subsystems/MuonShield/CMakeLists.txt (1)

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

TOML staging/install/compile-definitions boilerplate is duplicated across subsystems.

This block (find_package, private BUILD_INTERFACE link, dual configure_file staging, install, and fallback target_compile_definitions) is essentially copy-pasted in subsystems/NeutrinoDetector/CMakeLists.txt (and, per the comment on line 8, already exists a third time in Calorimeter's CMakeLists.txt). The earlier CMAKE_INSTALL_FULL_DATADIR fix had to be applied to two files separately, which is direct evidence of the duplication cost. See the consolidated comment below for a suggested shared-function extraction.

🤖 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/CMakeLists.txt` around lines 10 - 43, Extract the
repeated TOML dependency, staging, installation, and fallback-definition setup
from the MuonShield CMake configuration into the shared helper suggested by the
consolidated review comment. Update MuonShield to invoke that helper with its
target and MS.toml source, preserving the existing private tomlplusplus link,
both staging destinations, install destination, and
MS_TOML_DEFAULT_PATH/MS_TOML_INSTALL_PATH definitions; apply the same helper to
the corresponding NeutrinoDetector and Calorimeter configurations.
subsystems/NeutrinoDetector/src/SNDEnvelope.cpp (1)

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

Duplicate TOML vec3-parsing helpers vs. MuonShieldConfig.cpp.

toNumber/readVec3 reimplement the same required/fallback array-parsing pattern as nodeToNumber/readFixedArray in MuonShieldConfig.cpp (the code comment on Line 77 even says "like readMuonShieldConfig"). Consider factoring this into a shared TOML helper (e.g. a small header under include/SHiPGeometry/) used by both MuonShieldConfig.cpp and this file, so future validation changes don't need to be kept in sync manually in two places.

🤖 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 29 - 56, Factor
the duplicated TOML numeric and fixed-length array parsing from
toNumber/readVec3 and MuonShieldConfig.cpp into a shared helper under
include/SHiPGeometry/. Update both callers to use the shared helper while
preserving required-field handling, fallbacks, three-element validation, numeric
conversion, and existing error behavior.
subsystems/MuonShield/src/MuonShieldFactory.cpp (1)

92-150: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No validation that distinct blocks don't overlap each other in Z.

The block loop validates a single block's rotation-aware AABB against the envelope (Lines 142-150), but there's still no check that two different blocks in cfg.blocks don't overlap each other in Z. Per the PR discussion, this was flagged as an outstanding nitpick and is still pending. Since bLoZ/bHiZ are already computed per block here, tracking previously-seen Z ranges and comparing would be a fairly cheap addition (either here or in readMuonShieldConfig's existing validation pass).

🤖 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 92 - 150, The
block loop validates each block against the envelope but does not reject
overlaps between distinct blocks along Z. Track each computed block Z interval
from bLoZ and bHiZ, compare it with previously accepted intervals, and throw a
configuration error when intervals overlap; retain the existing envelope
validation and allow merely touching boundaries.
subsystems/MuonShield/test_muonshield.cpp (1)

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

Missing regression coverage for the rotation-aware AABB fixes.

No test exercises the two rotation-aware checks added in MuonShieldFactory::build(): (a) a rotated block whose AABB exceeds the envelope should throw, and (b) a rotated reserveSpace(...) box correctly intersects/carves a block. Both paths were previously buggy (per past review) before being fixed; a couple of small writeTempToml-based cases mirroring MuonShieldTaperMakesTrd/MuonShieldReservationCarvesIron would guard against regressions here.

🤖 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 96 - 138, Add
regression tests in the MuonShield test suite covering both rotation-aware
checks in MuonShieldFactory::build(): verify a rotated block whose computed AABB
exceeds the envelope throws, and verify a rotated reserveSpace(...) box
intersects and carves the target block as expected. Use small
writeTempToml-based cases consistent with MuonShieldTaperMakesTrd and
MuonShieldReservationCarvesIron, asserting the exception and resulting geometry
respectively.
🤖 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 116-125: Clarify the README’s description of the shipped default
geometry to state that it is a FairShip outer-envelope approximation, not the
finalized engineering layout. Update the status/TODO wording around the default
blocks to consistently reflect this approximation and their pending replacement
with the coordinator-supplied layout.

---

Nitpick comments:
In `@subsystems/MuonShield/CMakeLists.txt`:
- Around line 10-43: Extract the repeated TOML dependency, staging,
installation, and fallback-definition setup from the MuonShield CMake
configuration into the shared helper suggested by the consolidated review
comment. Update MuonShield to invoke that helper with its target and MS.toml
source, preserving the existing private tomlplusplus link, both staging
destinations, install destination, and MS_TOML_DEFAULT_PATH/MS_TOML_INSTALL_PATH
definitions; apply the same helper to the corresponding NeutrinoDetector and
Calorimeter configurations.

In `@subsystems/MuonShield/include/MuonShield/MuonShieldConfig.h`:
- Around line 76-88: Update the `@throws` documentation for readMuonShieldConfig
to include validation failures when a downstream face exceeds the envelope end
and when a block extends beyond the envelope in x or y. Keep the existing
documented failure cases unchanged and do not modify implementation behavior.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp`:
- Around line 174-207: Add mutual Z-overlap validation after the per-block
checks in the configuration validation routine. Compare each pair of blocks
using their [start[2], start[2] + size[2]] intervals, reject any overlapping
ranges with a runtime_error identifying both block indices, and allow merely
touching endpoints.
- Around line 119-158: Extend unknown-key validation to each [[block]] table in
the block-parsing loop, using the block schema keys start, size, rotation, and
taper. Emit the same warning format for unrecognized per-block keys before
reading the known fields, while preserving the existing parsing and validation
behavior.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 92-150: The block loop validates each block against the envelope
but does not reject overlaps between distinct blocks along Z. Track each
computed block Z interval from bLoZ and bHiZ, compare it with previously
accepted intervals, and throw a configuration error when intervals overlap;
retain the existing envelope validation and allow merely touching boundaries.

In `@subsystems/MuonShield/test_muonshield.cpp`:
- Around line 96-138: Add regression tests in the MuonShield test suite covering
both rotation-aware checks in MuonShieldFactory::build(): verify a rotated block
whose computed AABB exceeds the envelope throws, and verify a rotated
reserveSpace(...) box intersects and carves the target block as expected. Use
small writeTempToml-based cases consistent with MuonShieldTaperMakesTrd and
MuonShieldReservationCarvesIron, asserting the exception and resulting geometry
respectively.

In `@subsystems/NeutrinoDetector/CMakeLists.txt`:
- Around line 11-45: Extract the duplicated TOML dependency, SD.toml
staging/install, and resolveConfigPath() compile-definition setup from this
CMakeLists.txt and MuonShield’s CMakeLists.txt into the shared CMake helper
proposed by the consolidated review comment. Replace the local boilerplate with
the helper invocation, preserving the existing target, source file, install
destination, and generated-path behavior; also reuse that helper for the
existing Calorimeter equivalent.

In `@subsystems/NeutrinoDetector/src/SNDEnvelope.cpp`:
- Around line 29-56: Factor the duplicated TOML numeric and fixed-length array
parsing from toNumber/readVec3 and MuonShieldConfig.cpp into a shared helper
under include/SHiPGeometry/. Update both callers to use the shared helper while
preserving required-field handling, fallbacks, three-element validation, numeric
conversion, and existing error behavior.
🪄 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 Plus

Run ID: 8d9e83cc-eaaa-46f9-90b1-6fe59ac7934e

📥 Commits

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

📒 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 subsystems/MuonShield/README.md
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 37 minutes.

Add cmake/ShipAddTomlConfig.cmake (toml++ link, staging, install, and
*_TOML_DEFAULT_PATH/*_TOML_INSTALL_PATH defines) and
include/SHiPGeometry/TomlConfig.h (shared asDouble + readNumericArray).
MuonShield, NeutrinoDetector, and Calorimeter now call these instead of
each carrying its own copy of the CMake boilerplate and TOML numeric /
fixed-array parsing.

Error-message wording for array parsing is now uniform across subsystems;
the throw conditions are unchanged.
Add cmake/ShipAddTomlConfig.cmake (toml++ link, staging, install, and
*_TOML_DEFAULT_PATH/*_TOML_INSTALL_PATH defines) and
include/SHiPGeometry/TomlConfig.h (shared asDouble + readNumericArray).
MuonShield, NeutrinoDetector, and Calorimeter now call these instead of
each carrying its own copy of the CMake boilerplate and TOML numeric /
fixed-array parsing.

Error-message wording for array parsing is now uniform across subsystems;
the throw conditions are unchanged.
@matclim

matclim commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 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

♻️ Duplicate comments (2)
src/SHiPGeometry.cpp (1)

58-62: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Register the SD.toml reservation before building the shield. The builder embeds the SND but never calls reserveSpace(), leaving m_reservations empty and the intersecting iron solid. Read the SND envelope once and use its centre, size, and rotation for both reservation and embedding.

  • src/SHiPGeometry.cpp#L58-L62: call reserveSpace(...) from the parsed SND envelope before MuonShieldFactory::build().
  • subsystems/MuonShield/README.md#L24-L42: retain the carving description only after the builder wiring exists.
  • subsystems/NeutrinoDetector/README.md#L51-L59: retain the reserveSpace claim only after the builder wiring exists.
🤖 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 58 - 62, Update src/SHiPGeometry.cpp lines
58-62 to parse the SND envelope once, then pass its centre, size, and rotation
to reserveSpace(...) before MuonShieldFactory::build(), reusing the same values
for embedDaughter(...). Retain the carving description in
subsystems/MuonShield/README.md lines 24-42 and the reserveSpace claim in
subsystems/NeutrinoDetector/README.md lines 51-59 only after the builder wiring
is implemented.
subsystems/MuonShield/src/MuonShieldFactory.cpp (1)

110-131: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use transformed AABBs for the carve decision.

The intersection check uses unrotated block and reservation half-extents, even though both support rotation. A rotated reservation or block can be classified as non-intersecting, skipping the subtraction and leaving iron in the intended cavity. Compute each transformed box’s world AABB from its corners; also reject a rotated block whose AABB exceeds the Air envelope.

🤖 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 110 - 131,
Update the carve-intersection logic around the reservation loop to compute
world-axis-aligned bounding boxes from all eight transformed corners for both
the block and each reservation, including their rotations. Use those AABBs for
overlap testing, and reject rotated blocks whose transformed AABB exceeds the
Air envelope before applying subtraction; preserve the existing transformed
reservation subtraction via resInBlock.
🧹 Nitpick comments (2)
subsystems/MuonShield/test_muonshield.cpp (1)

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

Assert that magnet 6 is carved too.

The SND reservation spans magnets 6 and 7, but this test only verifies the last magnet’s subtraction shape. Assert getChildVol(5) is also a GeoShapeSubtraction so a partial cavity regression is caught.

Proposed test addition
     CHECK(dynamic_cast<const GeoBox*>(ms->getChildVol(0)->getLogVol()->getShape()) != nullptr);
+    CHECK(dynamic_cast<const GeoShapeSubtraction*>(
+              ms->getChildVol(5)->getLogVol()->getShape()) != nullptr);
     // The last magnet intersects the reservation → its shape is a subtraction.
     CHECK(dynamic_cast<const GeoShapeSubtraction*>(ms->getChildVol(6)->getLogVol()->getShape()) !=
           nullptr);
🤖 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 89 - 93, Extend the
test assertions after the existing magnet 1 check to verify that
ms->getChildVol(5) has a GeoShapeSubtraction shape, while preserving the current
assertion for ms->getChildVol(6). This ensures both reserved magnets are
confirmed as carved.
subsystems/MuonShield/src/MuonShieldFactory.cpp (1)

81-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject overlapping block Z-ranges before placing siblings.

The current validation only checks each block against the envelope; it never compares blocks with each other. A TOML typo can therefore create overlapping iron children. Validate pairwise Z intervals (using transformed bounds when rotation is enabled) and add a rejection test.

🤖 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` at line 81, Extend
validation in MuonShieldFactory’s block-processing flow to compare every pair of
sibling block Z-intervals before placement, using transformed bounds when
rotation is enabled. Reject configurations with any overlap while preserving the
existing envelope validation, and add a test covering overlapping block ranges.
🤖 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 29-33: Update the magnet-6 description in
subsystems/MuonShield/README.md lines 29-33 to identify it as an iron box rather
than a widening GeoTrd, and update the corresponding test comment in
subsystems/MuonShield/test_muonshield.cpp lines 74-75 to match the GeoBox
assertion; no code behavior changes are needed.

In `@subsystems/MuonShield/src/MuonShieldConfig.cpp`:
- Around line 139-161: Add the missing transverse envelope validation inside the
block loop alongside the existing Z checks. In the validation flow using
b.start, b.size, and taper-aware downstream dimensions, compute each block’s
maximum absolute x/y extent across its tapered span and reject blocks exceeding
envelope_half_x_mm or envelope_half_y_mm, including the block location and
offending extent in the runtime error.
- Around line 139-161: Validate distinct block Z intervals after the per-block
checks in the configuration validation flow, using each block’s start[2] and
size[2]. Detect any overlapping intervals, including unsorted blocks, and throw
a runtime_error identifying both conflicting block indices and the configuration
path; retain the existing envelope and taper validation unchanged.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 44-52: Restore the m_built guard in
MuonShieldFactory::embedDaughter and reserveSpace so they throw before accepting
registrations after geometry has been built. Add the same precondition to
build() to reject repeated builds, preserving the existing hierarchy creation
only for the first build.

---

Duplicate comments:
In `@src/SHiPGeometry.cpp`:
- Around line 58-62: Update src/SHiPGeometry.cpp lines 58-62 to parse the SND
envelope once, then pass its centre, size, and rotation to reserveSpace(...)
before MuonShieldFactory::build(), reusing the same values for
embedDaughter(...). Retain the carving description in
subsystems/MuonShield/README.md lines 24-42 and the reserveSpace claim in
subsystems/NeutrinoDetector/README.md lines 51-59 only after the builder wiring
is implemented.

In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Around line 110-131: Update the carve-intersection logic around the
reservation loop to compute world-axis-aligned bounding boxes from all eight
transformed corners for both the block and each reservation, including their
rotations. Use those AABBs for overlap testing, and reject rotated blocks whose
transformed AABB exceeds the Air envelope before applying subtraction; preserve
the existing transformed reservation subtraction via resInBlock.

---

Nitpick comments:
In `@subsystems/MuonShield/src/MuonShieldFactory.cpp`:
- Line 81: Extend validation in MuonShieldFactory’s block-processing flow to
compare every pair of sibling block Z-intervals before placement, using
transformed bounds when rotation is enabled. Reject configurations with any
overlap while preserving the existing envelope validation, and add a test
covering overlapping block ranges.

In `@subsystems/MuonShield/test_muonshield.cpp`:
- Around line 89-93: Extend the test assertions after the existing magnet 1
check to verify that ms->getChildVol(5) has a GeoShapeSubtraction shape, while
preserving the current assertion for ms->getChildVol(6). This ensures both
reserved magnets are confirmed as carved.
🪄 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 Plus

Run ID: fb64bba5-f18d-47a4-9d56-1c090f759f94

📥 Commits

Reviewing files that changed from the base of the PR and between 626e0bb and 4145f54.

📒 Files selected for processing (20)
  • CMakeLists.txt
  • cmake/ShipAddTomlConfig.cmake
  • include/SHiPGeometry/TomlConfig.h
  • src/SHiPGeometry.cpp
  • subsystems/Calorimeter/CMakeLists.txt
  • subsystems/Calorimeter/src/CalorimeterConfig.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 on lines +29 to +33
muon_shield (Air envelope, 3520 × 2640 × 27540 mm) centre z = 18.31 m
├─ block_0 magnet 1 (Iron box)
├─ … magnets 2–5 (Iron boxes; magnet 6 is a GeoTrd, widens in Y)
├─ block_5 magnet 6 (Iron − SND box) ┐ carved by A − B
├─ block_6 magnet 7 (Iron − SND box) ┘ (magnets the reservation intersects)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the stale magnet-6 shape description. Magnet 6 is asserted as a GeoBox, but both comments still describe it as a widening GeoTrd.

  • subsystems/MuonShield/README.md#L29-L33: describe magnet 6 as an iron box.
  • subsystems/MuonShield/test_muonshield.cpp#L74-L75: update the stale test comment to match the GeoBox assertion.
📍 Affects 2 files
  • subsystems/MuonShield/README.md#L29-L33 (this comment)
  • subsystems/MuonShield/test_muonshield.cpp#L74-L75
🤖 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/README.md` around lines 29 - 33, Update the magnet-6
description in subsystems/MuonShield/README.md lines 29-33 to identify it as an
iron box rather than a widening GeoTrd, and update the corresponding test
comment in subsystems/MuonShield/test_muonshield.cpp lines 74-75 to match the
GeoBox assertion; no code behavior changes are needed.

Comment on lines +139 to +161
for (std::size_t i = 0; i < cfg.blocks.size(); ++i) {
const auto& b = cfg.blocks[i];
const std::string where = "block " + std::to_string(i) + " in " + path;
if (b.size[0] <= 0.0 || b.size[1] <= 0.0 || b.size[2] <= 0.0)
throw std::runtime_error("MuonShieldConfig: " + where + " has a non-positive size");

// Taper must not collapse (or invert) the downstream face.
const double farHalfX = 0.5 * b.size[0] + b.size[2] * std::tan(b.taper_deg[0] * kDegToRad);
const double farHalfY = 0.5 * b.size[1] + b.size[2] * std::tan(b.taper_deg[1] * kDegToRad);
if (farHalfX <= 0.0 || farHalfY <= 0.0)
throw std::runtime_error("MuonShieldConfig: " + where +
" has a taper that collapses its downstream face");

// The block must sit within the envelope in Z (light sanity check;
// rotations are not accounted for here).
if (b.start[2] < envStartMm - kEps || b.start[2] > envEndMm + kEps)
throw std::runtime_error("MuonShieldConfig: " + where + " has its upstream face (z = " +
std::to_string(b.start[2]) + " mm) outside the envelope");
if (b.start[2] + b.size[2] > envEndMm + kEps)
throw std::runtime_error(
"MuonShieldConfig: " + where + " has its downstream face (z = " +
std::to_string(b.start[2] + b.size[2]) + " mm) beyond the envelope end");
}

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

Transverse (x/y) envelope check for blocks is missing — regression of a previously agreed fix.

A past review thread on this file requested this exact check ("Can we also check the envelope in x/y? For collisions with TCC8 this might be relevant.") and the author replied it had been added ("Each block's transverse extent (|start| + half-size, taper-aware) is now checked against envelope_half_x/y_mm..."). That check is absent from the current validation loop — only size positivity, taper-collapse, and Z-extent are validated. Cross-checked MuonShieldFactory.cpp's block-placement code too; no equivalent check exists there either. With MS.toml's magnet 7 already within ~50 mm of the x/y envelope limits, future edits could silently produce a magnet that pokes outside the Air container without any error, risking undetected TCC8 collisions.

🛡️ Proposed fix
         // Taper must not collapse (or invert) the downstream face.
         const double farHalfX = 0.5 * b.size[0] + b.size[2] * std::tan(b.taper_deg[0] * kDegToRad);
         const double farHalfY = 0.5 * b.size[1] + b.size[2] * std::tan(b.taper_deg[1] * kDegToRad);
         if (farHalfX <= 0.0 || farHalfY <= 0.0)
             throw std::runtime_error("MuonShieldConfig: " + where +
                                      " has a taper that collapses its downstream face");
+
+        // Transverse extent must stay within the envelope (taper-aware).
+        const double maxHalfX = std::max(0.5 * b.size[0], farHalfX);
+        const double maxHalfY = std::max(0.5 * b.size[1], farHalfY);
+        if (std::abs(b.start[0]) + maxHalfX > cfg.envelope_half_x_mm + kEps ||
+            std::abs(b.start[1]) + maxHalfY > cfg.envelope_half_y_mm + kEps)
+            throw std::runtime_error("MuonShieldConfig: " + where +
+                                     " extends beyond the envelope in x/y");
🤖 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 139 - 161, Add
the missing transverse envelope validation inside the block loop alongside the
existing Z checks. In the validation flow using b.start, b.size, and taper-aware
downstream dimensions, compute each block’s maximum absolute x/y extent across
its tapered span and reject blocks exceeding envelope_half_x_mm or
envelope_half_y_mm, including the block location and offending extent in the
runtime error.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distinct blocks are not checked for Z overlap.

This matches the open nitpick from the latest PR review ("validate that distinct blocks do not overlap in Z"). Blocks are listed sequentially in MS.toml but nothing enforces non-overlap; a future edit that shifts a block's start/size could silently create overlapping iron solids (GeoModel overlap), since only per-block Z-in-envelope bounds are checked here, not block-to-block relationships.

🛡️ Proposed fix (append after the per-block loop)
+    // Distinct blocks must not overlap in Z (order-independent).
+    std::vector<std::size_t> order(cfg.blocks.size());
+    for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
+    std::sort(order.begin(), order.end(), [&](std::size_t a, std::size_t b) {
+        return cfg.blocks[a].start[2] < cfg.blocks[b].start[2];
+    });
+    for (std::size_t k = 1; k < order.size(); ++k) {
+        const auto& prev = cfg.blocks[order[k - 1]];
+        const auto& cur = cfg.blocks[order[k]];
+        if (prev.start[2] + prev.size[2] > cur.start[2] + kEps)
+            throw std::runtime_error("MuonShieldConfig: blocks " + std::to_string(order[k - 1]) +
+                                     " and " + std::to_string(order[k]) + " overlap in z in " + path);
+    }
+
     return cfg;
📝 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
for (std::size_t i = 0; i < cfg.blocks.size(); ++i) {
const auto& b = cfg.blocks[i];
const std::string where = "block " + std::to_string(i) + " in " + path;
if (b.size[0] <= 0.0 || b.size[1] <= 0.0 || b.size[2] <= 0.0)
throw std::runtime_error("MuonShieldConfig: " + where + " has a non-positive size");
// Taper must not collapse (or invert) the downstream face.
const double farHalfX = 0.5 * b.size[0] + b.size[2] * std::tan(b.taper_deg[0] * kDegToRad);
const double farHalfY = 0.5 * b.size[1] + b.size[2] * std::tan(b.taper_deg[1] * kDegToRad);
if (farHalfX <= 0.0 || farHalfY <= 0.0)
throw std::runtime_error("MuonShieldConfig: " + where +
" has a taper that collapses its downstream face");
// The block must sit within the envelope in Z (light sanity check;
// rotations are not accounted for here).
if (b.start[2] < envStartMm - kEps || b.start[2] > envEndMm + kEps)
throw std::runtime_error("MuonShieldConfig: " + where + " has its upstream face (z = " +
std::to_string(b.start[2]) + " mm) outside the envelope");
if (b.start[2] + b.size[2] > envEndMm + kEps)
throw std::runtime_error(
"MuonShieldConfig: " + where + " has its downstream face (z = " +
std::to_string(b.start[2] + b.size[2]) + " mm) beyond the envelope end");
}
for (std::size_t i = 0; i < cfg.blocks.size(); ++i) {
const auto& b = cfg.blocks[i];
const std::string where = "block " + std::to_string(i) + " in " + path;
if (b.size[0] <= 0.0 || b.size[1] <= 0.0 || b.size[2] <= 0.0)
throw std::runtime_error("MuonShieldConfig: " + where + " has a non-positive size");
// Taper must not collapse (or invert) the downstream face.
const double farHalfX = 0.5 * b.size[0] + b.size[2] * std::tan(b.taper_deg[0] * kDegToRad);
const double farHalfY = 0.5 * b.size[1] + b.size[2] * std::tan(b.taper_deg[1] * kDegToRad);
if (farHalfX <= 0.0 || farHalfY <= 0.0)
throw std::runtime_error("MuonShieldConfig: " + where +
" has a taper that collapses its downstream face");
// The block must sit within the envelope in Z (light sanity check;
// rotations are not accounted for here).
if (b.start[2] < envStartMm - kEps || b.start[2] > envEndMm + kEps)
throw std::runtime_error("MuonShieldConfig: " + where + " has its upstream face (z = " +
std::to_string(b.start[2]) + " mm) outside the envelope");
if (b.start[2] + b.size[2] > envEndMm + kEps)
throw std::runtime_error(
"MuonShieldConfig: " + where + " has its downstream face (z = " +
std::to_string(b.start[2] + b.size[2]) + " mm) beyond the envelope end");
}
// Distinct blocks must not overlap in Z (order-independent).
std::vector<std::size_t> order(cfg.blocks.size());
for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
std::sort(order.begin(), order.end(), [&](std::size_t a, std::size_t b) {
return cfg.blocks[a].start[2] < cfg.blocks[b].start[2];
});
for (std::size_t k = 1; k < order.size(); ++k) {
const auto& prev = cfg.blocks[order[k - 1]];
const auto& cur = cfg.blocks[order[k]];
if (prev.start[2] + prev.size[2] > cur.start[2] + kEps)
throw std::runtime_error("MuonShieldConfig: blocks " + std::to_string(order[k - 1]) +
" and " + std::to_string(order[k]) + " overlap in z in " + path);
}
return cfg;
🤖 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 139 - 161,
Validate distinct block Z intervals after the per-block checks in the
configuration validation flow, using each block’s start[2] and size[2]. Detect
any overlapping intervals, including unsorted blocks, and throw a runtime_error
identifying both conflicting block indices and the configuration path; retain
the existing envelope and taper validation unchanged.

Comment on lines +44 to +52
void MuonShieldFactory::embedDaughter(GeoPhysVol* daughter, double worldCentreZ_mm,
const std::string& name) {
m_daughters.push_back({daughter, worldCentreZ_mm, name});
}

return stationPhys;
void MuonShieldFactory::reserveSpace(const std::array<double, 3>& worldCentre_mm,
const std::array<double, 3>& size_mm,
const std::array<double, 3>& rotation_deg) {
m_reservations.push_back({worldCentre_mm, size_mm, rotation_deg});

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 | 🟡 Minor | ⚡ Quick win

Reject registrations after build and repeated builds.

These methods still accept mutations after the returned geometry is fixed, so those registrations are silently ignored; a second build() also recreates the hierarchy. Restore an m_built guard that throws before post-build mutations and repeat builds.

Also applies to: 61-62

🤖 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 44 - 52,
Restore the m_built guard in MuonShieldFactory::embedDaughter and reserveSpace
so they throw before accepting registrations after geometry has been built. Add
the same precondition to build() to reject repeated builds, preserving the
existing hierarchy creation only for the first build.

* @param size_mm Full box dimensions {x, y, z} (mm).
* @param rotation_deg Optional extrinsic X->Y->Z rotation about the centre.
*/
void reserveSpace(const std::array<double, 3>& worldCentre_mm,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems like reserveSpace is now only used in the tests. Is this intentional? Should it not be called before embedDaughter?

return resolveConfigPath(path, SD_TOML_DEFAULT_PATH, SD_TOML_INSTALL_PATH);
}

SNDEnvelope readSNDEnvelope(const std::string& rawPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is never actually called. Is this intentional?

Comment on lines +13 to +15
# The FairShip absorber (row 0) is the magnetised hadron stopper and lives in
# the Target subsystem (2.14-4.44 m); the shield front is at 2.14 m, so magnet 1
# begins at 4.59 m.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not true yet. We'd need to add the hadron absorber there.

Comment thread src/SHiPGeometry.cpp
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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we take this number from the config instead of hardcoding it?

It's also z-only. The reservation (if it's called, see other comment) supports 3d-positioning and rotation, which could cause a mismatch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we please not prefix files and classes with Ship?

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) *

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the same as in lines 125ff. Is there no 3D rotation in GeoModel? If not, maybe we can have a common helper in this file?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we move the known keys check and readNumeric here as well, to remove further duplication?


// Absolute fallback path baked in by CMake so out-of-source builds always find
// MS.toml even when the CWD doesn't contain a copy of it.
#ifndef MS_TOML_DEFAULT_PATH

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think now with the ship_add_toml_config CMake helper, this is no longer necessary, as it's always defined.

Comment on lines +19 to +25
#ifndef SD_TOML_DEFAULT_PATH
#define SD_TOML_DEFAULT_PATH "SD.toml"
#endif
#ifndef SD_TOML_INSTALL_PATH
#define SD_TOML_INSTALL_PATH ""
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think now with the ship_add_toml_config CMake helper, this is no longer necessary, as it's always defined.

Comment on lines +21 to +22
envelope_z_start_m = 4.5400
envelope_z_end_m = 32.0800

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since all other values are in mm, maybe we can just make these mm as well to avoid mistakes? This would also make a few unit conversions in MuonShieldConfig unnecessary.

Comment on lines +19 to +20
std::array<double, 3> centre_mm = {0.0, 0.0, 28950.0}; ///< world centre (mm)
std::array<double, 3> size_mm = {800.0, 800.0, 5100.0}; ///< full x, y, z (mm)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These seem unnecessary, as they are required in the SD.toml config.

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