feat(devices): keep the laser delay calibration in soft records on an IOC
This commit is contained in:
1 parent
f42c6baca6
commit
5284f55e9d
4 files changed
+210
-25
No files matched your search
@@ -10,9 +10,10 @@
|
||||
# instead of aborting unrelated scans. If the PVs are unreachable at startup, BEC disables only this
|
||||
# device (no I/O in __init__), the rest of the session loads.
|
||||
# Calibration values are NOT config keys: run dev.laser_delay.calibrate(...) after the laser restart
|
||||
# or the weekly scope check. They are kept across BEC restarts in a JSON file on the device server
|
||||
# host (calibration_file). dev.laser_delay.get_calibration_info() shows where, when and from which
|
||||
# scope value, and whether the restored calibration was verified.
|
||||
# or the weekly scope check. They are kept across BEC restarts either in soft records on an IOC
|
||||
# (calibration_pv_prefix, preferred once controls has added them) or in a JSON file on the device
|
||||
# server host (calibration_file); use one of the two. dev.laser_delay.get_calibration_info() shows
|
||||
# where, when and from which scope value, and whether the restored calibration was verified.
|
||||
|
||||
laser_delay:
|
||||
readoutPriority: baseline
|
||||
@@ -32,6 +33,7 @@ laser_delay:
|
||||
phase_path: script # "shortest" is opt-in, see LASER_DELAY.md
|
||||
fine_rollover: true # FINE 200 is written as the next coarse unit and 0 (open question)
|
||||
calibration_file: ~/.superxas_bec/laser_delay_calibration.json
|
||||
# calibration_pv_prefix: "X10DA-LAS-DELAY:" # once the soft records exist; remove calibration_file
|
||||
deviceTags:
|
||||
- laser
|
||||
- timing
|
||||
|
||||
@@ -49,10 +49,10 @@ from which scope value it was made, and whether it is verified:
|
||||
dev.laser_delay.get_calibration_info()
|
||||
# {'calibration': {...}, 'current_delay': 1.2, 'calibrated_at': '2026-10-07T19:09:28+00:00',
|
||||
# 'age_days': 0.1, 'scope_xray_minus_laser': -0.88, 'verified': True, 'unverified_reasons': [],
|
||||
# 'storage': '<home>/.superxas_bec/laser_delay_calibration.json'}
|
||||
# 'storage': 'PVs X10DA-LAS-DELAY:*'}
|
||||
```
|
||||
|
||||
If it cannot be saved (file not writable), `calibrate` raises an error
|
||||
If it cannot be saved (file not writable, calibration PVs not reachable), `calibrate` raises an error
|
||||
saying so. The calibration is active anyway, but would be lost at the next restart.
|
||||
|
||||
## 2. Move and scan
|
||||
@@ -177,12 +177,13 @@ phase shifter by hand.
|
||||
|
||||
## 4. After a BEC restart
|
||||
|
||||
The device keeps its calibration outside the device server, like the script's `Timing.offset`, in a
|
||||
JSON file on the device server host (config key `calibration_file`). `~` is the home of the account
|
||||
that runs the device server, so this only carries over while BEC always runs under the same account.
|
||||
Keeping it in autosaved soft records on an IOC instead, independent of the account, is on the branch
|
||||
`feat/laser-delay-calibration-pvs` (config key `calibration_pv_prefix`), once controls has added the
|
||||
records.
|
||||
The device keeps its calibration outside the device server, like the script's `Timing.offset`, in one
|
||||
of two places (config keys, use one):
|
||||
|
||||
- **`calibration_pv_prefix`** (preferred): soft records on an IOC, autosaved, independent of the
|
||||
account and the host BEC runs on. The records are listed below; they have to be added by controls.
|
||||
- **`calibration_file`**: a JSON file on the device server host. `~` is the home of the account that
|
||||
runs the device server, so this only carries over while BEC always runs under the same account.
|
||||
|
||||
It is written after every `calibrate` and `set_calibration`, and twice per move: before the first
|
||||
hardware write with a "move in progress" marker, and after the move without it. It holds the four
|
||||
@@ -203,7 +204,7 @@ When the device connects (device server start, config reload) it reads it back:
|
||||
- The current delay (the readback) comes back with a verified calibration. After a stopped move the
|
||||
store holds no delay, and the readback stays NaN until the next move.
|
||||
- A calibration older than 7 days is restored with a warning.
|
||||
- Nothing stored (no file) leaves the device uncalibrated; an unreadable file too, with
|
||||
- Nothing stored (no file, empty records) leaves the device uncalibrated; an unreadable store too, with
|
||||
an error in the log. The device still connects.
|
||||
|
||||
A move is refused, before any hardware write, when the "move in progress" marker cannot be saved:
|
||||
@@ -211,6 +212,27 @@ without it a later restore could trust a stale phase. Fix the store (see the dev
|
||||
move again. If only the save after the ramp fails, the error is logged and the marker stays set, so
|
||||
the next restart refuses moves until `calibrate`.
|
||||
|
||||
### Calibration records (for controls)
|
||||
|
||||
Prefix proposed for X10DA: `X10DA-LAS-DELAY:` (next to the old `X10DA-LAS-PHSH:`), on any IOC with
|
||||
autosave, for example the timing IOC. All records soft (no hardware link), `PINI` YES, all fields in
|
||||
autosave (`VAL`), no `DRVH`/`DRVL`:
|
||||
|
||||
| record | type | unit | content |
|
||||
|---|---|---|---|
|
||||
| `X10DA-LAS-DELAY:LASER-PERIOD` | `ao`, `PREC` 4 | ns | laser period at calibration |
|
||||
| `X10DA-LAS-DELAY:OFFSET-T` | `ao`, `PREC` 6 | ns | delay card offset |
|
||||
| `X10DA-LAS-DELAY:OFFSET-PHASE-T` | `ao`, `PREC` 6 | ns | phase shifter offset |
|
||||
| `X10DA-LAS-DELAY:PHASE-T` | `ao`, `PREC` 6 | ns | stored (unwrapped) phase, written twice per move |
|
||||
| `X10DA-LAS-DELAY:DELAY` | `ao`, `PREC` 4 | ns | current delay, NaN when unknown |
|
||||
| `X10DA-LAS-DELAY:CAL-SCOPE` | `ao`, `PREC` 4 | ns | scope reading of the last calibration, NaN after `set_calibration` |
|
||||
| `X10DA-LAS-DELAY:CAL-TIME` | `stringout` | | time of the last calibration, ISO 8601 UTC |
|
||||
| `X10DA-LAS-DELAY:MOVING` | `bo` (0/1) | | move in progress marker |
|
||||
|
||||
The device treats `LASER-PERIOD` 0 or NaN as "no calibration stored" (fresh records). Once they exist,
|
||||
set `calibration_pv_prefix: "X10DA-LAS-DELAY:"` and remove `calibration_file` in
|
||||
`x10da_laser_delay.yaml`. The script could read the same four numbers instead of `Timing.offset`.
|
||||
|
||||
`set_calibration` restores values by hand (for example from another file):
|
||||
|
||||
```python
|
||||
@@ -253,7 +275,8 @@ changed in the YAML file and a config reload.
|
||||
| `step_sleep` | 0.1 | seconds between phase steps, at least 0.1 (the tested value) |
|
||||
| `phase_path` | `script` | `script`: ramp the phase change the script computes. `shortest`: fold it into ±half a cavity period (section 6) |
|
||||
| `fine_rollover` | `true` | write a fine value of 200 as the next coarse unit and FINE 0 (same delay). `false`: write 200 like the script (open question 5) |
|
||||
| `calibration_file` | none (X10DA config for now: `~/.superxas_bec/laser_delay_calibration.json`) | JSON file that keeps the calibration across restarts (section 4); only changed by a config reload |
|
||||
| `calibration_pv_prefix` | none (X10DA: `X10DA-LAS-DELAY:` once the records exist) | soft records that keep the calibration across restarts (section 4); only changed by a config reload |
|
||||
| `calibration_file` | none (X10DA config for now: `~/.superxas_bec/laser_delay_calibration.json`) | JSON file that keeps the calibration instead (section 4); only changed by a config reload |
|
||||
| `delay_coarse_pv`, `delay_fine_pv`, `evr_period_pv`, `phase_v1_pv`, `phase_v2_pv` | see table above | PV names |
|
||||
| `limits` | none | optional `[low, high]` in ns, `low <= high` |
|
||||
|
||||
@@ -298,7 +321,7 @@ phase shifter `cavity_wraps` cavity periods further than usual, once.
|
||||
- Optional `phase_path: shortest` (section 6); the default is the script's path.
|
||||
- A fine value of 200 is written as the next coarse unit and 0 (`fine_rollover`, section 5).
|
||||
- `vmax` above 5 V is refused, and no voltage above 5 V is ever written.
|
||||
- The calibration is kept in a JSON file (section 4), restored automatically, and
|
||||
- The calibration is kept in soft records or a JSON file (section 4), restored automatically, and
|
||||
checked against the hardware before it is used.
|
||||
- Moves are refused when the phase shifter was turned outside BEC, its amplitude is wrong, or a
|
||||
restored calibration is not verified (section 2a). The script would move to a wrong delay.
|
||||
@@ -319,7 +342,7 @@ Answered on 7 Oct 2026 (via Jan, after the kickoff meeting):
|
||||
| 0.1 s per phase step | an artificial value, tested and known to be good | `step_sleep: 0.1` stays the default |
|
||||
| Shorter phase path | tested once, could be faster, not a priority: an advanced parameter | `phase_path: shortest` stays opt-in |
|
||||
| Maximum phase shifter voltage | hard limit 5 V | `vmax` is capped at 5 V |
|
||||
| Calibration after a BEC restart | redone about weekly, with the scope, after the laser is recalibrated | kept in a file and restored at startup, verified against the hardware (section 4) |
|
||||
| Calibration after a BEC restart | redone about weekly, with the scope, after the laser is recalibrated | kept in soft records (or a file) and restored at startup, verified against the hardware (section 4) |
|
||||
| FINE = 200 | not answered | rolled over into the next coarse unit (`fine_rollover: true`), still to confirm |
|
||||
|
||||
Still open:
|
||||
@@ -333,8 +356,7 @@ Still open:
|
||||
`phase_path: shortest` (section 6).
|
||||
3. **Vmax** for phase shifter 2: is 1.0 V right? The script has a comment that it may differ. Anything up
|
||||
to the 5 V hard limit is accepted.
|
||||
4. **Calibration records.** Controls to add the `X10DA-LAS-DELAY:` records; the device support for
|
||||
them (`calibration_pv_prefix`) is on the branch `feat/laser-delay-calibration-pvs`. Until then the
|
||||
4. **Calibration records.** Controls to add the `X10DA-LAS-DELAY:` records (section 4). Until then the
|
||||
calibration is kept in a file in the home of the account that runs the device server.
|
||||
|
||||
## 9. Simulation
|
||||
|
||||
@@ -35,8 +35,9 @@ Calibration (the script's ``CurrentSituation``, run after every laser restart) r
|
||||
period, the delay card and the phase shifter and combines them with the oscilloscope reading
|
||||
``s`` = X-ray minus laser: ``offset_t = -(2 DELAY + 0.01 FINE) - s`` and
|
||||
``offset_phase_t = -phase_t - (s mod cavity_period)``. The script kept these numbers in a file
|
||||
``Timing.offset``; here they are config signals on the device, kept across BEC restarts in a JSON
|
||||
file (``calibration_file``), see ``LASER_DELAY.md``.
|
||||
``Timing.offset``; here they are config signals on the device, kept across BEC restarts in soft
|
||||
records on an IOC (``calibration_pv_prefix``) or a JSON file (``calibration_file``), see
|
||||
``LASER_DELAY.md``.
|
||||
|
||||
The readback of the positioner is the last commanded delay. After ``calibrate(s)`` it is ``s``,
|
||||
because the model maps the current settings to ``t = s``. It is NaN before calibration, while a move
|
||||
@@ -61,7 +62,7 @@ from typing import TYPE_CHECKING, Callable
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import EpicsSignal, EpicsSignalRO
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO
|
||||
from ophyd import FormattedComponent as FCpt
|
||||
from ophyd import Kind, Signal
|
||||
from ophyd.positioner import PositionerBase
|
||||
@@ -464,6 +465,23 @@ def _phase_path(value):
|
||||
# --------------------------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class LaserDelayCalibrationPVs(Device):
|
||||
"""Soft records on an IOC that keep the laser delay calibration (autosaved).
|
||||
|
||||
One record per value, under a common prefix (e.g. ``X10DA-LAS-DELAY:``). The record list and
|
||||
types are in ``LASER_DELAY.md``. Used by ``LaserDelay`` when ``calibration_pv_prefix`` is set.
|
||||
"""
|
||||
|
||||
laser_period = Cpt(EpicsSignal, "LASER-PERIOD", kind=Kind.config)
|
||||
offset_t = Cpt(EpicsSignal, "OFFSET-T", kind=Kind.config)
|
||||
offset_phase_t = Cpt(EpicsSignal, "OFFSET-PHASE-T", kind=Kind.config)
|
||||
phase_t = Cpt(EpicsSignal, "PHASE-T", kind=Kind.config)
|
||||
current_delay = Cpt(EpicsSignal, "DELAY", kind=Kind.config)
|
||||
scope = Cpt(EpicsSignal, "CAL-SCOPE", kind=Kind.config)
|
||||
calibrated_at = Cpt(EpicsSignal, "CAL-TIME", string=True, kind=Kind.config)
|
||||
moving = Cpt(EpicsSignal, "MOVING", kind=Kind.config)
|
||||
|
||||
|
||||
class _FileCalibrationStore:
|
||||
"""Calibration kept in a JSON file on the device server host (like ``Timing.offset``)."""
|
||||
|
||||
@@ -497,6 +515,60 @@ class _FileCalibrationStore:
|
||||
"""Nothing to release."""
|
||||
|
||||
|
||||
class _PVCalibrationStore:
|
||||
"""Calibration kept in soft records on an IOC (``LaserDelayCalibrationPVs``)."""
|
||||
|
||||
CONNECT_TIMEOUT = 5.0
|
||||
|
||||
def __init__(self, prefix: str, name: str):
|
||||
self.prefix = prefix
|
||||
self.pvs = LaserDelayCalibrationPVs(prefix, name=f"{name}_calibration_pvs")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"PVs {self.prefix}*"
|
||||
|
||||
def load(self) -> dict | None:
|
||||
"""Content of the records, or None if they hold no calibration (laser period 0 or NaN)."""
|
||||
self.pvs.wait_for_connection(timeout=self.CONNECT_TIMEOUT)
|
||||
laser_period = float(self.pvs.laser_period.get())
|
||||
if not math.isfinite(laser_period) or laser_period <= 0:
|
||||
return None
|
||||
delay = float(self.pvs.current_delay.get())
|
||||
scope = float(self.pvs.scope.get())
|
||||
return {
|
||||
"calibration": {
|
||||
"laser_period": laser_period,
|
||||
"offset_t": float(self.pvs.offset_t.get()),
|
||||
"offset_phase_t": float(self.pvs.offset_phase_t.get()),
|
||||
"phase_t": float(self.pvs.phase_t.get()),
|
||||
},
|
||||
"current_delay": delay if math.isfinite(delay) else None,
|
||||
"scope_xray_minus_laser": scope if math.isfinite(scope) else None,
|
||||
"calibrated_at": str(self.pvs.calibrated_at.get()) or None,
|
||||
"move_in_progress": bool(int(self.pvs.moving.get())),
|
||||
}
|
||||
|
||||
def save(self, content: dict) -> None:
|
||||
"""Write the records. The move marker goes first when set and last when cleared."""
|
||||
moving = 1 if content.get("move_in_progress") else 0
|
||||
if moving:
|
||||
self.pvs.moving.put(moving, wait=True)
|
||||
calibration = content["calibration"]
|
||||
for key in ("laser_period", "offset_t", "offset_phase_t", "phase_t"):
|
||||
getattr(self.pvs, key).put(float(calibration[key]), wait=True)
|
||||
delay = content.get("current_delay")
|
||||
self.pvs.current_delay.put(math.nan if delay is None else float(delay), wait=True)
|
||||
scope = content.get("scope_xray_minus_laser")
|
||||
self.pvs.scope.put(math.nan if scope is None else float(scope), wait=True)
|
||||
self.pvs.calibrated_at.put(content.get("calibrated_at") or "", wait=True)
|
||||
if not moving:
|
||||
self.pvs.moving.put(moving, wait=True)
|
||||
|
||||
def destroy(self) -> None:
|
||||
"""Release the channel access connections."""
|
||||
self.pvs.destroy()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------- #
|
||||
# Device #
|
||||
# --------------------------------------------------------------------------------------------- #
|
||||
@@ -539,7 +611,10 @@ class LaserDelay(PSIDeviceBase, PositionerBase):
|
||||
fine_rollover (bool): write a fine value of 200 as the next coarse unit and 0 (default).
|
||||
False writes 200 like the script.
|
||||
calibration_file (str | None): JSON file on the device server host that keeps the
|
||||
calibration across BEC restarts. None: the calibration is lost at every restart.
|
||||
calibration across BEC restarts.
|
||||
calibration_pv_prefix (str | None): prefix of the soft records that keep the calibration
|
||||
(``LaserDelayCalibrationPVs``). Use this or ``calibration_file``, not both. None for
|
||||
both: the calibration is lost at every restart.
|
||||
limits (list[float] | None): optional soft limits ``[low, high]`` in ns.
|
||||
"""
|
||||
|
||||
@@ -658,6 +733,7 @@ class LaserDelay(PSIDeviceBase, PositionerBase):
|
||||
phase_v1_pv = _ConstructorOnly()
|
||||
phase_v2_pv = _ConstructorOnly()
|
||||
calibration_file = _ConstructorOnly()
|
||||
calibration_pv_prefix = _ConstructorOnly()
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-locals
|
||||
def __init__(
|
||||
@@ -678,17 +754,23 @@ class LaserDelay(PSIDeviceBase, PositionerBase):
|
||||
phase_path: str = "script",
|
||||
fine_rollover: bool = True,
|
||||
calibration_file: str | None = None,
|
||||
calibration_pv_prefix: str | None = None,
|
||||
limits: list[float] | tuple[float, float] | None = None,
|
||||
scan_info: ScanInfo | None = None,
|
||||
device_manager: DeviceManagerBase | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
if calibration_file and calibration_pv_prefix:
|
||||
raise ValueError(
|
||||
f"{name}: give either calibration_file or calibration_pv_prefix, not both"
|
||||
)
|
||||
self._delay_coarse_pv = delay_coarse_pv
|
||||
self._delay_fine_pv = delay_fine_pv
|
||||
self._evr_period_pv = evr_period_pv
|
||||
self._phase_v1_pv = phase_v1_pv
|
||||
self._phase_v2_pv = phase_v2_pv
|
||||
self._calibration_file = calibration_file
|
||||
self._calibration_pv_prefix = calibration_pv_prefix
|
||||
self._move_lock = threading.RLock() # guards _move_status and _move_stop
|
||||
self._hw_lock = threading.Lock() # held by the move task and by calibrate/set_calibration
|
||||
self._hw_permit = threading.local() # per-thread permission to write the hardware signals
|
||||
@@ -701,8 +783,10 @@ class LaserDelay(PSIDeviceBase, PositionerBase):
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self._position = math.nan
|
||||
self._store: _FileCalibrationStore | None = None
|
||||
if calibration_file:
|
||||
self._store: _FileCalibrationStore | _PVCalibrationStore | None = None
|
||||
if calibration_pv_prefix:
|
||||
self._store = _PVCalibrationStore(calibration_pv_prefix, self.name)
|
||||
elif calibration_file:
|
||||
self._store = _FileCalibrationStore(calibration_file)
|
||||
# Constants: validated by CheckedSignal.check_value, a bad value fails here.
|
||||
self.rf_period.put(float(rf_period))
|
||||
@@ -819,7 +903,7 @@ class LaserDelay(PSIDeviceBase, PositionerBase):
|
||||
Called after the device is connected and its signals are connected.
|
||||
Default values for signals should be set here.
|
||||
"""
|
||||
# Restore the calibration from the store (the file), see _load_calibration. Never
|
||||
# Restore the calibration from the store (file or PVs), see _load_calibration. Never
|
||||
# raises: the device connects uncalibrated if the restore fails.
|
||||
try:
|
||||
self._load_calibration()
|
||||
@@ -995,7 +1079,7 @@ class LaserDelay(PSIDeviceBase, PositionerBase):
|
||||
(UTC, ISO 8601), ``age_days``, ``scope_xray_minus_laser`` (None after
|
||||
``set_calibration``), ``verified`` (False after a restore that could not be checked
|
||||
against the hardware: moves are refused), ``unverified_reasons`` and ``storage`` (the
|
||||
file that keeps it across restarts, None if not kept).
|
||||
file or PV prefix that keeps it across restarts, None if not kept).
|
||||
"""
|
||||
calibrated_at = self._calibration_meta.get("calibrated_at")
|
||||
age_days = None
|
||||
|
||||
@@ -139,6 +139,7 @@ def test_default_pvs_constants_and_positioner_type(laser_delay):
|
||||
}
|
||||
assert config["laser_delay_fine_rollover"]["value"] is True
|
||||
assert laser_delay.calibration_file is None
|
||||
assert laser_delay.calibration_pv_prefix is None
|
||||
assert isinstance(laser_delay, BECPositionerProtocol)
|
||||
assert isinstance(laser_delay, BECDeviceProtocol)
|
||||
|
||||
@@ -194,6 +195,7 @@ def test_config_overrides_pvs_and_constants():
|
||||
[
|
||||
{"steps_per_rf_period": 35},
|
||||
{"vmax": 0.0},
|
||||
{"calibration_file": "/tmp/x.json", "calibration_pv_prefix": "X10DA-LAS-DELAY:"},
|
||||
{"phase_path": "fast"},
|
||||
{"steps_per_rf_period": 0},
|
||||
{"steps_per_rf_period": 2.5},
|
||||
@@ -1485,3 +1487,78 @@ def test_clamped_phase_voltage_fails_the_move_and_refuses_the_next(calibrated):
|
||||
assert not calibrated.get_calibration_info()["verified"]
|
||||
with pytest.raises(LaserDelayNotCalibratedError):
|
||||
calibrated.set(2.0)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------- #
|
||||
# Calibration kept in PVs #
|
||||
# --------------------------------------------------------------------------------------------- #
|
||||
|
||||
PV_PREFIX = "X10DA-LAS-DELAY:"
|
||||
|
||||
|
||||
def store_values(device) -> dict:
|
||||
pvs = device._store.pvs
|
||||
return {sig: getattr(pvs, sig)._read_pv.mock_data for sig in pvs.component_names}
|
||||
|
||||
|
||||
def test_calibration_pvs_are_written_and_restored():
|
||||
with patched_device(
|
||||
LaserDelay, name="laser_delay", step_sleep=0.0, calibration_pv_prefix=PV_PREFIX
|
||||
) as device:
|
||||
set_hardware(device)
|
||||
device.calibrate(SCOPE)
|
||||
device.set(1.5).wait(timeout=10)
|
||||
before, hardware, saved = (
|
||||
device.get_calibration(),
|
||||
hardware_of(device),
|
||||
store_values(device),
|
||||
)
|
||||
assert device._store.pvs.laser_period.pvname == f"{PV_PREFIX}LASER-PERIOD"
|
||||
assert saved["phase_t"] == before["phase_t"]
|
||||
assert saved["current_delay"] == 1.5
|
||||
assert saved["moving"] == 0
|
||||
assert device.get_calibration_info()["storage"] == f"PVs {PV_PREFIX}*"
|
||||
with patched_device(
|
||||
LaserDelay, name="laser_delay", step_sleep=0.0, calibration_pv_prefix=PV_PREFIX
|
||||
) as device:
|
||||
set_hardware(device, **hardware)
|
||||
for sig, value in saved.items():
|
||||
getattr(device._store.pvs, sig)._read_pv.mock_data = value
|
||||
device.on_connected()
|
||||
assert device.get_calibration() == before
|
||||
assert device.read()["laser_delay"]["value"] == 1.5
|
||||
assert device.get_calibration_info()["verified"]
|
||||
|
||||
|
||||
def test_empty_calibration_pvs_leave_the_device_uncalibrated():
|
||||
with patched_device(
|
||||
LaserDelay, name="laser_delay", step_sleep=0.0, calibration_pv_prefix=PV_PREFIX
|
||||
) as device:
|
||||
set_hardware(device)
|
||||
device._store.pvs.laser_period._read_pv.mock_data = 0.0 # fresh records, no autosave
|
||||
device.on_connected()
|
||||
with pytest.raises(LaserDelayNotCalibratedError, match="not calibrated"):
|
||||
device.set(1.0)
|
||||
|
||||
|
||||
def test_calibration_pv_marker_is_set_first_and_cleared_last():
|
||||
with patched_device(
|
||||
LaserDelay, name="laser_delay", step_sleep=0.0, calibration_pv_prefix=PV_PREFIX
|
||||
) as device:
|
||||
set_hardware(device)
|
||||
device.calibrate(SCOPE)
|
||||
order = []
|
||||
for sig in device._store.pvs.component_names:
|
||||
pv = getattr(device._store.pvs, sig)._write_pv
|
||||
original = pv.put
|
||||
|
||||
def recorded(value, *args, _sig=sig, _original=original, **kwargs):
|
||||
order.append((_sig, value))
|
||||
return _original(value, *args, **kwargs)
|
||||
|
||||
pv.put = recorded
|
||||
device.set(1.0).wait(timeout=10)
|
||||
names = [sig for sig, _ in order]
|
||||
assert order[0] == ("moving", 1)
|
||||
assert order[-1] == ("moving", 0)
|
||||
assert names.count("phase_t") == 2
|
||||
Reference in new issue
Block a user