chore(debug): add Serval sequence replay script for the TDC loss investigation
CI for superxas_bec / test (push) Failing after 40s
CI for superxas_bec / test (push) Failing after 40s
This commit is contained in:
1 parent
2db81faa12
commit
2e93c4c0a8
2 files changed
+242
No files matched your search
@@ -0,0 +1,22 @@
|
||||
# Debug code: TimePix TDC loss investigation
|
||||
|
||||
**Not production code.** Temporary tools for finding why the second TimePix board loses its TDC
|
||||
during BEC scans (October 2026). Nothing in `superxas_bec` imports these files; delete this
|
||||
folder once the issue is resolved.
|
||||
|
||||
## `serval_bec_sequence.py`
|
||||
|
||||
Replays, directly against Serval, the HTTP sequence that BEC + the ADASItpx 2.2 IOC send for a
|
||||
`timepix_scan`, and prints Serval's TDC1/TDC2 rates per point. Needs only Python 3 on the Serval
|
||||
machine (p6-0008); BEC, the IOC and Accos must not use the detector while it runs. Restart
|
||||
Serval before each run.
|
||||
|
||||
```bash
|
||||
python3 serval_bec_sequence.py --dry-run # print requests only
|
||||
python3 serval_bec_sequence.py --points 40 --no-tdc2 # BEC sequence, TDC1 only
|
||||
python3 serval_bec_sequence.py --points 40 --no-tdc2 --configure-once # config once, then start only
|
||||
python3 serval_bec_sequence.py --points 40 --no-tdc2 --software-trigger # one measurement, soft triggers
|
||||
```
|
||||
|
||||
`python3 serval_bec_sequence.py --help` lists all switches (`--chain-mode`, `--one-measurement`,
|
||||
`--raw-base` for recording raw data, ...).
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replay the Serval HTTP sequence that BEC + the ADASItpx 2.2 IOC send for a timepix_scan.
|
||||
|
||||
No BEC, IOC or tpx3app needed: only Python 3 (stdlib) and a running Serval.
|
||||
|
||||
Per scan point the IOC (asiTpx.cpp startMeasurement, release 2.2) does:
|
||||
1. GET /detector/config, overwrite its own fields, PUT the whole config back
|
||||
(Tdc = ["P0123", "P0123"] because BEC enables TDC1 and TDC2, rising edge, all channels)
|
||||
2. GET /server/destination, set Raw[0] (and Image), PUT it back
|
||||
3. GET /measurement/start (nTriggers = 1, so Serval ends the measurement after one frame)
|
||||
BEC waits for the frame and starts the next point the same way.
|
||||
|
||||
The script prints Serval's Tdc1/Tdc2 event rates per point. On this detector
|
||||
(2 boards x 4 chips) TDC1 at ~130 kHz reads ~1.04 MHz when all 8 chips see it and
|
||||
~520 kHz when one board loses it.
|
||||
|
||||
Switches to find which part of the sequence triggers the loss:
|
||||
--tdc2 / --no-tdc2 TDC2 "P0123" (BEC) or "" (Accos)
|
||||
--merge / --no-merge re-send Serval's full config (IOC 2.2) or only the IOC fields (IOC <= 2.1)
|
||||
--chain-mode NONE what the IOC sent before release 2.2 (BEC scans worked then)
|
||||
--one-measurement one measurement with nTriggers = points (Accos style) instead of one per point
|
||||
--configure-once send config + destination only before the first point, then only
|
||||
GET /measurement/start per point (no config back and forth)
|
||||
--software-trigger one measurement (SOFTWARESTART_TIMERSTOP, nTriggers = points), then
|
||||
GET /measurement/trigger/start per point (IOC: TriggerMode Software +
|
||||
cam1:TriggerSoftware); config is sent once
|
||||
--raw-base raw output, e.g. file:///data/SavedStream/ or tcp://connect@127.0.0.1:8451
|
||||
(a tcp target needs a listener; omit for no raw output)
|
||||
|
||||
Examples:
|
||||
python3 serval_bec_sequence.py --dry-run
|
||||
python3 serval_bec_sequence.py --points 40 --raw-base file:///data/SavedStream/ --raw-pattern bec_seq_%Hms_
|
||||
python3 serval_bec_sequence.py --points 40 --no-tdc2
|
||||
python3 serval_bec_sequence.py --points 40 --one-measurement
|
||||
python3 serval_bec_sequence.py --points 40 --configure-once
|
||||
python3 serval_bec_sequence.py --points 40 --software-trigger
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
class Serval:
|
||||
def __init__(self, url: str, dry_run: bool):
|
||||
self.url = url.rstrip("/")
|
||||
self.dry_run = dry_run
|
||||
|
||||
def _request(self, method: str, path: str, body: dict | None = None):
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
stamp = time.strftime("%H:%M:%S")
|
||||
print(f"{stamp} {method} {path}" + (f" {json.dumps(body)}" if body is not None else ""))
|
||||
if self.dry_run:
|
||||
return None
|
||||
req = urllib.request.Request(self.url + path, data=data, method=method)
|
||||
if data is not None:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode()
|
||||
except urllib.error.HTTPError as exc:
|
||||
sys.exit(f"{method} {path} failed: {exc.code} {exc.read().decode()}")
|
||||
try:
|
||||
return json.loads(text) if text else None
|
||||
except json.JSONDecodeError:
|
||||
return text
|
||||
|
||||
def get(self, path: str):
|
||||
return self._request("GET", path)
|
||||
|
||||
def put(self, path: str, body: dict):
|
||||
return self._request("PUT", path, body)
|
||||
|
||||
def dashboard(self) -> dict:
|
||||
if self.dry_run:
|
||||
return {"Measurement": {"Status": "DA_IDLE"}}
|
||||
req = urllib.request.Request(self.url + "/dashboard")
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
|
||||
def configure_detector(
|
||||
serval: Serval, args, n_triggers: int, trigger_mode: str = "AUTOTRIGSTART_TIMERSTOP"
|
||||
) -> None:
|
||||
config = (serval.get("/detector/config") or {}) if args.merge else {}
|
||||
config["ExposureTime"] = args.exposure
|
||||
config["BiasEnabled"] = 1 # the IOC sends an int, not a JSON boolean
|
||||
config["BiasVoltage"] = args.bias
|
||||
config["ChainMode"] = args.chain_mode
|
||||
config["TriggerPeriod"] = args.period
|
||||
config["nTriggers"] = n_triggers
|
||||
config["TriggerDelay"] = 0.0
|
||||
config["TriggerIn"] = 0
|
||||
config["TriggerOut"] = 0
|
||||
config["TriggerMode"] = trigger_mode
|
||||
config["Tdc"] = ["P0123", "P0123" if args.tdc2 else ""]
|
||||
serval.put("/detector/config", config)
|
||||
|
||||
|
||||
def configure_destination(serval: Serval, args) -> None:
|
||||
destination = (serval.get("/server/destination") or {}) if args.merge else {}
|
||||
if args.raw_base:
|
||||
raw = (destination.get("Raw") or [{}])[0]
|
||||
raw["Base"] = args.raw_base
|
||||
raw["FilePattern"] = args.raw_pattern
|
||||
if args.raw_base.startswith("tcp://"):
|
||||
raw.pop("SplitStrategy", None)
|
||||
destination["Raw"] = [raw]
|
||||
else:
|
||||
destination["Raw"] = None
|
||||
destination["Image"] = []
|
||||
destination["Preview"] = None
|
||||
serval.put("/server/destination", destination)
|
||||
|
||||
|
||||
def wait_for_measurement(serval: Serval, timeout: float) -> tuple[float, float, int]:
|
||||
"""Poll /dashboard until DA_IDLE; return max Tdc1/Tdc2 rates and dropped frames."""
|
||||
tdc1 = tdc2 = 0.0
|
||||
dropped = 0
|
||||
deadline = time.monotonic() + timeout
|
||||
time.sleep(0.5)
|
||||
while time.monotonic() < deadline:
|
||||
meas = serval.dashboard().get("Measurement") or {}
|
||||
tdc1 = max(tdc1, meas.get("Tdc1EventRate") or 0)
|
||||
tdc2 = max(tdc2, meas.get("Tdc2EventRate") or 0)
|
||||
dropped = meas.get("DroppedFrames") or 0
|
||||
if meas.get("Status") == "DA_IDLE":
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print(" measurement did not finish in time")
|
||||
return tdc1, tdc2, dropped
|
||||
|
||||
|
||||
def report(tdc1: float, tdc2: float, dropped: int) -> None:
|
||||
full = 1.0e6 # rough "all 8 chips" TDC1 rate at the beamline
|
||||
flag = " <-- below ~1 MHz, a board may have lost the TDC" if 0 < tdc1 < 0.75 * full else ""
|
||||
print(f" Tdc1EventRate {tdc1:.0f} Tdc2EventRate {tdc2:.0f} DroppedFrames {dropped}{flag}")
|
||||
|
||||
|
||||
def wait_for_frame(serval: Serval, frames: int, timeout: float) -> tuple[float, float, int]:
|
||||
"""Poll /dashboard until FrameCount reaches frames; return max Tdc1/Tdc2 rates and drops."""
|
||||
tdc1 = tdc2 = 0.0
|
||||
dropped = 0
|
||||
deadline = time.monotonic() + timeout
|
||||
time.sleep(0.5)
|
||||
while time.monotonic() < deadline:
|
||||
meas = serval.dashboard().get("Measurement") or {}
|
||||
tdc1 = max(tdc1, meas.get("Tdc1EventRate") or 0)
|
||||
tdc2 = max(tdc2, meas.get("Tdc2EventRate") or 0)
|
||||
dropped = meas.get("DroppedFrames") or 0
|
||||
if serval.dry_run or (meas.get("FrameCount") or 0) >= frames:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print(" frame did not arrive in time")
|
||||
return tdc1, tdc2, dropped
|
||||
|
||||
|
||||
def run_software_trigger(serval: Serval, args) -> None:
|
||||
"""One measurement, config sent once, one software trigger per point."""
|
||||
configure_detector(serval, args, args.points, trigger_mode="SOFTWARESTART_TIMERSTOP")
|
||||
configure_destination(serval, args)
|
||||
serval.get("/measurement/start")
|
||||
for point in range(1, args.points + 1):
|
||||
print(f"--- point {point}/{args.points}")
|
||||
serval.get("/measurement/trigger/start")
|
||||
report(*wait_for_frame(serval, point, args.period + 30))
|
||||
if point < args.points:
|
||||
time.sleep(args.pause)
|
||||
serval.get("/measurement/stop")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--url", default="http://localhost:8080")
|
||||
parser.add_argument("--points", type=int, default=40)
|
||||
parser.add_argument("--exposure", type=float, default=9.9979, help="s, BEC uses exp_time - 2.1 ms")
|
||||
parser.add_argument("--period", type=float, default=10.0, help="s")
|
||||
parser.add_argument("--bias", type=float, default=100.0)
|
||||
parser.add_argument("--chain-mode", default="LEADER", choices=["LEADER", "NONE"],
|
||||
help="LEADER = IOC 2.2 and Accos; NONE = what the IOC sent before 2.2")
|
||||
parser.add_argument("--tdc2", action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--merge", action=argparse.BooleanOptionalAction, default=True)
|
||||
mode = parser.add_mutually_exclusive_group()
|
||||
mode.add_argument("--one-measurement", action="store_true")
|
||||
mode.add_argument("--configure-once", action="store_true")
|
||||
mode.add_argument("--software-trigger", action="store_true")
|
||||
parser.add_argument("--raw-base", default="")
|
||||
parser.add_argument("--raw-pattern", default="")
|
||||
parser.add_argument("--pause", type=float, default=1.0, help="s between points (BEC: ~1 s)")
|
||||
parser.add_argument("--dry-run", action="store_true", help="print the requests only")
|
||||
args = parser.parse_args()
|
||||
|
||||
serval = Serval(args.url, args.dry_run)
|
||||
if args.software_trigger:
|
||||
run_software_trigger(serval, args)
|
||||
return
|
||||
runs = 1 if args.one_measurement else args.points
|
||||
n_triggers = args.points if args.one_measurement else 1
|
||||
timeout = n_triggers * args.period + 30
|
||||
|
||||
for run in range(1, runs + 1):
|
||||
print(f"--- measurement {run}/{runs}")
|
||||
if run == 1 or not args.configure_once:
|
||||
configure_detector(serval, args, n_triggers)
|
||||
configure_destination(serval, args)
|
||||
serval.get("/measurement/start")
|
||||
report(*wait_for_measurement(serval, timeout))
|
||||
if run < runs:
|
||||
time.sleep(args.pause)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user