Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ShipSoft/FairShip/llms.txt

Use this file to discover all available pages before exploring further.

The muon shield is the most critical background-suppression element in SHiP. When 400 GeV protons hit the tungsten target they produce a dense cone of secondary muons — up to 10⁸ per spill — that would otherwise flood the downstream spectrometer. The shield consists of a sequence of magnetised iron magnets whose transverse field deflects muons of both signs out of the detector acceptance. It is passive (no readout) but dominates the experiment’s overall length and weight budget, so its geometry is highly optimised. The ShipMuonShield C++ class in passive/ builds the shield from a flat parameter vector read from the shield_db dictionary in python/geometry_config.py, and the ShipBFieldMap, ShipBellField, and ShipCompField classes in field/ provide the associated magnetic field descriptions.

C++ class: ShipMuonShield

The shield geometry lives in passive/ShipMuonShield.h and passive/ShipMuonShield.cxx. It inherits from FairRoot’s FairModule and overrides ConstructGeometry().
// passive/ShipMuonShield.h (excerpt)
class ShipMuonShield : public FairModule {
 public:
  ShipMuonShield(std::vector<double> in_params, Double_t z,
                 const Bool_t WithConstShieldField, const Bool_t SC_key);
  void ConstructGeometry() override;
  void SetSNDSpace(Bool_t hole, Double_t hole_dx, Double_t hole_dy);

 protected:
  size_t nMagnets{0};
  Int_t nParams{0};           // always 15 per magnet section
  Bool_t fWithConstShieldField{false};
  Bool_t fSC_mag{false};      // superconducting / hybrid field flag
  std::vector<Double_t> shield_params;
  Bool_t snd_hole{false};
  Double_t snd_hole_dx = 0., snd_hole_dy = 0.;
  // ...
};
The constructor copies in_params into shield_params and computes nMagnets = in_params.size() / 15. If the vector length is not a multiple of 15 an error is logged.

SetSNDSpace

When SND design 2 (MTC) is active, the shield is asked to carve a rectangular hole matching the MTC detector footprint:
MuonShield.SetSNDSpace(
    hole    = true,
    hole_dx = (ship_geo.mtc_geo.width  + 5.0 * cm) / 2.0,
    hole_dy = (ship_geo.mtc_geo.height + 5.0 * cm) / 2.0,
);

Parameter format: 15 numbers per magnet

Each row of shield_db[name]["params"] encodes one magnet section as a list of exactly 15 floating-point numbers. The geometry engine unpacks them in the order shown below.
IndexSymbolDescription
0ZgapLongitudinal gap (cm) upstream of this magnet
1dZ (half-length)Half-length of the magnet along beam axis (cm)
2dXInInner half-width in x at the entrance face (cm)
3dXOutInner half-width in x at the exit face (cm)
4dYInInner half-height in y at the entrance face (cm)
5dYOutInner half-height in y at the exit face (cm)
6gapInHorizontal gap between left and right yoke halves at entrance (cm)
7gapOutHorizontal gap between left and right yoke halves at exit (cm)
8ratio_yokeInYoke cross-section ratio at entrance
9ratio_yokeOutYoke cross-section ratio at exit
10dY_yokeInYoke y-half-height at entrance face (cm)
11dY_yokeOutYoke y-half-height at exit face (cm)
12midGapInCentral midplane gap at entrance (cm)
13midGapOutCentral midplane gap at exit (cm)
14BgoalTarget field strength (T); sign encodes field direction
The full parameter vector passed to ShipMuonShield is the concatenation (flattened) of all rows:
# python/geometry_config.py
c.muShield.params = [item for sublist in params for item in sublist]

Named shield configurations: shield_db

The shield_db dictionary in python/geometry_config.py stores named geometry variants. Each entry has three keys:
KeyTypeMeaning
hybridboolUse a hybrid (superconducting + normal conducting) field configuration; passed as SC_key to ShipMuonShield
WithConstFieldboolUse a constant-value field inside the magnet apertures instead of a bell-shaped or map-based field
paramslist[list[float]]One sublist per magnet section, each with exactly 15 elements

TRY_2025 — current baseline

shield_db = {
    "TRY_2025": {
        "hybrid": False,
        "WithConstField": False,
        "params": [
            # Zgap,  dZ,    dXIn,  dXOut, dYIn,  dYOut, gapIn, gapOut,
            # ratio_yokeIn, ratio_yokeOut, dY_yokeIn, dY_yokeOut, midGapIn, midGapOut, Bgoal
            [  0,  115.5,  50.00, 50.00, 119.00, 119.00,  2.00,  2.00,  1.00,  1.00,  50.00, 50.00,  0.00,  0.00,  1.90],
            [ 20,  495.0,  67.10, 79.92,  27.00,  43.00,  5.00,  5.00,  1.38,  1.06,  67.10, 79.92,  0.00,  0.00,  1.90],
            [ 10,  280.48, 53.12, 49.56,  43.00,  56.00,  5.03,  5.00,  2.11,  2.40,  53.12, 49.56,  0.00,  0.00,  1.90],
            [ 10,  232.53,  2.73,  3.68,  56.00,  56.00,  5.00,  5.21, 60.44, 45.63,   2.73,  3.68,  0.50,  0.50, -1.91],
            [ 10,   85.0,  31.00,107.12,  56.00,  56.00,  5.27,  5.00,  4.55,  0.63,   1.00, 77.12,  0.00,  0.00, -1.91],
            [ 10,  233.82, 30.03, 40.00,  56.00,  56.00,  5.00,  5.01,  4.83,  3.37,  30.03, 40.00,  0.00,  0.00, -1.91],
        ],
    },
}
The total shield length is computed from these parameters:
c.muShield.length = sum(line[0] + line[1] * 2 for line in params)
c.muShield.nMagnets = len(params)   # 6 for TRY_2025

Selecting a shield at runtime

Pass --shieldName to run_simScript.py:
python macro/run_simScript.py --shieldName TRY_2025 --output geo_try2025.root
Internally this calls geometry_config.create_config(shieldName="TRY_2025"). The function raises ValueError if the name is not present in shield_db.

Magnet z-positions

geometry_config.py pre-computes the z-centre of each magnet and stores it in c.muShield.Entrance:
for i in range(len(c.muShield.Zgap)):
    if i == 0:
        c.muShield.Entrance.append(c.muShield.z + c.muShield.Zgap[i])
    else:
        c.muShield.Entrance.append(
            c.muShield.Entrance[i-1]
            + c.muShield.half_length[i-1] * 2
            + c.muShield.Zgap[i]
        )
These positions are consumed by configure_snd_mtc when zPosition == "auto" so the MTC sub-detector is aligned to the last magnet.

Magnetic field classes

Reads a 3-D field map from a ROOT file (distances in cm, fields in Tesla). Euler rotation angles and a global offset allow it to be positioned anywhere in the cavern.
// field/ShipBFieldMap.h
class ShipBFieldMap : public TVirtualMagField {
 public:
  ShipBFieldMap(const std::string& label,
                const std::string& mapFileName,
                double xOffset, double yOffset, double zOffset,
                double phi, double theta, double psi,
                double scale = 1.0,
                bool isSymmetric = false);
  // ...
};
The spectrometer field map file is set in geometry_config.py:
c.Bfield.fieldMap = "files/2025_02_12_SHiP_SpectrometerField_ECN3_MgB2.root"

Inspecting the shield geometry

After a simulation run, use the helper macro to print the geometry tree:
python macro/getGeoInformation.py \
    --geometry geo_my-run.root \
    --level 2
This calls TGeoManager::GetTopVolume() and recursively prints all daughter volumes down to depth 2, showing the ShipMuonShield assembly and its individual magnet sub-volumes (MagnetVolume_0, MagnetVolume_1, …).
Set --level 4 to see individual TGeoArb8 magnet-yoke pieces and verify that the geometry parameters match your shield_db entry.

Adding a new shield variant

1
Define the parameter set
2
Add a new entry to shield_db in python/geometry_config.py:
3
shield_db["MY_SHIELD"] = {
    "hybrid": False,
    "WithConstField": False,
    "params": [
        # [Zgap, dZ, dXIn, dXOut, dYIn, dYOut, gapIn, gapOut,
        #  ratio_yokeIn, ratio_yokeOut, dY_yokeIn, dY_yokeOut, midGapIn, midGapOut, Bgoal]
        [0, 100.0, 45.0, 45.0, 110.0, 110.0, 2.0, 2.0, 1.0, 1.0, 45.0, 45.0, 0.0, 0.0, 1.85],
        # ... more magnet sections ...
    ],
}
4
Update the CLI choices list
5
The --shieldName argument in macro/run_simScript.py has a choices restriction. Add your new name to the list so argparse accepts it:
6
# macro/run_simScript.py — find this block and extend the choices list
parser.add_argument(
    "--shieldName",
    help="The name of the muon shield in the database to use.",
    default="TRY_2025",
    choices=["TRY_2025", "MY_SHIELD"],  # add your name here
)
7
Run with the new name
8
python macro/run_simScript.py --shieldName MY_SHIELD --nEvents 100 \
    --output sim_my_shield.root
9
Verify the geometry
10
python macro/getGeoInformation.py --geometry sim_my_shield.root --level 3
Each row in params must have exactly 15 elements. If the count is wrong, ShipMuonShield logs an error and the resulting geometry will be incorrect. Always check nMagnets == len(params) after adding a new entry.

Build docs developers (and LLMs) love