Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions subsystems/DecayVolume/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ at the exit) made of two parts:
- **LAB liquid-scintillator sensor cells** in thin aluminium containers,
tiling the four faces of the frustum.

The decay region itself is a central **helium frustum**. It is sized strictly
inside the innermost sensor faces (`x_half − container_thickness`,
`y_half − container_thickness`) minus a clearance, so it cannot overlap the
structure or the sensors.
The decay region itself is **helium**, but it is not sized by a fixed inset.
It is *derived* from where the SBT material actually sits (see
[`SBTEnvelope.h`](include/DecayVolume/SBTEnvelope.h)): the innermost surfaces —
the side and top/bottom scintillator containers, and the top/bottom longitudinal
beam inner flanges, which hang *below* the sensor plane into the decay region —
bound it directly. Because that inner surface is not linear in Z (the side
containers present a flat outer face over the first part of each sub-frustum to
clear the columns), the helium is built as a stack of GeoTraps, two per
sub-frustum, each strictly inscribed inside the material minus a small
clearance. This cannot overlap the structure or the sensors.

Everything is held in an **air container** (the SBT is part of the experiment's
support frame, not a sealed vessel). The previous monolithic aluminium box
Expand All @@ -30,11 +36,17 @@ children of the container), which the clash-avoidance logic relies on.

```
/SHiP/decay_volume (Air box, 4400 × 6600 × 50400 mm)
├─ /SHiP/decay_volume/helium (PressurisedHe90, central frustum)
├─ /SHiP/decay_volume/helium_0..19 (PressurisedHe90, 20 inscribed GeoTrap slabs)
├─ /SHiP/decay_volume/sbt/structure_* (Iron, 312 GeoBox H-beam pieces)
└─ /SHiP/decay_volume/sbt/sensors_* (Al walls + LAB cells, 3380 GeoTrap)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

The tree above is by *name*, not by containment: `.../sbt/...` is a prefix on the
volume names, not a physical grouping volume. Every volume — helium slabs,
structure pieces and sensor pieces alike — is a direct child of
`/SHiP/decay_volume` (the clash-avoidance logic relies on this flat layout, and
`test_decayvolume` asserts all 3712 are direct children with no grandchildren).

Position in world: z = 58120 mm (centre), unchanged. The 50 m SBT is centred
on the container origin (entrance face at z = −25000 mm in the local frame).

Expand All @@ -47,6 +59,12 @@ on the container origin (entrance face at z = −25000 mm in the local frame).
| Longitudinal beams | 60 | 1/face (sub-frustum 0–4), 2/face (5–9); flanges only |
| Cross-beams | 66 | 11 rows × 2 faces × 3 boxes |

### Helium (20 GeoTrap)

Two inscribed slabs per sub-frustum (10 sub-frusta): the slab boundaries fall at
each sub-frustum entrance and at the side-container flat-piece edge, so the slab
faces follow the sawtooth inner surface without cutting into it.

### Sensors (3380 GeoTrap)

130 containers (80 side + 50 top/bottom), each Z-split into 2 pieces at the
Expand Down Expand Up @@ -77,7 +95,7 @@ entrance Z, and all material names. Unknown keys are reported on stderr.

## Status

- [x] C++ implementation (SBT structure + sensors + helium frustum)
- [x] C++ implementation (SBT structure + sensors + derived helium)
- [x] Frustum shape (replaces the old box vessel approximation)
- [x] Surround Background Tagger integrated
- [x] sbt.toml configuration
Expand All @@ -86,5 +104,9 @@ entrance Z, and all material names. Unknown keys are reported on stderr.
## Tests

`test_decayvolume` checks the container envelope, the 312 structure GeoBoxes,
the 3381 GeoTrap children (1 helium + 3380 sensors), the total of 3693 direct
children, and that the central decay region is a helium GeoTrap frustum.
and the 3400 GeoTrap children (20 helium slabs + 3380 sensors) for a total of
3712 direct children. Beyond the counts, it runs an exact separating-axis
overlap test between every helium slab and every SBT volume in the built tree,
asserting the helium neither protrudes into any material nor leaves a margin
beyond the configured clearance, and repeats that across 14 perturbed SBT
configurations so the guarantee survives re-parameterisation.
15 changes: 13 additions & 2 deletions subsystems/DecayVolume/include/DecayVolume/DecayVolumeFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#pragma once

#include "DecayVolume/SBTConfig.h"

#include <string>

class GeoPhysVol;
Expand All @@ -19,8 +21,9 @@ class SHiPMaterials;
* 50 m rectangular frustum — wrapped around a central helium decay volume.
* The SBT geometry is driven by sbt.toml (parsed into an SBTConfig).
*
* The helium frustum is sized strictly inside the innermost sensor faces, so
* it does not overlap the structure or the sensors.
* The helium is not an independent volume: it is derived from where the SBT
* material actually is (see SBTEnvelope.h), so that it can neither overlap the
* structure and sensors nor leave an unphysical margin behind.
*
* Z: 32.92 to 83.32 m -> centre 58.12 m; placement handled by SHiPGeometry.
*/
Expand All @@ -32,9 +35,17 @@ class DecayVolumeFactory {
/// Build the DecayVolume geometry; returns the air container.
[[nodiscard]] GeoPhysVol* build();

/// The config the last build() actually used.
///
/// Tests must reason about *this*, not a default-constructed SBTConfig:
/// the geometry comes from sbt.toml, and a test that checks a clearance
/// against the C++ default is validating a config it did not build.
[[nodiscard]] const SBTConfig& config() const { return m_config; }

private:
SHiPMaterials& m_materials;
std::string m_configPath;
SBTConfig m_config;

// Air container enclosing the SBT structure + sensors and helium (mm).
static constexpr double s_halfX = 2200.0;
Expand Down
105 changes: 104 additions & 1 deletion subsystems/DecayVolume/include/DecayVolume/SBTConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma once

#include <cmath>
#include <string>

namespace SHiPGeometry {
Expand Down Expand Up @@ -38,7 +39,13 @@ struct SBTConfig {
double sensor_clearance_mm = 1.0;

// ── Helium decay region ─────────────────────────────────────────────
double helium_clearance_mm = 10.0;
// Gap left between the helium and the innermost SBT material, measured along
// the coordinate axis. Must be >= 0; SBTEnvelope enforces that and is the
// only consumer. It is not a safety margin — the default of 1 um is simply
// enough to stop the helium and the SBT sharing a surface, which Geant4's
// navigator handles badly. 0 is legal and gives exact face-to-face contact.
// NOTE: this default must track sbt.toml.
double helium_clearance_mm = 0.001;

// ── Material names (resolved from SHiPMaterials) ────────────────────
std::string material_steel = "Iron";
Expand All @@ -54,6 +61,102 @@ struct SBTConfig {
double webHeight() const { return hbeam_height_mm - 2.0 * hbeam_flange_thickness_mm; }
/// Number of aluminium walls per container (n_cells + 1).
int nWalls() const { return n_cells + 1; }

// ── Frustum profile ─────────────────────────────────────────────────
/// Exit-face Z in the DecayVolume local frame (mm).
double zExit() const { return z_entrance_mm + total_length_mm; }
/// dx_half/dz of the frustum (dimensionless).
double xGrowth() const { return (x_half_exit_mm - x_half_entrance_mm) / total_length_mm; }
/// dy_half/dz of the frustum (dimensionless).
double yGrowth() const { return (y_half_exit_mm - y_half_entrance_mm) / total_length_mm; }
/// Half-extent of the frustum envelope in X at a given Z (mm).
double xHalfAt(double z_mm) const {
return x_half_entrance_mm + (z_mm - z_entrance_mm) * xGrowth();
}
/// Half-extent of the frustum envelope in Y at a given Z (mm).
double yHalfAt(double z_mm) const {
return y_half_entrance_mm + (z_mm - z_entrance_mm) * yGrowth();
}
/// Z of the start of sub-frustum @p s (mm).
double zSubLo(int s) const { return z_entrance_mm + s * subLength(); }

// ── H-beam cross-section primitives ─────────────────────────────────
/// Offset of a flange's mid-plane from the beam axis (mm).
double hbeamFlangeOffset() const {
return 0.5 * hbeam_height_mm - 0.5 * hbeam_flange_thickness_mm;
}
/// Reach of a flange's *outer* surface from the beam axis (mm) = h/2.
double hbeamHalfHeight() const { return 0.5 * hbeam_height_mm; }

// ══════════════════════════════════════════════════════════════════════
// PLACEMENT PRIMITIVES — the single source of truth.
//
// These are THE definitions of where SBT material sits. Both builders
// place volumes with them, and SBTEnvelope derives the helium inner
// envelope from them, so a change to any rule below propagates to the
// helium automatically and the two can never drift apart.
// Do not open-code these expressions anywhere else.
// ══════════════════════════════════════════════════════════════════════

/// Z offset, from the start of a sub-frustum, of the column front-flange
/// outer edge. Sensor containers are split here: the piece upstream of it
/// must present a *flat* outer face, or it would eat into the column.
double zSplitOffset() const { return 0.5 * hbeam_height_mm + 0.5 * hbeam_flange_thickness_mm; }

// --- Side (±X) scintillator containers -------------------------------
/// Half-thickness of a side container in X (mm).
double sideContainerHalfThickness() const { return 0.5 * container_thickness_mm; }
/// |X| of a side container's centroid, given the local frustum half-width.
double sideContainerCentreX(double x_half_mm) const {
return x_half_mm - 0.5 * hbeam_flange_width_mm - sideContainerHalfThickness();
}
/// |X| of a side container's innermost face (mm).
double sideSensorInnerX(double x_half_mm) const {
return sideContainerCentreX(x_half_mm) - sideContainerHalfThickness();
}

// --- Top/bottom (±Y) scintillator containers -------------------------
/// Half-thickness of a top/bottom container in Y (mm).
double topBottomContainerHalfThickness() const {
return 0.5 * container_thickness_mm - sensor_clearance_mm;
}
/// |Y| of a top/bottom container's centroid (mm).
double topBottomContainerCentreY(double y_half_mm) const {
return y_half_mm - 0.5 * container_thickness_mm;
}
/// |Y| of a top/bottom container's innermost face (mm).
double topBottomSensorInnerY(double y_half_mm) const {
return topBottomContainerCentreY(y_half_mm) - topBottomContainerHalfThickness();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// Half-extent in X available to top/bottom containers (mm).
///
/// The container stops half a flange width short of the frustum wall (that
/// is where the columns' inner face is), less a 1 mm gap so it does not
/// land flush against them. The 1 mm is inherited from the original
/// SBTSensorBuilder and is a literal, not sensor_clearance_mm — the two
/// happen to be equal at the current settings, which is worth being aware
/// of when changing sensor_clearance_mm, but they are not the same quantity
/// and this one is deliberately left as-is.
double topBottomAvailX(double x_half_mm) const {
return x_half_mm - 0.5 * hbeam_flange_width_mm - 1.0;
}

// --- Top/bottom longitudinal beams -----------------------------------
// NOTE: these beams *straddle* the scintillator plane — the outer flange
// sits above it, the web is omitted (it would pass through the cells),
// and the INNER FLANGE HANGS BELOW IT, INSIDE THE DECAY REGION. It, not
// the scintillator, is the innermost material in ±Y.
/// |Y| of a top/bottom longitudinal beam's axis (mm).
double longBeamCentreY(double y_half_mm) const { return y_half_mm - 0.5 * webHeight(); }
/// |Y| reached by a longitudinal beam's inner flange surface (mm).
///
/// The beam is inclined by the frustum taper, so its cross-section is
/// rotated: the flange surface lies hbeamHalfHeight()/cos(atan(yGrowth))
/// from the axis measured in world Y, not hbeamHalfHeight().
double longBeamInnerY(double y_half_mm) const {
const double g = yGrowth();
return longBeamCentreY(y_half_mm) - hbeamHalfHeight() * std::sqrt(1.0 + g * g);
}
};

/**
Expand Down
105 changes: 105 additions & 0 deletions subsystems/DecayVolume/include/DecayVolume/SBTEnvelope.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (C) CERN for the benefit of the SHiP Collaboration

#pragma once

#include <vector>

class GeoMaterial;
class GeoPhysVol;

namespace SHiPGeometry {

struct SBTConfig;

/**
* @brief The innermost free region of the SBT, and the helium that fills it.
*
* The helium decay region must fill the space inside the SBT exactly: it may
* not protrude into any steel or scintillator, and it may not leave an
* arbitrary safety margin behind either. That makes its size a *consequence*
* of where the SBT material is, never an independent parameter.
*
* This header therefore derives the helium purely from the placement
* primitives on SBTConfig — the same ones SBTStructureBuilder and
* SBTSensorBuilder place their volumes with. Change a placement rule and the
* helium follows automatically; there is no second copy of the arithmetic to
* forget to update.
*
* Two properties of the SBT make this non-trivial, and both are handled here:
*
* - **The X envelope is not linear in Z.** Side containers are split at the
* column front-flange edge (SBTConfig::zSplitOffset()) and the upstream
* piece has a *flat* outer face frozen at the sub-frustum's entrance
* half-width, so it does not clash with the column. The inner surface is
* therefore a sawtooth, dipping inward by up to xGrowth()*zSplitOffset()
* relative to the frustum. A single linear GeoTrap cannot follow it.
*
* - **The innermost material in Y is steel, not scintillator.** The top and
* bottom longitudinal beams straddle the scintillator plane and their
* inner flange hangs *below* it, into the decay region. It sits deeper
* than the cells by (hbeam_height - flange_thickness) - container_thickness
* + sensor_clearance, and it — not the scintillator — bounds the helium.
*
* Consequently the helium is built as a stack of GeoTraps, two per
* sub-frustum, each exactly filling the envelope over its Z span.
*/

/// One Z slab of the helium: a GeoTrap with rectangular faces at z_lo/z_hi.
struct HeliumPiece {
double z_lo_mm = 0.0;
double z_hi_mm = 0.0;
double dx_lo_mm = 0.0; ///< half-width in X at z_lo
double dx_hi_mm = 0.0; ///< half-width in X at z_hi
double dy_lo_mm = 0.0; ///< half-height in Y at z_lo
double dy_hi_mm = 0.0; ///< half-height in Y at z_hi
};

/**
* @brief |X| of the innermost SBT material at @p z_mm (mm).
*
* Minimum over every volume class that can reach the decay region in X.
* Currently only the side scintillator containers do; the columns and corner
* beams sit a further half-flange-width outboard.
*/
double innerFreeHalfX(const SBTConfig& cfg, double z_mm);

/**
* @brief |Y| of the innermost SBT material at @p z_mm (mm).
*
* Minimum over every volume class that can reach the decay region in Y: the
* top/bottom scintillator containers *and* the inner flange of the top/bottom
* longitudinal beams, which is the binding one.
*/
double innerFreeHalfY(const SBTConfig& cfg, double z_mm);

/**
* @brief Z values at which innerFreeHalfX/Y change slope.
*
* innerFreeHalfX/Y are piecewise linear with knots exactly here, so a linear
* shape that respects the envelope at these Z values respects it everywhere.
* The helium slab boundaries are drawn from this list.
*/
std::vector<double> envelopeKnots(const SBTConfig& cfg);

/**
* @brief The helium slabs filling the SBT interior.
*
* Each slab is inset from innerFreeHalfX/Y by cfg.helium_clearance_mm (0 by
* default: the helium touches the SBT and leaves no margin).
*
* @throws std::runtime_error if any slab would have a non-positive half-width,
* i.e. the configured beams/containers have swallowed the decay region.
*/
std::vector<HeliumPiece> heliumPieces(const SBTConfig& cfg);

/**
* @brief Place the helium slabs into @p container.
*
* Mirrors SBTStructureBuilder / SBTSensorBuilder: takes an SBTConfig and nothing
* else, so the geometry can be built (and swept) without going through sbt.toml.
* Call it *after* the two builders — the helium is a consequence of them.
*/
void buildHelium(GeoPhysVol* container, const GeoMaterial* helium, const SBTConfig& cfg);

} // namespace SHiPGeometry
29 changes: 25 additions & 4 deletions subsystems/DecayVolume/sbt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,31 @@ n_cells = 6
sensor_clearance_mm = 1.0

# ── Helium decay region ─────────────────────────────────────────────────
# The central helium volume is a frustum sized strictly inside the innermost
# sensor faces (x_half - container_thickness, y_half - container_thickness)
# minus this clearance, so it cannot overlap the structure or sensors.
helium_clearance_mm = 10.0
# The helium is NOT sized from a parameter here. It is derived at build time
# from where the SBT material actually is (see SBTEnvelope.h), so that it can
# never overlap the structure or the sensors and never leaves an unphysical
# margin behind. Two consequences are worth knowing about:
#
# * In X it is bounded by the side scintillator containers, whose outer face
# is FLAT over the first zSplitOffset() = (hbeam_height + flange_thickness)/2
# of each sub-frustum so as to clear the columns. The free region is
# therefore a sawtooth, and the helium is built as one GeoTrap per segment
# (2 per sub-frustum) rather than a single frustum.
#
# * In Y it is bounded by STEEL, not scintillator: the top/bottom
# longitudinal beams straddle the sensor plane and their inner flange hangs
# below it, reaching y_half - 0.5*web_height - 0.5*hbeam_height*sec(taper).
# That is ~13.5 mm deeper than the scintillator's inner face.
#
# This is the gap left between the helium and the SBT surfaces, measured along
# the coordinate axis. It must be >= 0, and is validated by SBTEnvelope, its only
# consumer. It is NOT a safety margin: 1 um is far below any engineering
# tolerance and costs ~0.0001% of the fiducial volume. Its only job is to keep
# the helium and the SBT from sharing a surface, because coincident boundaries
# are a classic source of stuck tracks in Geant4's navigator.
# Set it to 0.0 only if you want exact face-to-face contact and have checked
# that the navigator copes.
helium_clearance_mm = 0.001

# ── Materials (resolved from the central SHiPMaterials catalogue) ────────
material_steel = "Iron"
Expand Down
Loading
Loading