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 Straw Tube Spectrometer (SST) is the primary charged-particle tracker in SHiP. It consists of four tracking stations — T1, T2, T3, and T4 — arranged around a dipole spectrometer magnet that sits between T2 and T3. Charged decay products from hidden particles traverse these stations, and the momentum measured from the bending angle in the magnetic field is one of the key quantities used to reconstruct the invariant mass of the decay vertex. Each station contains straw drift tubes arranged in two axial and two stereo views, providing a space-point resolution of ~120 μm per straw. The strawtubes C++ class builds the geometry from a YAML configuration file, and the strawtubesDetector Python class performs the digitisation from Monte Carlo hit points to measured drift distances.

Station layout and coordinates

The four stations are positioned symmetrically around the spectrometer magnet centre at z = 89.57 m. From geometry_config.py:
TrMagGap = 3.5 * u.m   # distance from magnet centre to adjacent station
TrGap    = 2.0 * u.m   # distance between paired stations

c.TrackStation1 = AttrDict(z = c.z - TrMagGap - TrGap)   # ≈ 84.07 m
c.TrackStation2 = AttrDict(z = c.z - TrMagGap)            # ≈ 86.07 m
c.TrackStation3 = AttrDict(z = c.z + TrMagGap)            # ≈ 93.07 m
c.TrackStation4 = AttrDict(z = c.z + TrMagGap + TrGap)   # ≈ 95.07 m
c.z = 89.57 m is the absolute z-position of the spectrometer magnet centre, defined in geometry_config.py. All track-station positions are derived from this single anchor.

C++ class: strawtubes

The geometry class lives in strawtubes/strawtubes.h and inherits from SHiP::Detector<strawtubesPoint>:
// strawtubes/strawtubes.h (excerpt)
class strawtubes : public SHiP::Detector<strawtubesPoint> {
 public:
  explicit strawtubes(std::string medium);

  Bool_t ProcessHits(FairVolume* v = nullptr) override;

  void SetzPositions(Double_t z1, Double_t z2, Double_t z3, Double_t z4);
  void SetApertureArea(Double_t width, Double_t height);
  void SetStrawDiameter(Double_t outer_straw_diameter, Double_t wall_thickness);
  void SetStrawPitch(Double_t straw_pitch, Double_t layer_offset);
  void SetDeltazLayer(Double_t delta_z_layer);
  void SetStereoAngle(Double_t view_angle);
  void SetWireThickness(Double_t wire_thickness);
  void SetDeltazView(Double_t delta_z_view);
  void SetFrameMaterial(const std::string& frame_material);
  void SetStationEnvelope(Double_t width, Double_t height, Double_t length);
  void SetStrawResolution(Double_t v_drift, Double_t sigma_spatial);
  // ...
};

Geometry configuration: geometry/strawtubes_config.yaml

All straw-tube dimensions come from geometry/strawtubes_config.yaml, read via configure_strawtubes() in python/shipDet_conf.py:
# geometry/strawtubes_config.yaml
SST:
  width:                200    # aperture half-width in x  (cm)
  height:               300    # aperture half-height in y (cm)
  wire_thickness:       0.003  # sense wire radius (cm)
  wall_thickness:       0.0036 # straw wall thickness (cm)
  outer_straw_diameter: 2      # outer diameter of straw (cm)
  straw_pitch:          2      # centre-to-centre distance between straws (cm)
  y_layer_offset:       1      # y-offset between adjacent layers (cm)
  delta_z_layer:        1.732  # z-spacing between layers within one view (cm)
  delta_z_view:         12     # z-spacing between stereo views (cm)
  view_angle:           4.57   # stereo angle (degrees)
  station_width:        300    # station envelope half-width x (cm)
  station_height:       350    # station envelope half-height y (cm)
  station_length:       50     # station envelope half-length z (cm)

Frame material options: strawDesign

The strawDesign parameter selects the mechanical frame material for the station envelopes. Only two values are supported:
strawDesignFrame materialNotes
4AluminiumLighter option
10 (default)SteelBaseline for 2025 geometry; also sets cave.floorHeightMuonShield = cave.floorHeightTankA
The medium inside the straws follows the decay-vessel fill:
# python/shipDet_conf.py
ship_geo.strawtubes_geo.medium = (
    "vacuums" if ship_geo.DecayVolumeMedium == "vacuums" else "air"
)

Python digitiser: strawtubesDetector

python/detectors/strawtubesDetector.py subclasses BaseDetector and implements the digitize() method:
# python/detectors/strawtubesDetector.py (excerpt)
from BaseDetector import BaseDetector

class strawtubesDetector(BaseDetector):
    def digitize(self) -> None:
        """Convert strawtubesPoint MC hits to strawtubesHit drift distances.

        The earliest hit per straw (smallest TDC value) is marked valid;
        all later hits in the same straw are marked invalid.
        """
        earliest_per_det_id = {}
        for index, point in enumerate(self.intree.strawtubesPoint):
            hit = ROOT.strawtubesHit(point, self.intree.t0)
            self.det.push_back(hit)
            if hit.isValid():
                detector_id = hit.GetDetectorID()
                if detector_id in earliest_per_det_id:
                    earliest = earliest_per_det_id[detector_id]
                    if self.det[earliest].GetTDC() > hit.GetTDC():
                        self.det[earliest].setInvalid()
                        earliest_per_det_id[detector_id] = index
                    else:
                        self.det[index].setInvalid()
                else:
                    earliest_per_det_id[detector_id] = index
The output branch in the ROOT tree is Digi_strawtubesHits.

Digitisation parameters

Drift velocity and spatial resolution are set in geometry_config.py and forwarded to the strawtubes geometry object:
c.strawtubesDigi.v_drift       = 1.0 / (30 * u.ns / u.mm)  # drift velocity (mm/ns)
c.strawtubesDigi.sigma_spatial = 0.012 * u.cm               # spatial resolution
These are also used in the track-fitting stage via GetStrawResolution().

Reading straw hits from the simulation tree

# Open a simulated ROOT file and iterate over straw MC hits
import ROOT

f = ROOT.TFile.Open("ship.conical.Pythia8-TGeant4_000.root")
tree = f.Get("cbmsim")

for event in tree:
    for hit in event.strawtubesPoint:
        print(
            hit.GetDetectorID(),   # encodes station, view, layer, straw number
            hit.GetTrackID(),      # MC track index
            hit.GetTime(),         # hit time (ns)
            hit.GetEnergyLoss(),   # energy deposit (GeV)
        )

ACTS digitisation config

Track finding and fitting in the spectrometer uses the ACTS framework. The digitisation configuration is stored in python/StrawTracker-digi-config.json:
{
  "acts-geometry-hierarchy-map": {
    "format-version": 0,
    "value-identifier": "digitization-configuration"
  },
  "entries": [
    {
      "volume": 1,
      "value": {
        "smearing": [
          { "index": 0, "mean": 0.0, "stddev": 0.0168, "type": "Gauss" },
          { "index": 1, "mean": 0.0, "stddev": 0.0168, "type": "Gauss" },
          { "index": 5, "mean": 0.0, "stddev": 0.0168, "type": "Gauss" }
        ]
      }
    }
  ]
}
The standard deviation of 0.0168 cm matches the sigma_spatial = 0.012 cm intrinsic resolution convoluted with the drift distance uncertainty.
Straw hit smearing can be disabled during reconstruction for debugging by passing --noStrawSmearing to macro/ShipReco.py. This sets all drift distances to the wire position and uses a wider smearing window.

Spectrometer magnet and field map

The dipole magnet is built by ShipMagnet (passive/ShipMagnet.h). For the 2025 baseline (magnetDesign = 4, MISIS design), aperture and yoke dimensions are:
c.Bfield.x         = 2.2 * u.m    # half-aperture in x
c.Bfield.y         = 3.5 * u.m    # half-aperture in y
c.Bfield.YokeWidth = 0.8 * u.m    # full yoke width
c.Bfield.YokeDepth = 1.4 * u.m    # yoke half-length
c.Bfield.CoilThick = 25.0 * u.cm  # coil thickness
c.Bfield.fieldMap  = "files/2025_02_12_SHiP_SpectrometerField_ECN3_MgB2.root"
The field map ROOT file is read by ShipBFieldMap and provides a full 3-D MgB₂ superconducting magnet field in the ECN3 cavern configuration. To override the map at runtime:
python macro/run_simScript.py \
    --field_map files/my_custom_field.root \
    --shieldName TRY_2025

T0 correction and drift-distance reconstruction

After digitisation, the withT0Estimate() method in strawtubesDetector performs a per-event t0 correction. It averages TDC values across all valid straws (corrected for time-of-flight from the first straw plane) to estimate the event t0, then recomputes drift distances:
# Simplified logic from strawtubesDetector.withT0Estimate()
v_drift = global_variables.modules["strawtubes"].StrawVdrift()
for aDigi in self.det:
    if not aDigi.isValid():
        continue
    detID = aDigi.GetDetectorID()
    global_variables.modules["strawtubes"].StrawEndPoints(detID, start, stop)
    delt1 = (start[2] - z1) / u.speedOfLight
    t0   += aDigi.GetDigi() - delt1
The corrected drift distance GetDigi() (in cm) is what is passed to track fitting.

Track fitting

Spectrometer track fitting uses the GenFit2 framework with a Runge-Kutta track representation (RKTrackRep) and WireMeasurement objects built from each valid straw hit. The ACTS-based reconstruction path (python/ACTSReco.py) uses the smeared local coordinates from the digi config JSON above.

Detector Overview

Full downstream map of all SHiP sub-detectors with class references.

Muon Shield

The magnetised muon shield upstream of the spectrometer acceptance.

Build docs developers (and LLMs) love