Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-26 08:04:21

0001 """Pure-Python coverage for the Examples-module EDM bindings (measurements,
0002 tracks/track states, track containers) that needs no detector or simulation.
0003 Mirrors Python/Core/tests/test_event_data.py, but for acts.examples types.
0004 """
0005 
0006 import numpy as np
0007 import pytest
0008 
0009 import acts
0010 import acts.examples as ae
0011 
0012 
0013 def test_measurement_creation():
0014     meas_properties = [
0015         {
0016             "geometryId": acts.GeometryIdentifier(798),
0017             "indices": [0],
0018             "parameters": [1.0],
0019             "covariance": [0.1],
0020         },
0021         {
0022             "geometryId": acts.GeometryIdentifier(123),
0023             "indices": [0, 1],
0024             "parameters": [1.0, 2.0],
0025             "covariance": [0.1, 0.1],
0026         },
0027         {
0028             "geometryId": acts.GeometryIdentifier(456),
0029             "indices": [0, 1, 4],
0030             "parameters": [3.0, 4.0, 5.0],
0031             "covariance": [0.2, 0.2, 0.2],
0032         },
0033     ]
0034 
0035     container = acts.examples.MeasurementContainer()
0036     container.reserve(3)
0037     for meas_prop in meas_properties:
0038         meas = container.emplaceMeasurement(**meas_prop)
0039 
0040     for i in range(len(meas_properties)):
0041         meas = container[i]
0042         meas_prop = meas_properties[i]
0043 
0044         dim = len(meas_prop["indices"])
0045         assert meas.geometryId.value == meas_prop["geometryId"].value
0046         assert [meas_prop["indices"][i] == meas.subspaceIndices[i] for i in range(dim)]
0047         indices = meas_prop["indices"]
0048         assert [
0049             meas_prop["parameters"][i] == meas.fullParameters[indices[i]]
0050             for i in range(dim)
0051         ]
0052         assert [
0053             meas_prop["covariance"][i] == meas.fullCovariance[indices[i], indices[i]]
0054             for i in range(dim)
0055         ]
0056 
0057     assert len(container) == 3
0058 
0059     # Build a subset from indices 0 and 2 and verify it mirrors the container data
0060     subset = acts.examples.MeasurementSubset(container, [0, 2])
0061     assert len(subset) == 2
0062 
0063     # Iteration covers exactly the selected measurements in order
0064     subset_list = list(subset)
0065     assert len(subset_list) == 2
0066     assert subset_list[0].index == 0
0067     assert subset_list[1].index == 2
0068 
0069     # __getitem__ by subset position
0070     assert subset[0].index == 0
0071     assert subset[1].index == 2
0072 
0073     # getMeasurement by original-container index
0074     assert (
0075         subset.getMeasurement(0).geometryId.value
0076         == meas_properties[0]["geometryId"].value
0077     )
0078     assert (
0079         subset.getMeasurement(2).geometryId.value
0080         == meas_properties[2]["geometryId"].value
0081     )
0082 
0083     # Measurement data is consistent with the container entries
0084     for pos, orig_idx in enumerate([0, 2]):
0085         meas = subset[pos]
0086         meas_prop = meas_properties[orig_idx]
0087         dim = len(meas_prop["indices"])
0088         assert meas.geometryId.value == meas_prop["geometryId"].value
0089         indices = meas_prop["indices"]
0090         assert [meas.subspaceIndices[i] == meas_prop["indices"][i] for i in range(dim)]
0091         assert [
0092             meas.fullParameters[indices[i]] == meas_prop["parameters"][i]
0093             for i in range(dim)
0094         ]
0095 
0096 
0097 def test_measurement_map_creation():
0098     from acts.examples import (
0099         MeasurementParticlesMap,
0100         MeasurementSimHitsMap,
0101         ParticleMeasurementsMap,
0102         SimBarcode,
0103         SimHitMeasurementsMap,
0104     )
0105 
0106     # MeasurementSimHitsMap: meas 0 → simhits {10, 11}, meas 1 → simhit {20}
0107     m = MeasurementSimHitsMap()
0108     m.insert(0, 10)
0109     m.insert(0, 11)  # same key — multi-map
0110     m.insert(1, 20)
0111     assert len(m) == 3
0112 
0113     assert 0 in m
0114     assert 2 not in m
0115 
0116     vals = m.valuesFor(0)
0117     assert sorted(vals) == [10, 11]
0118     assert m.valuesFor(1) == [20]
0119     assert m.valuesFor(99) == []
0120 
0121     pairs = list(m)
0122     assert len(pairs) == 3
0123     assert all(isinstance(k, int) and isinstance(v, int) for k, v in pairs)
0124 
0125     inv = m.invert()
0126     assert isinstance(inv, SimHitMeasurementsMap)
0127     assert len(inv) == 3
0128     assert inv.valuesFor(10) == [0]
0129     assert inv.valuesFor(11) == [0]
0130     assert inv.valuesFor(20) == [1]
0131 
0132     # MeasurementParticlesMap: meas 0 came from two particles, meas 1 from one
0133     bc0 = SimBarcode()
0134     bc0.particle = 1
0135     bc1 = SimBarcode()
0136     bc1.particle = 2
0137     mp = MeasurementParticlesMap()
0138     mp.insert(0, bc0)
0139     mp.insert(0, bc1)  # same measurement, two particles
0140     mp.insert(1, bc0)
0141     assert len(mp) == 3
0142 
0143     assert mp.valuesFor(0) == [bc0, bc1]
0144 
0145     inv_p = mp.invert()
0146     assert isinstance(inv_p, ParticleMeasurementsMap)
0147     assert len(inv_p) == 3
0148 
0149 
0150 # --- Track / track state / track container -------------------------------
0151 #
0152 # Coverage for the track/track-state container bindings that the
0153 # simulation-driven tests (test_truth_tracking.py, test_histogram_fit_backends.py)
0154 # don't exercise: parity between the mutable and const proxies, and the
0155 # const <-> mutable TrackContainer round trip.
0156 
0157 
0158 def _fill_track(track):
0159     surface = acts.Surface.createPerigee(acts.Vector3(0, 0, 0))
0160     geo_id = acts.GeometryIdentifier()
0161 
0162     track.referenceSurface = surface
0163     track.parameters = acts.BoundVector(0.1, 0.2, 0.3, 1.4, 0.01, 0.0)
0164     track.covariance = acts.BoundMatrix.Identity()
0165     track.particleHypothesis = acts.ParticleHypothesis.muon
0166     track.nMeasurements = 3
0167     track.nHoles = 1
0168     track.nOutliers = 2
0169     track.nSharedHits = 1
0170     track.chi2 = 4.5
0171     track.nDoF = 5
0172 
0173     state = track.appendTrackState(ae.TrackStatePropMask.All)
0174     state.typeFlags.setIsMeasurement()
0175     state.referenceSurface = surface
0176     state.uncalibratedSourceLink = ae.IndexSourceLink(geo_id, 0).toSourceLink()
0177     state.predicted = acts.BoundVector(0.1, 0.2, 0.3, 1.4, 0.01, 0.0)
0178     state.predictedCovariance = acts.BoundMatrix.Identity()
0179     state.filtered = acts.BoundVector(0.15, 0.25, 0.3, 1.4, 0.01, 0.0)
0180     state.filteredCovariance = acts.BoundMatrix.Identity()
0181     state.smoothed = acts.BoundVector(0.12, 0.22, 0.3, 1.4, 0.01, 0.0)
0182     state.smoothedCovariance = acts.BoundMatrix.Identity()
0183     state.jacobian = acts.BoundMatrix.Identity()
0184     state.chi2 = 1.5
0185     state.pathLength = 12.0
0186     state.allocateCalibrated(2)
0187     state.effectiveCalibrated = [1.0, 2.0]
0188     state.effectiveCalibratedCovariance = [[1.0, 0.0], [0.0, 1.0]]
0189     state.setProjectorSubspaceIndices([0, 1])
0190 
0191     return geo_id, state
0192 
0193 
0194 def _check_track(track, state, *, linked):
0195     assert track.index == 0
0196     assert track.tipIndex == state.index
0197     assert track.stemIndex == (state.index if linked else ae.kTrackIndexInvalid)
0198     assert track.hasReferenceSurface
0199     assert track.parameters[0] == pytest.approx(0.1)
0200     assert track.covariance[0, 0] == pytest.approx(1.0)
0201     assert track.particleHypothesis.absolutePdg == acts.PdgParticle.eMuon
0202     assert track.nMeasurements == 3
0203     assert track.nHoles == 1
0204     assert track.nOutliers == 2
0205     assert track.nSharedHits == 1
0206     assert track.chi2 == pytest.approx(4.5)
0207     assert track.nDoF == 5
0208     assert track.nTrackStates == 1
0209     assert track.isForwardLinked is linked
0210 
0211 
0212 def _check_state(state, geo_id, *, is_const):
0213     assert state.hasReferenceSurface
0214     assert state.referenceSurface is not None
0215     assert state.hasPredicted
0216     assert state.hasFiltered
0217     assert state.hasSmoothed
0218     assert state.hasJacobian
0219     assert state.hasProjector
0220     assert state.hasUncalibratedSourceLink
0221     assert state.hasCalibrated
0222     assert state.calibratedSize == 2
0223     assert state.effectiveCalibrated == pytest.approx([1.0, 2.0])
0224     assert np.allclose(state.effectiveCalibratedCovariance, [[1.0, 0.0], [0.0, 1.0]])
0225     assert list(state.projectorSubspaceIndices) == [0, 1]
0226     assert state.chi2 == pytest.approx(1.5)
0227     assert state.pathLength == pytest.approx(12.0)
0228     assert state.predicted[0] == pytest.approx(0.1)
0229     assert state.filtered[0] == pytest.approx(0.15)
0230     assert state.smoothed[0] == pytest.approx(0.12)
0231     assert all(
0232         state.jacobian[i, j] == pytest.approx(1.0 if i == j else 0.0)
0233         for i in range(6)
0234         for j in range(6)
0235     )
0236     assert state.parameters[0] == pytest.approx(state.smoothed[0])
0237     assert state.typeFlags.isMeasurement
0238 
0239     # the originally reported gap: uncalibratedSourceLink and referenceSurface
0240     # must be readable on the const proxy, not just the mutable one.
0241     isl = ae.IndexSourceLink.fromSourceLink(state.uncalibratedSourceLink)
0242     assert isl.geometryId() == geo_id
0243 
0244     proxy_type = ae.ConstTrackStateProxy if is_const else ae.TrackStateProxy
0245     assert isinstance(state, proxy_type)
0246 
0247 
0248 def test_mutable_track_proxy():
0249     tc = ae.TrackContainer()
0250     track = tc.makeTrack()
0251     geo_id, state = _fill_track(track)
0252 
0253     _check_track(track, state, linked=False)
0254     _check_state(state, geo_id, is_const=False)
0255 
0256     # trackStates()/trackStatesReversed() are only meaningful once linked
0257     track.linkForward()
0258     fwd = list(track.trackStates)
0259     rev = list(track.trackStatesReversed)
0260     assert len(fwd) == len(rev) == 1
0261     assert fwd[0].index == rev[0].index == state.index
0262 
0263     assert track.hasColumn("doesNotExist") is False
0264 
0265 
0266 def test_const_mutable_parity():
0267     """The const and mutable track/track-state proxies must read back
0268     identical values for every property they share -- the parity the
0269     templated binder is supposed to guarantee by construction."""
0270     tc = ae.TrackContainer()
0271     track = tc.makeTrack()
0272     geo_id, _ = _fill_track(track)
0273     track.linkForward()
0274 
0275     const_tc = tc.makeConst()
0276     const_track = const_tc[0]
0277     const_state = next(iter(const_track.trackStatesReversed))
0278 
0279     _check_track(const_track, const_state, linked=True)
0280     _check_state(const_state, geo_id, is_const=True)
0281 
0282     assert isinstance(const_track, ae.ConstTrackProxy)
0283     assert const_track.hasColumn("doesNotExist") is False
0284 
0285 
0286 def test_const_to_mutable_round_trip():
0287     tc = ae.TrackContainer()
0288     track = tc.makeTrack()
0289     _fill_track(track)
0290     track.linkForward()
0291 
0292     const_tc = tc.makeConst()
0293 
0294     # acts.examples.TrackContainer(const_tc) and const_tc.makeMutable() are
0295     # both independent, fully mutable copies.
0296     for mutable_copy in (ae.TrackContainer(const_tc), const_tc.makeMutable()):
0297         assert len(mutable_copy) == 1
0298         copy_track = mutable_copy[0]
0299         assert copy_track.chi2 == pytest.approx(4.5)
0300         assert copy_track.nMeasurements == 3
0301 
0302         copy_track.chi2 = 99.0
0303         assert copy_track.chi2 == pytest.approx(99.0)
0304         # the source const container is untouched by mutating the copy
0305         assert const_tc[0].chi2 == pytest.approx(4.5)
0306 
0307 
0308 def test_track_container_iteration_and_getitem():
0309     tc = ae.TrackContainer()
0310     for i in range(3):
0311         t = tc.makeTrack()
0312         t.chi2 = float(i)
0313 
0314     assert len(tc) == 3
0315     assert [t.chi2 for t in tc] == [0.0, 1.0, 2.0]
0316     assert tc[1].chi2 == pytest.approx(1.0)
0317     assert tc.getTrack(2).chi2 == pytest.approx(2.0)
0318 
0319     const_tc = tc.makeConst()
0320     assert len(const_tc) == 3
0321     assert [t.chi2 for t in const_tc] == [0.0, 1.0, 2.0]
0322     assert const_tc[1].chi2 == pytest.approx(1.0)
0323     assert const_tc.getTrack(2).chi2 == pytest.approx(2.0)
0324 
0325 
0326 def test_track_container_soa_numpy_views():
0327     tc = ae.TrackContainer()
0328     for i in range(3):
0329         t = tc.makeTrack()
0330         t.parameters = acts.BoundVector(float(i), 0.0, 0.0, 1.4, 0.01, 0.0)
0331         t.chi2 = float(i) + 0.5
0332         t.nOutliers = i
0333         t.nSharedHits = 2 * i
0334 
0335     const_tc = tc.makeConst()
0336 
0337     for i, track in enumerate(const_tc):
0338         assert const_tc.parameters[i, 0] == pytest.approx(track.parameters[0])
0339         assert const_tc.chi2[i] == pytest.approx(track.chi2)
0340         assert const_tc.nOutliers[i] == track.nOutliers
0341         assert const_tc.nSharedHits[i] == track.nSharedHits
0342 
0343 
0344 def test_ensure_dynamic_columns_and_copy_from():
0345     tc = ae.TrackContainer()
0346     track = tc.makeTrack()
0347     _fill_track(track)
0348     track.linkForward()
0349     const_tc = tc.makeConst()
0350 
0351     dst = ae.TrackContainer()
0352     dst.ensureDynamicColumns(const_tc)
0353     dst_track = dst.makeTrack()
0354     dst_track.copyFrom(const_tc[0])
0355 
0356     assert dst_track.chi2 == pytest.approx(4.5)
0357     assert dst_track.nMeasurements == 3
0358     assert len(list(dst_track.trackStates)) == 1
0359 
0360 
0361 def test_any_proxy():
0362     """AnyMutableTrackProxy/AnyConstTrackProxy/AnyMutableTrackStateProxy/
0363     AnyConstTrackStateProxy are type-erased views onto the same underlying
0364     storage as the concrete proxy they were constructed from -- reading them
0365     must agree, and mutating through the Any* handle must be visible back on
0366     the original proxy.
0367 
0368     Note: an Any*Proxy constructed from a mutable TrackProxy/TrackStateProxy
0369     must not outlive a TrackContainer.makeConst() call on its container --
0370     that moves the backing storage out, and the Any*Proxy would dangle just
0371     like the original proxy would.
0372     """
0373     tc = ae.TrackContainer()
0374     track = tc.makeTrack()
0375     geo_id, state = _fill_track(track)
0376 
0377     any_mut_track = ae.AnyMutableTrackProxy(track)
0378     assert any_mut_track.chi2 == pytest.approx(4.5)
0379     assert any_mut_track.tipIndex == track.tipIndex
0380     assert any_mut_track.parameters[0] == pytest.approx(track.parameters[0])
0381     any_mut_track.chi2 = 99.0
0382     assert track.chi2 == pytest.approx(99.0)  # same storage
0383     track.chi2 = 4.5
0384 
0385     any_mut_state = ae.AnyMutableTrackStateProxy(state)
0386     assert any_mut_state.chi2 == pytest.approx(1.5)
0387     assert any_mut_state.predicted[0] == pytest.approx(state.predicted[0])
0388     isl = ae.IndexSourceLink.fromSourceLink(any_mut_state.uncalibratedSourceLink)
0389     assert isl.geometryId() == geo_id
0390     assert any_mut_state.effectiveCalibrated == pytest.approx([1.0, 2.0])
0391     any_mut_state.chi2 = 88.0
0392     assert state.chi2 == pytest.approx(88.0)  # same storage
0393     state.chi2 = 1.5
0394 
0395     const_tc = tc.makeConst()
0396     const_track = const_tc[0]
0397     const_state = next(iter(const_track.trackStatesReversed))
0398 
0399     any_const_track = ae.AnyConstTrackProxy(const_track)
0400     assert any_const_track.chi2 == pytest.approx(4.5)
0401 
0402     any_const_state = ae.AnyConstTrackStateProxy(const_state)
0403     assert any_const_state.chi2 == pytest.approx(1.5)