Skip to content

Commit 08c5eaa

Browse files
sawenzelclaude
andcommitted
Shift the first orbit along with the ITS ramp-up start of run
This fixes a problem in the ITS ramp-up anchoring shift and adds a unit test. - The shift moved --sor and --timestamp but left --first-orbit at the orbit of start-of-run, while the timeframes a job simulates are placed by orbit. - A job at production offset 0 therefore still sampled the first orbit of the run, where ITS/Calib/TimeDeadMap masks all 24120 chips, and wrote no ITS digits at all: no ITS tracks, no reconstructed collisions, and an AOD whose collision table is empty. - HBFUtils.startTime and HBFUtils.orbitFirst also disagreed by the ramp-up duration. - Both coordinates now move together in shift_anchor_past_ITS_rampup. - For run 571781 with SPLITID=1 the first orbit moves from 20505888 to 20562116, past the 34080 orbits that run's dead map masks. https://its.cern.ch/jira/browse/O2-6565 https://its.cern.ch/jira/browse/O2-6894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 39ee30b commit 08c5eaa

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

MC/bin/o2dpg_sim_workflow_anchored.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,24 @@ def retrieve_ITS_RampDuration(ccdbreader, timestamp):
320320
print("WARNING: ITS ramp duration vector is empty, using 0")
321321
return 0
322322

323+
def milliseconds_to_orbits(milliseconds):
324+
"""
325+
Number of complete LHC orbits contained in a time span given in milliseconds.
326+
Rounded up, so that a span used to skip something is never cut short.
327+
"""
328+
return int(math.ceil(1000. * milliseconds / LHCOrbitMUS))
329+
330+
def shift_anchor_past_ITS_rampup(run_start, first_orbit, ITS_rampup):
331+
"""
332+
Moves the anchoring point past the ITS ramp-up period, in both of its
333+
coordinates, and returns the pair (start of run in ms, first orbit).
334+
The two have to move together: the timeframes a job simulates are placed by
335+
orbit (first orbit plus the production offset), so shifting only the
336+
timestamp leaves a job at production offset 0 inside the ramp, where the ITS
337+
time-dead map masks every chip.
338+
"""
339+
return run_start + ITS_rampup, first_orbit + milliseconds_to_orbits(ITS_rampup)
340+
323341
def retrieve_MinBias_CTPScaler_Rate(raw_rate_at, finaltime, trig_eff_arg, NBunches, ColSystem, eCM, run_number = -1):
324342
"""
325343
Turns the raw CTP counting rate at finaltime (in milliseconds) into the interaction rate for
@@ -590,10 +608,10 @@ def main():
590608
run_start = GLOparams["SOR"]
591609
run_end = GLOparams["EOR"]
592610

593-
# Adjust start of run using ITS ramp-up period
611+
# Adjust the anchoring point using the ITS ramp-up period
594612
ITS_rampup = retrieve_ITS_RampDuration(ccdbreader, run_start)
595613
print(f"ITS ramp-up time: {ITS_rampup} ms")
596-
effective_run_start = run_start + ITS_rampup
614+
effective_run_start, effective_first_orbit = shift_anchor_past_ITS_rampup(run_start, GLOparams["FirstOrbit"], ITS_rampup)
597615
mid_run_timestamp = (effective_run_start + run_end) // 2
598616

599617
# --------
@@ -676,6 +694,7 @@ def main():
676694

677695
# this is anchored to
678696
print ("Determined start-of-run to be: ", effective_run_start)
697+
print ("Determined first orbit to be: ", effective_first_orbit)
679698
print ("Determined end-of-run to be: ", run_end)
680699
print ("Determined timestamp to be : ", timestamp)
681700
print ("Determined offset to be : ", prod_offset)
@@ -721,7 +740,7 @@ def main():
721740
# needs to be handled as further below:
722741
energyarg = (" -eCM " + str(eCM)) if A1 == A2 else (" -eA " + str(eA) + " -eB " + str(eB))
723742
forwardargs += " -tf " + str(args.tf) + " --sor " + str(effective_run_start) + " --timestamp " + str(timestamp) + " --production-offset " + str(prod_offset) + " -run " + str(args.run_number) + " --run-anchored --first-orbit " \
724-
+ str(GLOparams["FirstOrbit"]) + " --orbitsPerTF " + str(GLOparams["OrbitsPerTF"]) + str(energyarg)
743+
+ str(effective_first_orbit) + " --orbitsPerTF " + str(GLOparams["OrbitsPerTF"]) + str(energyarg)
725744
# the following options can be overwritten/influenced from the outside
726745
if not '-col' in forwardargs:
727746
forwardargs += ' -col ' + ColSystem
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
"""Offline test: the ITS ramp-up shift must move the first orbit, not only the timestamp."""
3+
import os
4+
import sys
5+
import unittest
6+
7+
HERE = os.path.dirname(os.path.abspath(__file__))
8+
sys.path.insert(0, os.path.dirname(HERE))
9+
10+
os.environ.setdefault("O2DPG_ROOT", os.path.dirname(os.path.dirname(os.path.dirname(HERE))))
11+
12+
try:
13+
import o2dpg_sim_workflow_anchored as anchored # needs ROOT
14+
except ImportError as exc: # pragma: no cover
15+
anchored = None
16+
IMPORT_ERROR = exc
17+
18+
# Run 571781 of LHC26e9, whose SPLITID=1 job reconstructed no collisions at all:
19+
# the ITS time-dead map masks all 24120 chips up to orbit 20539968 and the job was
20+
# placed at the run's first orbit. See https://its.cern.ch/jira/browse/O2-6894
21+
FIRST_ORBIT = 20505888
22+
SOR = 1778806732526
23+
ITS_RAMPUP_MS = 5000
24+
FIRST_ALIVE_ORBIT = 20539968
25+
26+
27+
@unittest.skipIf(anchored is None, "o2dpg_sim_workflow_anchored not importable (needs ROOT)")
28+
class TestRampUpShift(unittest.TestCase):
29+
30+
def test_orbits_from_milliseconds(self):
31+
orbit_ms = anchored.LHCOrbitMUS / 1000.
32+
self.assertEqual(anchored.milliseconds_to_orbits(0), 0)
33+
# a span of one orbit minus an epsilon still has to cover a full orbit
34+
self.assertEqual(anchored.milliseconds_to_orbits(0.5 * orbit_ms), 1)
35+
self.assertEqual(anchored.milliseconds_to_orbits(orbit_ms), 1)
36+
37+
def test_both_coordinates_move(self):
38+
"""A ramp-up of a few seconds must move the orbit as well as the timestamp."""
39+
start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS)
40+
self.assertEqual(start, SOR + ITS_RAMPUP_MS)
41+
self.assertGreater(orbit, FIRST_ORBIT)
42+
43+
def test_nothing_moves_without_a_ramp(self):
44+
self.assertEqual(anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, 0),
45+
(SOR, FIRST_ORBIT))
46+
47+
def test_shifted_orbit_is_never_inside_the_ramp(self):
48+
"""The shifted orbit must sit at or after the shifted timestamp, never before."""
49+
for ramp_ms in (0, 1, 500, ITS_RAMPUP_MS, 30000):
50+
start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ramp_ms)
51+
time_of_orbit = SOR + (orbit - FIRST_ORBIT) * anchored.LHCOrbitMUS / 1000.
52+
self.assertGreaterEqual(time_of_orbit, start,
53+
f"orbit shift falls short of the ramp for {ramp_ms} ms")
54+
55+
def test_shift_agrees_with_the_timestamp_to_orbit_conversion(self):
56+
"""Closure: the shifted orbit is what main() derives from the shifted timestamp."""
57+
start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS)
58+
# this is the conversion main() uses for the exclude_timestamp() check
59+
derived = FIRST_ORBIT + int((start - SOR) / (anchored.LHCOrbitMUS / 1000.))
60+
self.assertLessEqual(abs(orbit - derived), 1)
61+
62+
def test_split_id_one_clears_the_its_dead_window(self):
63+
"""Regression: the first job of a production must not sample the dead window."""
64+
_, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS)
65+
self.assertGreater(orbit, FIRST_ALIVE_ORBIT)
66+
67+
68+
if __name__ == "__main__":
69+
unittest.main()

0 commit comments

Comments
 (0)