From 9fac07662fb2ba2cfefed7e9a76eb8881dbe2463 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Tue, 27 Oct 2026 18:12:41 +0100 Subject: [PATCH 01/52] feat(aerotech): A3200 controller and motor for the SAXS-TT Aerotech stages Npaq/A3200 via the ASCII command interface on the A3200 Windows PC: enable, home (HOMEASYNC), MOVEABS, abort, fault ack, ~STATUS readback. Simulated controller (sim_a3200), device configs for hardware and simulation, tests. Commissioning checklist in devices/aerotech/README.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/device_configs/sastt_aerotech.yaml | 91 +++ .../simulated_sastt_aerotech.yaml | 86 +++ csaxs_bec/devices/aerotech/README.md | 47 ++ csaxs_bec/devices/aerotech/__init__.py | 0 csaxs_bec/devices/aerotech/a3200.py | 570 ++++++++++++++++++ csaxs_bec/devices/sim/sim_a3200.py | 298 +++++++++ tests/tests_devices/test_a3200.py | 207 +++++++ 7 files changed, 1299 insertions(+) create mode 100644 csaxs_bec/device_configs/sastt_aerotech.yaml create mode 100644 csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml create mode 100644 csaxs_bec/devices/aerotech/README.md create mode 100644 csaxs_bec/devices/aerotech/__init__.py create mode 100644 csaxs_bec/devices/aerotech/a3200.py create mode 100644 csaxs_bec/devices/sim/sim_a3200.py create mode 100644 tests/tests_devices/test_a3200.py diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml new file mode 100644 index 00000000..dc5b4f88 --- /dev/null +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -0,0 +1,91 @@ +############################################################ +# SAXS-TT Aerotech stages: Npaq-B-40B-40B/ULTRA, A3200 software on a Windows PC +# (FireWire to the Npaq). BEC connects to the A3200 ASCII command interface on that PC. +# A3200 setup: CommandSetup = 0x00010004 (enabled + Always Send EOS), CommandPort = 8000. +# +# TODO commissioning: host name of the A3200 PC, A3200 axis names, velocities, limits. +# UU (ADRS-150 slipring follower) follows U on the controller and is not a BEC device. +# Limits [0, 0] = no BEC soft limits; the controller's SoftwareLimitLow/High still apply. +############################################################ + +aerox: + description: Aerotech X, ALS25020 linear stage + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: X + axis_nr: 0 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 1.0 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroy: + description: Aerotech Y, AVSI125 vertical lift + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: Y + axis_nr: 1 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 0.5 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroz: + description: Aerotech Z, ATS20010 linear stage + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: Z + axis_nr: 2 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 1.0 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aerorot: + description: Aerotech U, ABRT-200 rotation stage (UU follows on the controller) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: U + axis_nr: 3 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 10.0 + egu: deg + tolerance: 0.001 + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml new file mode 100644 index 00000000..b89ffc68 --- /dev/null +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -0,0 +1,86 @@ +############################################################ +# Simulated SAXS-TT Aerotech stages (sim_a3200), same devices as sastt_aerotech.yaml. +# Axes start enabled and homed (sim_enabled / sim_homed in deviceConfig to change). +############################################################ + +aerox: + description: Aerotech X, ALS25020 linear stage + deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor + deviceConfig: + axis_Id: X + axis_nr: 0 + host: sim-a3200-saxstt + port: 8000 + velocity: 1.0 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroy: + description: Aerotech Y, AVSI125 vertical lift + deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor + deviceConfig: + axis_Id: Y + axis_nr: 1 + host: sim-a3200-saxstt + port: 8000 + velocity: 0.5 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroz: + description: Aerotech Z, ATS20010 linear stage + deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor + deviceConfig: + axis_Id: Z + axis_nr: 2 + host: sim-a3200-saxstt + port: 8000 + velocity: 1.0 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aerorot: + description: Aerotech U, ABRT-200 rotation stage (UU follows on the controller) + deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor + deviceConfig: + axis_Id: U + axis_nr: 3 + host: sim-a3200-saxstt + port: 8000 + velocity: 10.0 + egu: deg + tolerance: 0.001 + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md new file mode 100644 index 00000000..e1aa544d --- /dev/null +++ b/csaxs_bec/devices/aerotech/README.md @@ -0,0 +1,47 @@ +# Aerotech A3200 (SAXS-TT) + +Hardware: Npaq-B-40B-40B/ULTRA drive rack, commanded over FireWire by the A3200 software +(SMC) on a Windows PC. The Npaq Ethernet port is for Modbus I/O only, so BEC talks to the +A3200 ASCII command interface on the Windows PC (TCP, default port 8000). + +| BEC device | A3200 axis | Stage | Type | +|------------|------------|----------|------------------------------------------------| +| aerox | X | ALS25020 | linear | +| aeroy | Y | AVSI125 | vertical lift, 25 mm | +| aeroz | Z | ATS20010 | linear | +| aerorot | U | ABRT-200 | rotation | +| - | UU | ADRS-150 | slipring follower of U, handled on the controller | + +Configs: `device_configs/sastt_aerotech.yaml` (hardware), +`device_configs/simulated_omny/simulated_sastt_aerotech.yaml` (simulation, `sim_a3200.py`). + +Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface"), copy at +`~/gitea/SAXS-TT_docs` on the workstation. + +## Protocol + +- one AeroBasic line per command, terminated by `\n` +- reply: `%` (+ data) success, `!` invalid syntax, `#` task fault (`~LASTERROR` gives the text) +- `~STATUS (X, ProgramPositionFeedback) (X, AxisStatus) ...` reads several items at once +- only asynchronous motion commands are used (`MOVEABS`, `HOMEASYNC`, `ABORT`); blocking + commands like `HOME` would block the socket until they finish +- motion commands are never retried; status reads are retried once + +## Commissioning checklist + +1. A3200 runs on the Windows PC, all axes enable and move in Motion Composer. +2. Export the parameter file (`.prme`) and keep a copy. +3. Configuration Manager: `CommandSetup = 0x00010004` (ASCII interface enabled + + Always Send EOS), `CommandPort = 8000`, reset the controller. +4. Windows firewall: allow inbound TCP 8000 from the BEC host. +5. Check with `nc 8000`, send `~VERSION`, expect `%6.04...`. +6. Set host, axis names, velocities and limits in `sastt_aerotech.yaml`. +7. Check `ProgramPositionFeedback` matches the position shown in Motion Composer. +8. Check how UU follows U (gantry, slave or program) before rotating. + +## Next: continuous line scan + +Fast axis moving continuously, slow axis stepping between lines, start of each line +triggered by position with PSO (position synchronized output) on the fast axis: +`PSOCONTROL`, `PSOWINDOW`/`PSODISTANCE`, `PSOOUTPUT` drive the digital output when the +armed position is crossed. To be implemented once the hardware is reachable. diff --git a/csaxs_bec/devices/aerotech/__init__.py b/csaxs_bec/devices/aerotech/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py new file mode 100644 index 00000000..be51d51d --- /dev/null +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -0,0 +1,570 @@ +""" +Aerotech A3200 motion controller (Npaq drive rack) via the A3200 ASCII command interface. + +The Npaq is commanded over FireWire by the A3200 software (SMC) running on a Windows PC. +BEC talks to that PC through the ASCII command interface, a line-based TCP protocol: + +- a command is an AeroBasic line (e.g. ``MOVEABS X 5 10``) or a server command (``~STATUS ...``) +- the reply starts with one character: ``%`` success, ``!`` invalid syntax, ``#`` task fault +- data follows the success character (``%1.234``), the reply is terminated by EOS (``\\n``) + +Required controller configuration (Configuration Manager, then reset the controller): + +- CommandSetup = 0x00010004 (ASCII interface enabled + "Always Send EOS") +- CommandPort = 8000 (default used here) +- the default command characters: EOS ``\\n``, success ``%``, invalid ``!``, fault ``#`` + +Blocking AeroBasic commands (HOME, WAIT ...) block the socket until they finish, so only +asynchronous commands are used for motion (MOVEABS, HOMEASYNC, ABORT). +""" + +import threading +import time + +import numpy as np +from bec_lib import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, PositionerBase, Signal +from ophyd.status import wait as status_wait +from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices.utils.controller import Controller, retry_once, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected +from prettytable import PrettyTable + +logger = bec_logger.logger + +EOS = "\n" +ACK_CHAR = "%" +NAK_CHAR = "!" +FAULT_CHAR = "#" + +# Axis Status bits (A3200 help: Motion Composer Suite > System Diagnostics > Axis Status) +AXISSTATUS_HOMED = 1 << 0 +AXISSTATUS_PROFILING = 1 << 1 +AXISSTATUS_HOMING = 1 << 4 +AXISSTATUS_ENABLING = 1 << 5 +AXISSTATUS_JOGGING = 1 << 8 +AXISSTATUS_DRIVE_PENDING = 1 << 9 +AXISSTATUS_MOVE_DONE = 1 << 22 + +AXISSTATUS_BUSY_MASK = ( + AXISSTATUS_PROFILING + | AXISSTATUS_HOMING + | AXISSTATUS_ENABLING + | AXISSTATUS_JOGGING + | AXISSTATUS_DRIVE_PENDING +) + +# Drive Status bits +DRIVESTATUS_ENABLED = 1 << 0 +DRIVESTATUS_CW_EOT_LIMIT = 1 << 1 +DRIVESTATUS_CCW_EOT_LIMIT = 1 << 2 +DRIVESTATUS_HOME_LIMIT = 1 << 3 + +# Axis Fault bits +AXIS_FAULT_NAMES = { + 0: "PositionError", + 1: "OverCurrent", + 2: "CwEOTLimit", + 3: "CcwEOTLimit", + 4: "CwSoftLimit", + 5: "CcwSoftLimit", + 6: "AmplifierFault", + 7: "PositionFbk", + 8: "VelocityFbk", + 9: "HallFault", + 10: "MaxVelocity", + 11: "EstopFault", + 12: "VelocityError", + 14: "ProbeFault", + 15: "ExternalFault", + 17: "MotorTemp", + 18: "AmplifierTemp", + 19: "EncoderFault", + 20: "CommLost", + 22: "GantryMisalign", + 23: "FbkScalingFault", + 24: "MrkSearchFault", + 25: "SafeZoneFault", + 26: "InPosTimeout", + 27: "VoltageClamp", + 28: "PowerSupply", + 29: "MissedInterrupt", + 30: "Internal", +} + + +def decode_axis_fault(fault: int) -> list[str]: + """Return the names of all fault bits set in an Axis Fault word.""" + return [name for bit, name in AXIS_FAULT_NAMES.items() if fault & (1 << bit)] + + +class A3200CommunicationError(Exception): + pass + + +class A3200Error(Exception): + pass + + +class A3200Controller(Controller): + """ + Controller for an Aerotech A3200 system, one instance per (host, port). + + Axes are addressed by their A3200 axis name (e.g. "X", "Y", "U"). The axis_nr given to + set_axis is only the slot in the controller's axis list. + """ + + _axes_per_controller = 32 + USER_ACCESS = [ + "describe", + "socket_put_and_receive", + "command", + "get_version", + "enable_axis", + "disable_axis", + "home_axis", + "fault_ack", + "abort_axis", + "abort_all_axes", + "axis_status", + "print_command_history", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not hasattr(self, "_rx_buffer"): + self._rx_buffer = "" + + @threadlocked + def socket_put(self, val: str) -> None: + self.command_history.append(f"[PUT]: {val}") + self.sock.put(f"{val}{EOS}".encode()) + + @threadlocked + def socket_get(self, timeout: float = 5.0) -> str: + """Receive one EOS-terminated reply. The EOS character is stripped.""" + start = time.time() + while EOS not in self._rx_buffer: + chunk = self.sock.receive().decode() + if chunk: + self._rx_buffer += chunk + continue + if time.time() - start > timeout: + partial, self._rx_buffer = self._rx_buffer, "" + raise A3200CommunicationError( + f"No complete reply within {timeout} s. Received so far:" + f" {partial!r}. Is 'Always Send EOS' set in CommandSetup?" + ) + time.sleep(0.005) + response, self._rx_buffer = self._rx_buffer.split(EOS, 1) + self.command_history.append(f"[GET]: {response}") + return response + + @threadlocked + def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str: + self.socket_put(val) + return self.socket_get() + + @threadlocked + def command(self, cmd: str) -> str: + """ + Send a command and return the reply data (without the success character). + Not retried: a motion command must not be sent twice. + + Raises: + A3200Error: invalid command ('!') or task fault ('#'); for a fault the + controller's ~LASTERROR message is included. + A3200CommunicationError: unexpected reply. + """ + reply = self.socket_put_and_receive(cmd) + if not reply: + raise A3200CommunicationError(f"Empty reply to '{cmd}'.") + code, data = reply[0], reply[1:].strip() + if code == ACK_CHAR: + return data + if code == NAK_CHAR: + raise A3200Error(f"Controller rejected '{cmd}' as invalid.") + if code == FAULT_CHAR: + raise A3200Error(f"'{cmd}' failed: {self.last_error()}") + raise A3200CommunicationError(f"Unexpected reply to '{cmd}': {reply!r}") + + def last_error(self) -> str: + reply = self.socket_put_and_receive("~LASTERROR") + return reply[1:].strip() if reply.startswith(ACK_CHAR) else reply + + def get_version(self) -> str: + return self.command("~VERSION") + + @retry_once + @threadlocked + def get_status_items(self, items: list[tuple]) -> list[float]: + """ + Read several status items in one round trip with ~STATUS. + + Args: + items: tuples as in the ~STATUS syntax, e.g. [("X", "PositionFeedback")] + """ + query = " ".join(f"({', '.join(str(i) for i in item)})" for item in items) + values = self.command(f"~STATUS {query}").split() + if len(values) != len(items): + raise A3200CommunicationError( + f"Expected {len(items)} values for ~STATUS {query}, received {values}" + ) + return [float(v) for v in values] + + def axis_status(self, axis: str) -> dict: + """Position, status words and decoded flags of one axis.""" + position, axis_status, drive_status, axis_fault = self.get_status_items( + [ + (axis, "ProgramPositionFeedback"), + (axis, "AxisStatus"), + (axis, "DriveStatus"), + (axis, "AxisFault"), + ] + ) + axis_status, drive_status, axis_fault = ( + int(axis_status), + int(drive_status), + int(axis_fault), + ) + return { + "position": position, + "axis_status": axis_status, + "drive_status": drive_status, + "axis_fault": axis_fault, + "enabled": bool(drive_status & DRIVESTATUS_ENABLED), + "homed": bool(axis_status & AXISSTATUS_HOMED), + "moving": self._is_moving(axis_status), + "faults": decode_axis_fault(axis_fault), + "cw_limit": bool(drive_status & DRIVESTATUS_CW_EOT_LIMIT), + "ccw_limit": bool(drive_status & DRIVESTATUS_CCW_EOT_LIMIT), + } + + @staticmethod + def _is_moving(axis_status: int) -> bool: + return bool(axis_status & AXISSTATUS_BUSY_MASK) or not axis_status & AXISSTATUS_MOVE_DONE + + def is_axis_moving(self, axis: str) -> bool: + (axis_status,) = self.get_status_items([(axis, "AxisStatus")]) + return self._is_moving(int(axis_status)) + + def get_position(self, axis: str) -> float: + (position,) = self.get_status_items([(axis, "ProgramPositionFeedback")]) + return position + + def check_ready_to_move(self, axis: str, require_homed: bool = True) -> None: + """Raise A3200Error if the axis is faulted, disabled or (optionally) not homed.""" + status = self.axis_status(axis) + if status["faults"]: + raise A3200Error( + f"Axis {axis} has faults {status['faults']}. Clear them with fault_ack('{axis}')." + ) + if not status["enabled"]: + raise A3200Error(f"Axis {axis} is not enabled. Use enable_axis('{axis}').") + if require_homed and not status["homed"]: + raise A3200Error(f"Axis {axis} is not homed. Use home_axis('{axis}').") + + def move_absolute(self, axis: str, position: float, velocity: float) -> None: + """Start an asynchronous absolute move (MOVEABS returns before the move is done).""" + if velocity <= 0: + raise ValueError(f"Velocity must be positive, received {velocity}.") + self.command(f"MOVEABS {axis} {position:.6f} {velocity:.6f}") + + def enable_axis(self, axis: str) -> None: + self.command(f"ENABLE {axis}") + + def disable_axis(self, axis: str) -> None: + self.command(f"DISABLE {axis}") + + def home_axis(self, axis: str, wait: bool = True, timeout: float = 300) -> None: + """Home an axis with HOMEASYNC (non-blocking on the socket), optionally wait.""" + self.command(f"HOMEASYNC {axis}") + if not wait: + return + start = time.time() + time.sleep(0.1) + while self.is_axis_moving(axis): + if time.time() - start > timeout: + self.abort_axis(axis) + raise A3200Error(f"Homing of axis {axis} did not finish within {timeout} s.") + time.sleep(0.1) + status = self.axis_status(axis) + if not status["homed"]: + raise A3200Error(f"Homing of axis {axis} failed. Faults: {status['faults']}") + + def fault_ack(self, axis: str) -> None: + self.command(f"FAULTACK {axis}") + + def abort_axis(self, axis: str) -> None: + self.command(f"ABORT {axis}") + + def abort_all_axes(self) -> None: + axes = [ax.axis_Id for ax in self._axis if ax is not None] + if axes: + self.command(f"ABORT {' '.join(axes)}") + + def describe(self) -> None: + t = PrettyTable() + t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}" + t.field_names = ["Axis", "Name", "Enabled", "Homed", "Moving", "Faults", "Position"] + for axis in self._axis: + if axis is None: + continue + status = self.axis_status(axis.axis_Id) + t.add_row( + [ + axis.axis_Id, + axis.name, + status["enabled"], + status["homed"], + status["moving"], + ", ".join(status["faults"]) or "-", + f"{status['position'] * axis.sign:.4f}", + ] + ) + print(t) + + +class A3200SignalBase(SocketSignal): + def __init__(self, signal_name, **kwargs): + self.signal_name = signal_name + super().__init__(**kwargs) + self.controller = self.root.controller if hasattr(self.root, "controller") else None + + +class A3200SignalRO(A3200SignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self._metadata["write_access"] = False + + def _socket_set(self, val): + raise ReadOnlyError("Read-only signals cannot be set") + + +class A3200ReadbackSignal(A3200SignalRO): + @retry_once + @threadlocked + def _socket_get(self) -> float: + return self.controller.get_position(self.parent.axis_Id) * self.parent.sign + + def read(self): + self._metadata["timestamp"] = time.time() + return super().read() + + +class A3200SetpointSignal(A3200SignalBase): + def __init__(self, signal_name, **kwargs): + super().__init__(signal_name, **kwargs) + self.setpoint = 0.0 + + def _socket_get(self) -> float: + """Last setpoint sent; it is not read back from the controller.""" + return self.setpoint + + @threadlocked + def _socket_set(self, val: float) -> None: + self.setpoint = val + self.controller.move_absolute( + self.parent.axis_Id, val * self.parent.sign, self.parent.motor_velocity.get() + ) + + +class A3200MotorIsMoving(A3200SignalRO): + @threadlocked + def _socket_get(self) -> bool: + return self.controller.is_axis_moving(self.parent.axis_Id) + + def get(self): + val = super().get() + if val is not None: + self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time()) + return val + + +class A3200AxisHomed(A3200SignalRO): + @threadlocked + def _socket_get(self) -> bool: + return self.controller.axis_status(self.parent.axis_Id)["homed"] + + +class A3200Motor(Device, PositionerBase): + """ + One axis of an Aerotech A3200 controller. + + Args: + axis_Id: A3200 axis name, e.g. "X". + axis_nr: slot of this axis in the controller's axis list (unique per controller). + velocity: move speed in user units per second (A3200 user units: mm or deg). + egu: engineering units shown in BEC. + require_homed: refuse moves while the axis is not homed. + """ + + USER_ACCESS = ["controller", "enable", "disable", "home", "fault_ack", "status"] + readback = Cpt(A3200ReadbackSignal, signal_name="readback", kind="hinted") + user_setpoint = Cpt(A3200SetpointSignal, signal_name="setpoint") + motor_is_moving = Cpt(A3200MotorIsMoving, signal_name="motor_is_moving", kind="normal") + homed = Cpt(A3200AxisHomed, signal_name="homed", kind="config") + motor_velocity = Cpt(Signal, value=1.0, kind="config") + high_limit_travel = Cpt(Signal, value=0, kind="omitted") + low_limit_travel = Cpt(Signal, value=0, kind="omitted") + + SUB_READBACK = "readback" + SUB_CONNECTION_CHANGE = "connection_change" + _default_sub = SUB_READBACK + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + axis_nr=0, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + host="localhost", + port=8000, + limits=None, + sign=1, + velocity=1.0, + egu="mm", + tolerance=0.001, + require_homed=True, + socket_cls=SocketIO, + device_manager=None, + **kwargs, + ): + self.controller = A3200Controller( + socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager + ) + self.axis_Id = axis_Id + self.axis_nr = axis_nr + self.controller.set_axis(axis=self, axis_nr=axis_nr) + self.sign = sign + self.tolerance = tolerance + self.require_homed = require_homed + self._egu = egu + self.device_manager = device_manager + + super().__init__( + prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self.readback.name = self.name + self.motor_velocity.put(velocity) + self.controller.subscribe( + self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE + ) + self._update_connection_state() + + if limits is not None: + assert len(limits) == 2 + self.low_limit_travel.put(limits[0]) + self.high_limit_travel.put(limits[1]) + + def wait_for_connection(self, timeout: float = 30.0) -> bool: + self.controller.on(timeout=timeout) + self.user_setpoint.setpoint = self.readback.get() + + def destroy(self): + """Make sure to turn off the controller socket on destroy.""" + self.controller.off(update_config=False) + return super().destroy() + + @property + def limits(self): + return (self.low_limit_travel.get(), self.high_limit_travel.get()) + + @property + def low_limit(self): + return self.limits[0] + + @property + def high_limit(self): + return self.limits[1] + + @property + def egu(self): + return self._egu + + def check_value(self, pos): + """Check that the position is within the soft limits""" + low_limit, high_limit = self.limits + if low_limit < high_limit and not (low_limit <= pos <= high_limit): + raise LimitError(f"position={pos} not within limits {self.limits}") + + def _update_connection_state(self, **kwargs): + for walk in self.walk_signals(): + walk.item._metadata["connected"] = self.controller.connected + + @raise_if_disconnected + def move(self, position, wait=True, **kwargs): + """Move to a position. Returns a MoveStatus that finishes when the axis is done.""" + # Check before the MoveStatus exists: a failed MoveStatus calls stop() (ABORT) from + # a callback thread, which could abort a move or homing started right after. + self.check_value(position) + self.controller.check_ready_to_move(self.axis_Id, require_homed=self.require_homed) + self._started_moving = False + timeout = kwargs.pop("timeout", 100) + status = super().move(position, timeout=timeout, **kwargs) + try: + self.user_setpoint.put(position, wait=False) + except Exception: + self._done_moving(success=False) + raise + + def move_and_finish(): + # MOVEABS is asynchronous; give the controller one cycle to set the busy bits + time.sleep(0.05) + while self.motor_is_moving.get(): + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + time.sleep(0.1) + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) + if not success: + faults = self.controller.axis_status(self.axis_Id)["faults"] + logger.warning( + f"{self.name} stopped at {val[self.name]['value']} instead of {position}." + f" Faults: {faults}" + ) + self._done_moving(success=success) + + threading.Thread(target=move_and_finish, daemon=True).start() + try: + if wait: + status_wait(status) + except KeyboardInterrupt: + self.stop() + raise + + return status + + def stop(self, *, success=False): + self.controller.abort_axis(self.axis_Id) + return super().stop(success=success) + + def enable(self) -> None: + self.controller.enable_axis(self.axis_Id) + + def disable(self) -> None: + self.controller.disable_axis(self.axis_Id) + + def home(self, wait: bool = True) -> None: + self.controller.home_axis(self.axis_Id, wait=wait) + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + + def fault_ack(self) -> None: + self.controller.fault_ack(self.axis_Id) + + def status(self) -> dict: + return self.controller.axis_status(self.axis_Id) diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py new file mode 100644 index 00000000..40a0f802 --- /dev/null +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -0,0 +1,298 @@ +""" +Simulated Aerotech A3200 controller (ASCII command interface). + +Implements the command subset used by `A3200Controller`: ~VERSION, ~LASTERROR, ~STATUS, +ENABLE, DISABLE, MOVEABS, MOVEINC, ABORT, HOMEASYNC, FAULTACK, ACKNOWLEDGEALL. +Replies follow the real interface with "Always Send EOS": "%\\n", "!\\n" or "#\\n". + +Simulated behavior: +- constant-velocity motion with linear position interpolation (no ramps) +- HOMEASYNC moves the axis to 0 at the home speed and sets the Homed bit +- moving a disabled axis is a task fault ("#"), like on the real controller +- faults can be injected via `SimA3200State.axis(name).fault`, FAULTACK clears them +""" + +from __future__ import annotations + +import re +import threading +import time + +from bec_lib.logger import bec_logger + +from csaxs_bec.devices.aerotech.a3200 import ( + AXISSTATUS_HOMED, + AXISSTATUS_HOMING, + AXISSTATUS_JOGGING, + AXISSTATUS_MOVE_DONE, + DRIVESTATUS_ENABLED, + A3200Motor, +) +from csaxs_bec.devices.sim.sim_socket import SimSocketBase, SimStateRegistry + +logger = bec_logger.logger + +_STATUS_ITEM_RE = re.compile(r"\(([^)]*)\)") + + +class _SimA3200Axis: + """State of one simulated A3200 axis (positions in user units).""" + + def __init__(self, name: str): + self.name = name + self.enabled = False + self.homed = False + self.homing = False + self.fault = 0 + self.home_velocity = 10.0 + self.pos = 0.0 + self._move_start = 0.0 + self._move_target = 0.0 + self._move_t0 = 0.0 + self._move_t1 = 0.0 + + def position(self) -> float: + now = time.time() + if now >= self._move_t1: + return self.pos + frac = (now - self._move_t0) / (self._move_t1 - self._move_t0) + return self._move_start + frac * (self._move_target - self._move_start) + + def is_moving(self) -> bool: + moving = time.time() < self._move_t1 + if not moving and self.homing: + self.homing = False + self.homed = True + return moving + + def start_move(self, target: float, velocity: float): + current = self.position() + self._move_start = current + self._move_target = target + self._move_t0 = time.time() + self._move_t1 = self._move_t0 + abs(target - current) / abs(velocity) + self.pos = target + + def abort(self): + self.pos = self.position() + self._move_t1 = 0.0 + self.homing = False + + def axis_status(self) -> int: + moving = self.is_moving() # first: finishes a homing move + status = 0 + if self.homed: + status |= AXISSTATUS_HOMED + if self.homing: + status |= AXISSTATUS_HOMING + if moving: + status |= AXISSTATUS_JOGGING + else: + status |= AXISSTATUS_MOVE_DONE + return status + + def drive_status(self) -> int: + return DRIVESTATUS_ENABLED if self.enabled else 0 + + +class SimA3200State: + """Shared state of one simulated A3200 controller.""" + + VERSION = "6.4.4.16" + + def __init__(self, host, port): + self.host = host + self.port = port + self._axes = {} + self._lock = threading.RLock() + + def axis(self, name: str) -> _SimA3200Axis: + with self._lock: + key = name.upper() + if key not in self._axes: + self._axes[key] = _SimA3200Axis(key) + return self._axes[key] + + def configure_axis( + self, name, initial_position=None, enabled=None, homed=None, home_velocity=None + ): + ax = self.axis(name) + if initial_position is not None: + ax.pos = float(initial_position) + if enabled is not None: + ax.enabled = bool(enabled) + if homed is not None: + ax.homed = bool(homed) + if home_velocity is not None: + ax.home_velocity = float(home_velocity) + + +class _TaskFault(Exception): + pass + + +class SimA3200Socket(SimSocketBase): + state_cls = SimA3200State + + def __init__(self, host, port, socket_timeout: int = 2): + super().__init__(host, port, socket_timeout) + self._last_error = "No Error" + + def handle_command(self, line: str): + try: + data = self._dispatch(line) + except _TaskFault as exc: + self._last_error = str(exc) + return "#\n" + except (ValueError, IndexError): + self._last_error = f"Invalid command: {line}" + return "!\n" + if data is None: + return "!\n" + return f"%{data}\n" + + def _dispatch(self, line: str): + state = self.state + words = line.split() + cmd = words[0].upper() + args = words[1:] + + if cmd == "~VERSION": + return state.VERSION + if cmd == "~LASTERROR": + return self._last_error + if cmd == "~STATUS": + return " ".join(self._status_item(item) for item in _STATUS_ITEM_RE.findall(line)) + if cmd == "ENABLE": + for name in self._require_axes(args): + state.axis(name).enabled = True + return "" + if cmd == "DISABLE": + for name in self._require_axes(args): + ax = state.axis(name) + ax.abort() + ax.enabled = False + return "" + if cmd in ("MOVEABS", "MOVEINC"): + name, value, velocity = args[0], float(args[1]), float(args[2]) + if velocity == 0: + raise _TaskFault("Speed of zero is not allowed.") + ax = self._ready_axis(name) + target = value if cmd == "MOVEABS" else ax.position() + value + ax.start_move(target, velocity) + return "" + if cmd == "HOMEASYNC": + for name in self._require_axes(args): + ax = self._ready_axis(name) + ax.homing = True + ax.homed = False + ax.start_move(0.0, ax.home_velocity) + return "" + if cmd == "ABORT": + for name in self._require_axes(args): + state.axis(name).abort() + return "" + if cmd == "FAULTACK": + for name in self._require_axes(args): + state.axis(name).fault = 0 + return "" + if cmd == "ACKNOWLEDGEALL": + for ax in state._axes.values(): + ax.fault = 0 + return "" + logger.warning(f"[sim a3200] {self.host}:{self.port} unhandled command '{line}'") + return None + + @staticmethod + def _require_axes(args): + if not args: + raise ValueError("No axis given.") + return args + + def _ready_axis(self, name: str) -> _SimA3200Axis: + ax = self.state.axis(name) + if not ax.enabled: + raise _TaskFault("The axis is not enabled.") + if ax.fault: + raise _TaskFault("The axis has a fault.") + return ax + + def _status_item(self, item: str) -> str: + parts = [p.strip() for p in item.split(",")] + if len(parts) < 2: + raise ValueError(f"Unsupported status item ({item})") + ax = self.state.axis(parts[0]) + data_item = parts[1] + if data_item in ( + "PositionFeedback", + "ProgramPositionFeedback", + "PositionCommand", + "ProgramPositionCommand", + ): + return f"{ax.position():.6f}" + if data_item == "AxisStatus": + return str(ax.axis_status()) + if data_item == "DriveStatus": + return str(ax.drive_status()) + if data_item == "AxisFault": + return str(ax.fault) + raise ValueError(f"Unsupported status item ({item})") + + +class SimA3200Motor(A3200Motor): + """A3200Motor connected to the simulated A3200 controller. + + Note: the signature must list all deviceConfig keys explicitly. The BEC device + server only passes config keys that appear in inspect.signature(dev_cls) and only + injects device_manager if it is an explicit parameter. + """ + + def __init__( + self, + axis_Id, + prefix="", + *, + name, + axis_nr=0, + host="localhost", + port=8000, + limits=None, + sign=1, + velocity=1.0, + egu="mm", + tolerance=0.001, + require_homed=True, + device_manager=None, + sim_initial_position=None, + sim_enabled=True, + sim_homed=True, + sim_home_velocity=None, + **kwargs, + ): + super().__init__( + axis_Id, + prefix, + name=name, + axis_nr=axis_nr, + host=host, + port=port, + limits=limits, + sign=sign, + velocity=velocity, + egu=egu, + tolerance=tolerance, + require_homed=require_homed, + socket_cls=SimA3200Socket, + device_manager=device_manager, + **kwargs, + ) + state = SimStateRegistry.get(SimA3200State, host, port) + state.configure_axis( + axis_Id, + initial_position=( + None if sim_initial_position is None else sim_initial_position * sign + ), + enabled=sim_enabled, + homed=sim_homed, + home_velocity=sim_home_velocity, + ) diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py new file mode 100644 index 00000000..eada5f51 --- /dev/null +++ b/tests/tests_devices/test_a3200.py @@ -0,0 +1,207 @@ +import pytest +from bec_server.device_server.tests.utils import DMMock +from ophyd.utils import LimitError +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.aerotech.a3200 import ( + A3200CommunicationError, + A3200Controller, + A3200Error, + A3200Motor, + decode_axis_fault, +) +from csaxs_bec.devices.sim.sim_a3200 import SimA3200Motor, SimA3200State +from csaxs_bec.devices.sim.sim_socket import SimStateRegistry + +_HOST = "test-a3200.psi.ch" +_PORT = 8000 + + +@pytest.fixture(autouse=True) +def _reset_controllers(): + A3200Controller._reset_controller() + SimStateRegistry.reset() + yield + SimStateRegistry.reset() + A3200Controller._reset_controller() + + +@pytest.fixture +def mock_motor(): + motor = A3200Motor( + "X", name="aerox", host=_HOST, port=_PORT, socket_cls=SocketMock, device_manager=DMMock() + ) + motor.controller.on() + motor.controller.sock.flush_buffer() + yield motor + motor.controller.off() + + +def _sim_motor(axis_Id="X", axis_nr=0, **kwargs): + kwargs.setdefault("velocity", 1000.0) + kwargs.setdefault("sim_home_velocity", 1000.0) + motor = SimA3200Motor( + axis_Id, + name=f"aero{axis_Id.lower()}", + axis_nr=axis_nr, + host=_HOST, + port=_PORT, + device_manager=DMMock(), + **kwargs, + ) + motor.wait_for_connection() + return motor + + +# --- protocol (SocketMock) --------------------------------------------------- + + +def test_command_returns_data_after_ack(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"%6.04.004.0016\n"] + assert mock_motor.controller.get_version() == "6.04.004.0016" + assert mock_motor.controller.sock.buffer_put == [b"~VERSION\n"] + + +def test_reply_split_over_several_receives(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"%1.2", b"5 4194", b"305\n"] + assert mock_motor.controller.get_status_items([("X", "A"), ("X", "B")]) == [1.25, 4194305.0] + + +def test_two_replies_in_one_receive_are_kept_apart(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"%\n%7.5\n"] + mock_motor.controller.enable_axis("X") + assert mock_motor.controller.get_position("X") == 7.5 + + +def test_fault_reply_raises_with_last_error(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"#\n", b"%The axis is not enabled.\n"] + with pytest.raises(A3200Error, match="The axis is not enabled."): + mock_motor.controller.command("HOME X") + assert mock_motor.controller.sock.buffer_put == [b"HOME X\n", b"~LASTERROR\n"] + + +def test_invalid_reply_raises(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"!\n"] + with pytest.raises(A3200Error, match="invalid"): + mock_motor.controller.command("ENABLE") + + +def test_motion_command_is_not_retried(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"#\n", b"%fault\n", b"%\n", b"%\n"] + with pytest.raises(A3200Error): + mock_motor.controller.move_absolute("X", 1.0, 2.0) + assert mock_motor.controller.sock.buffer_put == [ + b"MOVEABS X 1.000000 2.000000\n", + b"~LASTERROR\n", + ] + + +def test_missing_eos_times_out(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"%"] + with pytest.raises(A3200CommunicationError, match="Always Send EOS"): + mock_motor.controller.socket_get(timeout=0.05) + + +def test_status_query_format(mock_motor): + mock_motor.controller.sock.buffer_recv = [b"%1.5 4194305 1 0\n"] + status = mock_motor.controller.axis_status("X") + assert mock_motor.controller.sock.buffer_put == [ + b"~STATUS (X, ProgramPositionFeedback) (X, AxisStatus) (X, DriveStatus) (X, AxisFault)\n" + ] + assert status["position"] == 1.5 + assert status["enabled"] and status["homed"] + assert not status["moving"] + assert status["faults"] == [] + + +def test_decode_axis_fault(): + assert decode_axis_fault(0) == [] + assert decode_axis_fault((1 << 0) | (1 << 2) | (1 << 11)) == [ + "PositionError", + "CwEOTLimit", + "EstopFault", + ] + + +# --- simulated controller ---------------------------------------------------- + + +def test_sim_move_and_readback(): + motor = _sim_motor(sim_initial_position=1.0) + assert motor.readback.get() == pytest.approx(1.0) + motor.move(5.0, wait=True) + assert motor.readback.get() == pytest.approx(5.0) + assert not motor.motor_is_moving.get() + + +def test_sim_sign_is_applied(): + motor = _sim_motor(sign=-1, sim_initial_position=2.0) + assert motor.readback.get() == pytest.approx(2.0) + motor.move(-3.0, wait=True) + assert motor.readback.get() == pytest.approx(-3.0) + state = SimStateRegistry.get(SimA3200State, _HOST, _PORT) + assert state.axis("X").position() == pytest.approx(3.0) + + +def test_sim_move_refused_when_disabled(): + motor = _sim_motor(sim_enabled=False) + with pytest.raises(A3200Error, match="not enabled"): + motor.move(1.0, wait=True) + motor.enable() + motor.move(1.0, wait=True) + assert motor.readback.get() == pytest.approx(1.0) + + +def test_sim_move_refused_when_not_homed(): + motor = _sim_motor(sim_homed=False, sim_initial_position=3.0) + with pytest.raises(A3200Error, match="not homed"): + motor.move(1.0, wait=True) + motor.home() + assert "[PUT]: ABORT X" not in motor.controller.command_history + assert motor.status()["homed"] + assert motor.readback.get() == pytest.approx(0.0) + motor.move(1.0, wait=True) + assert motor.readback.get() == pytest.approx(1.0) + + +def test_sim_unhomed_move_allowed_if_configured(): + motor = _sim_motor(sim_homed=False, require_homed=False) + motor.move(2.0, wait=True) + assert motor.readback.get() == pytest.approx(2.0) + + +def test_sim_fault_blocks_move_until_acknowledged(): + motor = _sim_motor() + SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X").fault = 1 << 2 + with pytest.raises(A3200Error, match="CwEOTLimit"): + motor.move(1.0, wait=True) + motor.fault_ack() + motor.move(1.0, wait=True) + assert motor.readback.get() == pytest.approx(1.0) + + +def test_sim_soft_limits(): + motor = _sim_motor(limits=[-1, 1]) + with pytest.raises(LimitError): + motor.move(2.0, wait=True) + + +def test_sim_stop_aborts_motion(): + motor = _sim_motor(velocity=1.0) + status = motor.move(10.0, wait=False) + motor.stop() + with pytest.raises(Exception): + status.wait(timeout=2) + assert motor.readback.get() < 1.0 + assert not motor.motor_is_moving.get() + + +def test_sim_two_axes_share_one_controller(): + x = _sim_motor("X", axis_nr=0) + y = _sim_motor("Y", axis_nr=1) + assert x.controller is y.controller + x.move(1.0, wait=True) + y.move(2.0, wait=True) + assert x.readback.get() == pytest.approx(1.0) + assert y.readback.get() == pytest.approx(2.0) + x.controller.describe() -- 2.54.0 From fe36f374fb16edb3f0ab94fc5ef6ac6e1728793e Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Tue, 27 Oct 2026 18:28:29 +0100 Subject: [PATCH 02/52] feat(aerotech): aerotech_cont_grid scan with PSO line trigger and position capture - A3200Motor: arm_line_trigger/disarm_line_trigger (PSO window on the fast axis drives DDG1 EXT), acceleration (RAMP RATE before each move), encoder_channel - A3200PositionCapture: Npaq DATAACQ latches the encoder counters on each detector trigger (UINT1), read back per line via ARRAY READ and published as async data; offset calibrated with a software PSO fire - aerotech_cont_grid: cont_grid geometry, line start triggered by position instead of a software sleep - simulator support for RAMP, $global, UNITSTOCOUNTS, PSO, DATAACQ, ARRAY READ - configs, tests, README with wiring and commissioning checklist Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/device_configs/sastt_aerotech.yaml | 34 +- .../simulated_sastt_aerotech.yaml | 30 +- csaxs_bec/devices/aerotech/README.md | 44 ++- csaxs_bec/devices/aerotech/a3200.py | 257 +++++++++++- csaxs_bec/devices/sim/sim_a3200.py | 194 ++++++++- csaxs_bec/scans/__init__.py | 1 + .../scans/scans_v4/aerotech_cont_grid.py | 369 ++++++++++++++++++ tests/tests_devices/test_a3200.py | 111 +++++- .../test_aerotech_cont_grid_scan.py | 185 +++++++++ 9 files changed, 1210 insertions(+), 15 deletions(-) create mode 100644 csaxs_bec/scans/scans_v4/aerotech_cont_grid.py create mode 100644 tests/tests_scans/test_aerotech_cont_grid_scan.py diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index dc5b4f88..72e2dca9 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -3,7 +3,9 @@ # (FireWire to the Npaq). BEC connects to the A3200 ASCII command interface on that PC. # A3200 setup: CommandSetup = 0x00010004 (enabled + Always Send EOS), CommandPort = 8000. # -# TODO commissioning: host name of the A3200 PC, A3200 axis names, velocities, limits. +# TODO commissioning: host name of the A3200 PC, A3200 axis names, encoder channels, +# velocities (= maximum scan velocity), accelerations, limits. +# Wiring for aerotech_cont_grid: PSO output (J7) -> DDG1 EXT, detector trigger -> UINT1 (J12). # UU (ADRS-150 slipring follower) follows U on the controller and is not a BEC device. # Limits [0, 0] = no BEC soft limits; the controller's SoftwareLimitLow/High still apply. ############################################################ @@ -14,9 +16,11 @@ aerox: deviceConfig: axis_Id: X axis_nr: 0 + encoder_channel: 0 + acceleration: 100.0 host: a3200-saxstt.psi.ch port: 8000 - velocity: 1.0 + velocity: 10.0 egu: mm limits: - 0 @@ -34,6 +38,8 @@ aeroy: deviceConfig: axis_Id: Y axis_nr: 1 + encoder_channel: 1 + acceleration: 20.0 host: a3200-saxstt.psi.ch port: 8000 velocity: 0.5 @@ -54,9 +60,11 @@ aeroz: deviceConfig: axis_Id: Z axis_nr: 2 + encoder_channel: 2 + acceleration: 100.0 host: a3200-saxstt.psi.ch port: 8000 - velocity: 1.0 + velocity: 10.0 egu: mm limits: - 0 @@ -74,6 +82,8 @@ aerorot: deviceConfig: axis_Id: U axis_nr: 3 + encoder_channel: 3 + acceleration: 360.0 host: a3200-saxstt.psi.ch port: 8000 velocity: 10.0 @@ -89,3 +99,21 @@ aerorot: readoutPriority: baseline deviceTags: - sastt_aerotech +aerocapture: + description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture + deviceConfig: + host: a3200-saxstt.psi.ch + port: 8000 + drive_axis: X + channels: + 0: X + 1: Y + 2: Z + 3: U + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: async + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index b89ffc68..d18b2971 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -9,9 +9,11 @@ aerox: deviceConfig: axis_Id: X axis_nr: 0 + encoder_channel: 0 + acceleration: 100.0 host: sim-a3200-saxstt port: 8000 - velocity: 1.0 + velocity: 10.0 egu: mm limits: - 0 @@ -29,6 +31,8 @@ aeroy: deviceConfig: axis_Id: Y axis_nr: 1 + encoder_channel: 1 + acceleration: 20.0 host: sim-a3200-saxstt port: 8000 velocity: 0.5 @@ -49,9 +53,11 @@ aeroz: deviceConfig: axis_Id: Z axis_nr: 2 + encoder_channel: 2 + acceleration: 100.0 host: sim-a3200-saxstt port: 8000 - velocity: 1.0 + velocity: 10.0 egu: mm limits: - 0 @@ -69,6 +75,8 @@ aerorot: deviceConfig: axis_Id: U axis_nr: 3 + encoder_channel: 3 + acceleration: 360.0 host: sim-a3200-saxstt port: 8000 velocity: 10.0 @@ -84,3 +92,21 @@ aerorot: readoutPriority: baseline deviceTags: - sastt_aerotech +aerocapture: + description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) + deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200PositionCapture + deviceConfig: + host: sim-a3200-saxstt + port: 8000 + drive_axis: X + channels: + 0: X + 1: Y + 2: Z + 3: U + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: async + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index e1aa544d..347dae79 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -11,6 +11,7 @@ A3200 ASCII command interface on the Windows PC (TCP, default port 8000). | aeroz | Z | ATS20010 | linear | | aerorot | U | ABRT-200 | rotation | | - | UU | ADRS-150 | slipring follower of U, handled on the controller | +| aerocapture | - | - | position capture (DATAACQ) of X, Y, Z, U | Configs: `device_configs/sastt_aerotech.yaml` (hardware), `device_configs/simulated_omny/simulated_sastt_aerotech.yaml` (simulation, `sim_a3200.py`). @@ -39,9 +40,42 @@ Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface" 7. Check `ProgramPositionFeedback` matches the position shown in Motion Composer. 8. Check how UU follows U (gantry, slave or program) before rotating. -## Next: continuous line scan +## Scan: aerotech_cont_grid -Fast axis moving continuously, slow axis stepping between lines, start of each line -triggered by position with PSO (position synchronized output) on the fast axis: -`PSOCONTROL`, `PSOWINDOW`/`PSODISTANCE`, `PSOOUTPUT` drive the digital output when the -armed position is crossed. To be implemented once the hardware is reachable. +`scans_v4/aerotech_cont_grid.py`, same arguments as `cont_grid`: + +```python +scans.aerotech_cont_grid(dev.aerox, 0, 2, 0.01, dev.aeroy, 0, 1, 0.05, exp_time=0.01) +``` + +- fast axis moves continuously at `fast_step_size / exp_time`, the step axis steps between lines +- line start is triggered by position: PSO window on the fast axis, the output goes high + at `fast_start - velocity * shutter_open_delay` and triggers DDG1 (external, rising edge), + so the first exposure starts at `fast_start` +- run-up / run-out = ramp distance `v^2 / (2 * acceleration)` + 20 % + 50 ms at speed +- `capture_positions=True` (default): device `aerocapture` latches the encoder counters of + all configured channels on each detector trigger (DATAACQ on UINT1, up to 16 kHz, drive + array of 2M elements). After each line they are read back, converted to user units and + published as async data `aerocapture_positions_enc`. +- the counter-to-position offset is calibrated in pre_scan with one software PSO fire, + before DDG1 is switched to the external trigger + +Wiring: + +| Signal | From | To | +|--------|------|----| +| line trigger | Npaq PSO output (J7, differential / opto / open collector) | DDG1 EXT | +| exposure trigger | detector trigger (DDG2 AB) | Npaq UINT1 (J12 pins 1+/2-, 5 V opto, 5-15 mA) | + +Commissioning of the scan: + +1. Encoder channel of each axis (`encoder_channel`, `aerocapture.channels`); check that + the counter direction matches the axis direction (calibration only fixes the offset). +2. PSO output: select the output used (`PSOOUTPUT CONTROL`), PSO interlock driven or + disabled via the `IOSetup` axis parameter (Npaq manual), polarity (JP16). +3. `acceleration` of the fast axis = the ramp rate BEC sets before each move. +4. `velocity` of the fast axis is the maximum scan velocity. +5. `GlobalDoubles` >= 256: BEC uses `$global[0..199]` for array reads and `$global[255]` + as scratch variable. +6. Check one line with the scope: PSO edge at the trigger position, number of UINT1 edges + = frames per line, captured positions vs. requested positions. diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index be51d51d..6d5a84dc 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -27,6 +27,7 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError +from ophyd_devices import AsyncMultiSignal, PSIDeviceBase from ophyd_devices.utils.controller import Controller, retry_once, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected from prettytable import PrettyTable @@ -131,10 +132,17 @@ class A3200Controller(Controller): "print_command_history", ] + # $global variables used by BEC: one scratch variable for evaluate() and a block + # for drive array reads. The controller has GlobalDoubles (default 256) of them. + scratch_global = 255 + array_global_base = 0 + array_read_chunk = 200 + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not hasattr(self, "_rx_buffer"): self._rx_buffer = "" + self._counts_per_unit = {} @threadlocked def socket_put(self, val: str) -> None: @@ -304,6 +312,81 @@ class A3200Controller(Controller): if axes: self.command(f"ABORT {' '.join(axes)}") + def set_ramp_rate(self, axis: str, rate: float) -> None: + """Rate-based ramping for axis-based moves (MOVEABS), rate in user units/s^2.""" + self.command(f"RAMP MODE RATE {axis}") + self.command(f"RAMP RATE {axis} {rate:.6f}") + + # --- global variables (scratch space for functions and array reads) ----------- + + def get_globals(self, start: int, count: int) -> list[float]: + """Read $global[start] ... $global[start + count - 1] in one ~STATUS round trip.""" + return self.get_status_items([(f"$global[{i}]",) for i in range(start, start + count)]) + + def evaluate(self, expression: str) -> float: + """Evaluate an AeroBasic expression on the controller, e.g. UNITSTOCOUNTS(X, 1).""" + self.command(f"$global[{self.scratch_global}] = {expression}") + return self.get_globals(self.scratch_global, 1)[0] + + def counts_per_unit(self, axis: str) -> float: + """Encoder counts per user unit of an axis (cached, it is a fixed parameter).""" + if axis not in self._counts_per_unit: + self._counts_per_unit[axis] = self.evaluate(f"UNITSTOCOUNTS({axis}, 1)") + return self._counts_per_unit[axis] + + # --- PSO (position synchronized output) ----------------------------------- + + def pso_arm_window(self, axis: str, encoder_channel: int, low: float, high: float) -> None: + """ + PSO output high while the axis is inside [low, high] (user units, absolute). + + The window counter is loaded with the current position (axis at rest), so the + range is given relative to it in encoder counts. + """ + cpu = self.counts_per_unit(axis) + current = self.get_position(axis) + low_counts, high_counts = sorted( + (int(round((low - current) * cpu)), int(round((high - current) * cpu))) + ) + self.command(f"PSOCONTROL {axis} RESET") + self.command(f"PSOWINDOW {axis} 1 INPUT {encoder_channel}") + self.command(f"PSOWINDOW {axis} 1 LOAD 0") + self.command(f"PSOWINDOW {axis} 1 RANGE {low_counts}, {high_counts}") + self.command(f"PSOOUTPUT {axis} WINDOW") + self.command(f"PSOCONTROL {axis} ARM") + + def pso_off(self, axis: str) -> None: + self.command(f"PSOWINDOW {axis} 1 OFF") + self.command(f"PSOCONTROL {axis} OFF") + + # --- DATAACQ (position capture into the drive array) ----------------------- + + def dataacq_arm(self, axis: str, trigger: int, channels: list[int], n_triggers: int) -> None: + """ + Capture the encoder counters of `channels` on each trigger into the drive array, + starting at index 0. Npaq trigger 0 = Input 12 (UINT1) rising edge, 6 = PSO firing. + """ + self.command(f"DATAACQ {axis} 1 OFF") + self.command(f"DATAACQ {axis} 1 TRIGGER {trigger}") + for channel in sorted(channels): + self.command(f"DATAACQ {axis} 1 INPUT {channel}") + self.command(f"DATAACQ {axis} 1 ARRAY 0, {n_triggers * len(channels)}") + + def dataacq_off(self, axis: str) -> None: + self.command(f"DATAACQ {axis} 1 OFF") + + def read_drive_array(self, axis: str, start: int, count: int) -> list[float]: + """Read drive array elements through $global[array_global_base ...] in chunks.""" + values = [] + chunk = self.array_read_chunk + for offset in range(0, count, chunk): + n = min(chunk, count - offset) + self.command( + f"ARRAY {axis} READ $global[{self.array_global_base}] {start + offset} {n} FAST" + ) + values.extend(self.get_globals(self.array_global_base, n)) + return values + def describe(self) -> None: t = PrettyTable() t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}" @@ -365,6 +448,9 @@ class A3200SetpointSignal(A3200SignalBase): @threadlocked def _socket_set(self, val: float) -> None: self.setpoint = val + acceleration = self.parent.motor_acceleration.get() + if acceleration: + self.controller.set_ramp_rate(self.parent.axis_Id, acceleration) self.controller.move_absolute( self.parent.axis_Id, val * self.parent.sign, self.parent.motor_velocity.get() ) @@ -398,14 +484,28 @@ class A3200Motor(Device, PositionerBase): velocity: move speed in user units per second (A3200 user units: mm or deg). egu: engineering units shown in BEC. require_homed: refuse moves while the axis is not homed. + acceleration: ramp rate for moves in user units/s^2; None keeps the controller's + ramp settings (DefaultRampRate). + encoder_channel: Npaq local encoder counter of this axis, needed for the PSO line + trigger (arm_line_trigger). """ - USER_ACCESS = ["controller", "enable", "disable", "home", "fault_ack", "status"] + USER_ACCESS = [ + "controller", + "enable", + "disable", + "home", + "fault_ack", + "status", + "arm_line_trigger", + "disarm_line_trigger", + ] readback = Cpt(A3200ReadbackSignal, signal_name="readback", kind="hinted") user_setpoint = Cpt(A3200SetpointSignal, signal_name="setpoint") motor_is_moving = Cpt(A3200MotorIsMoving, signal_name="motor_is_moving", kind="normal") homed = Cpt(A3200AxisHomed, signal_name="homed", kind="config") motor_velocity = Cpt(Signal, value=1.0, kind="config") + motor_acceleration = Cpt(Signal, value=0.0, kind="config") high_limit_travel = Cpt(Signal, value=0, kind="omitted") low_limit_travel = Cpt(Signal, value=0, kind="omitted") @@ -432,6 +532,8 @@ class A3200Motor(Device, PositionerBase): egu="mm", tolerance=0.001, require_homed=True, + acceleration=None, + encoder_channel=None, socket_cls=SocketIO, device_manager=None, **kwargs, @@ -445,6 +547,7 @@ class A3200Motor(Device, PositionerBase): self.sign = sign self.tolerance = tolerance self.require_homed = require_homed + self.encoder_channel = encoder_channel self._egu = egu self.device_manager = device_manager @@ -459,6 +562,7 @@ class A3200Motor(Device, PositionerBase): ) self.readback.name = self.name self.motor_velocity.put(velocity) + self.motor_acceleration.put(acceleration or 0.0) self.controller.subscribe( self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE ) @@ -566,5 +670,156 @@ class A3200Motor(Device, PositionerBase): def fault_ack(self) -> None: self.controller.fault_ack(self.axis_Id) + def arm_line_trigger(self, start: float, end: float) -> None: + """ + Arm the PSO output to go high when the axis enters [start, end] and low when it + leaves it. The rising edge starts the exposure sequence of a scan line. The axis + must be at rest outside the window. + """ + if self.encoder_channel is None: + raise A3200Error(f"{self.name}: encoder_channel is not configured, no PSO trigger.") + self.controller.pso_arm_window( + self.axis_Id, self.encoder_channel, start * self.sign, end * self.sign + ) + + def disarm_line_trigger(self) -> None: + self.controller.pso_off(self.axis_Id) + def status(self) -> dict: return self.controller.axis_status(self.axis_Id) + + +NPAQ_ENCODER_CHANNELS = 6 +DATAACQ_TRIGGER_UINT1_RISING = 0 +DATAACQ_TRIGGER_PSO_FIRING_EVENT = 6 + + +class A3200PositionCapture(PSIDeviceBase): + """ + Latch axis positions on an external trigger (Npaq DATAACQ, drive array). + + Each rising edge on the trigger input (default: Input 12 / UINT1, the detector + exposure trigger) stores the encoder counters of the configured channels in the + drive array. After a scan line, read_line() reads them back, converts them to user + units and publishes them as async data (signals enc0 ... enc5). + + Args: + channels: Npaq local encoder counter -> A3200 axis name, e.g. {0: "X", 1: "Y"}. + Values are in controller coordinates (the BEC motor sign is not applied). + drive_axis: any axis of the Npaq, used to address the drive (shared array). + trigger_input: DATAACQ trigger, 0 = Input 12 (UINT1) rising edge. + """ + + USER_ACCESS = ["controller", "arm", "read_line", "calibrate", "disarm"] + + positions = Cpt( + AsyncMultiSignal, + name="positions", + signals=[f"enc{i}" for i in range(NPAQ_ENCODER_CHANNELS)], + ndim=1, + async_update={"type": "add", "max_shape": [None]}, + max_size=1000, + kind="normal", + doc="Positions latched at each trigger, in user units, one sub-signal per channel.", + ) + + def __init__( + self, + prefix="", + *, + name, + host="localhost", + port=8000, + channels=None, + drive_axis="X", + trigger_input=DATAACQ_TRIGGER_UINT1_RISING, + socket_cls=SocketIO, + device_manager=None, + scan_info=None, + **kwargs, + ): + self.controller = A3200Controller( + socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager + ) + self.channels = {int(ch): str(axis) for ch, axis in (channels or {}).items()} + for ch in self.channels: + if not 0 <= ch < NPAQ_ENCODER_CHANNELS: + raise ValueError(f"Encoder channel {ch} out of range 0..{NPAQ_ENCODER_CHANNELS-1}") + self.drive_axis = drive_axis + self.trigger_input = trigger_input + self._offsets = {ch: 0.0 for ch in self.channels} + self._n_triggers = 0 + self._correlation_group = "monitored" + super().__init__( + prefix, name=name, scan_info=scan_info, device_manager=device_manager, **kwargs + ) + if len(self.channels) < 2: + raise ValueError("Configure at least two encoder channels for position capture.") + # only the configured channels are published (strict signal validation) + self.positions.signal_info.signals = self.positions._unify_signals( + [f"enc{ch}" for ch in sorted(self.channels)] + ) + + def wait_for_connection(self, all_signals=False, timeout: float = 30.0) -> None: + self.controller.on(timeout=timeout) + + def destroy(self): + self.controller.off(update_config=False) + return super().destroy() + + def on_stage(self) -> None: + frames = getattr(getattr(self, "scan_info", None), "msg", None) + frames_per_trigger = getattr(frames, "frames_per_trigger", 1) if frames else 1 + self._correlation_group = "monitored" if frames_per_trigger == 1 else "burst_group" + + def calibrate(self) -> dict[int, float]: + """ + Determine the offset between encoder counts and program position of each channel: + one capture triggered by a software PSO fire with the axes at rest. + + Fires the PSO output once: the line trigger receiver (DDG) must not be armed. + """ + channels = sorted(self.channels) + self.controller.dataacq_arm( + self.drive_axis, DATAACQ_TRIGGER_PSO_FIRING_EVENT, channels, n_triggers=1 + ) + self.controller.command(f"PSOCONTROL {self.drive_axis} RESET") + self.controller.command(f"PSOCONTROL {self.drive_axis} FIRE") + time.sleep(0.05) + raw = self.controller.read_drive_array(self.drive_axis, 0, len(channels)) + self.controller.dataacq_off(self.drive_axis) + for ch, counts in zip(channels, raw): + axis = self.channels[ch] + position = self.controller.get_position(axis) + self._offsets[ch] = position - counts / self.controller.counts_per_unit(axis) + return dict(self._offsets) + + def arm(self, n_triggers: int) -> None: + """Arm the capture of the next n_triggers trigger edges (start of the array).""" + self._n_triggers = int(n_triggers) + self.controller.dataacq_arm( + self.drive_axis, self.trigger_input, sorted(self.channels), self._n_triggers + ) + + def disarm(self) -> None: + self.controller.dataacq_off(self.drive_axis) + + def read_line(self) -> dict[str, list[float]]: + """Read the positions of the armed triggers and publish them. Returns them too.""" + channels = sorted(self.channels) + raw = self.controller.read_drive_array(self.drive_axis, 0, self._n_triggers * len(channels)) + # Npaq interleaves the inputs per trigger, in ascending channel order + data = np.asarray(raw, dtype=float).reshape(self._n_triggers, len(channels)) + timestamp = time.time() + result = {} + for i, ch in enumerate(channels): + cpu = self.controller.counts_per_unit(self.channels[ch]) + result[f"enc{ch}"] = data[:, i] / cpu + self._offsets[ch] + self.positions.put( + { + f"{self.positions.name}_{key}": {"value": value, "timestamp": timestamp} + for key, value in result.items() + }, + correlation_group=self._correlation_group, + ) + return {key: value.tolist() for key, value in result.items()} diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index 40a0f802..18f37910 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -2,7 +2,9 @@ Simulated Aerotech A3200 controller (ASCII command interface). Implements the command subset used by `A3200Controller`: ~VERSION, ~LASTERROR, ~STATUS, -ENABLE, DISABLE, MOVEABS, MOVEINC, ABORT, HOMEASYNC, FAULTACK, ACKNOWLEDGEALL. +ENABLE, DISABLE, MOVEABS, MOVEINC, ABORT, HOMEASYNC, FAULTACK, ACKNOWLEDGEALL, RAMP, +$global assignments (numbers, UNITSTOCOUNTS), PSOCONTROL/PSOWINDOW/PSOOUTPUT (window mode), +DATAACQ and ARRAY READ. Replies follow the real interface with "Always Send EOS": "%\\n", "!\\n" or "#\\n". Simulated behavior: @@ -10,6 +12,11 @@ Simulated behavior: - HOMEASYNC moves the axis to 0 at the home speed and sets the Homed bit - moving a disabled axis is a task fault ("#"), like on the real controller - faults can be injected via `SimA3200State.axis(name).fault`, FAULTACK clears them +- encoder counter of a channel = position * counts_per_unit + counter_offset +- detector exposures: when a move starts with the PSO window armed and DATAACQ armed on + UINT1, the armed number of triggers is latched immediately, evenly spread over the + PSO window of the moving axis (other channels at their current position) +- PSOCONTROL FIRE latches one capture if DATAACQ is armed on the PSO firing event """ from __future__ import annotations @@ -27,12 +34,15 @@ from csaxs_bec.devices.aerotech.a3200 import ( AXISSTATUS_MOVE_DONE, DRIVESTATUS_ENABLED, A3200Motor, + A3200PositionCapture, ) from csaxs_bec.devices.sim.sim_socket import SimSocketBase, SimStateRegistry logger = bec_logger.logger _STATUS_ITEM_RE = re.compile(r"\(([^)]*)\)") +_GLOBAL_RE = re.compile(r"^\$global\[(\d+)\]$", re.IGNORECASE) +_UNITSTOCOUNTS_RE = re.compile(r"^UNITSTOCOUNTS\(\s*(\w+)\s*,\s*([-+0-9.eE]+)\s*\)$", re.I) class _SimA3200Axis: @@ -45,6 +55,9 @@ class _SimA3200Axis: self.homing = False self.fault = 0 self.home_velocity = 10.0 + self.counts_per_unit = 10000.0 + self.counter_offset = 0 + self.ramp_rate = None self.pos = 0.0 self._move_start = 0.0 self._move_target = 0.0 @@ -105,6 +118,46 @@ class SimA3200State: self.port = port self._axes = {} self._lock = threading.RLock() + self.globals = {} + self.channel_axis = {} # Npaq encoder channel -> axis name + self.drive_array = [0.0] * 4096 + self.pso = {"armed": False, "mode": None, "channel": None, "range": None, "load": 0} + self.pso_window_entries = 0 # rising edges of the PSO window output (line triggers) + self.dataacq = {"armed": False, "trigger": None, "inputs": set(), "n_elements": 0} + + def counter(self, channel: int) -> int: + ax = self.axis(self.channel_axis[channel]) + return int(round(ax.position() * ax.counts_per_unit)) + ax.counter_offset + + def latch(self, positions: dict[str, float] | None = None, index: int = 0) -> None: + """Store one DATAACQ sample (all inputs, ascending channel) at trigger `index`.""" + inputs = sorted(self.dataacq["inputs"]) + for i, ch in enumerate(inputs): + ax = self.axis(self.channel_axis[ch]) + if positions and ax.name in positions: + value = int(round(positions[ax.name] * ax.counts_per_unit)) + ax.counter_offset + else: + value = self.counter(ch) + self.drive_array[index * len(inputs) + i] = float(value) + + def on_move_start(self, ax: _SimA3200Axis, start: float, target: float) -> None: + """Simulate the line trigger and the detector exposures of a scan line.""" + pso, acq = self.pso, self.dataacq + if not (pso["armed"] and pso["mode"] == "window" and pso["range"] is not None): + return + if self.channel_axis.get(pso["channel"]) != ax.name: + return + low = start + pso["range"][0] / ax.counts_per_unit + high = start + pso["range"][1] / ax.counts_per_unit + if not (min(start, target) <= low and high <= max(start, target)): + return + self.pso_window_entries += 1 + if acq["armed"] and acq["trigger"] == 0 and acq["inputs"]: + n_triggers = acq["n_elements"] // len(acq["inputs"]) + step = (high - low) / max(n_triggers, 1) + for k in range(n_triggers): + self.latch({ax.name: low + k * step}, index=k) + acq["armed"] = False def axis(self, name: str) -> _SimA3200Axis: with self._lock: @@ -114,9 +167,23 @@ class SimA3200State: return self._axes[key] def configure_axis( - self, name, initial_position=None, enabled=None, homed=None, home_velocity=None + self, + name, + initial_position=None, + enabled=None, + homed=None, + home_velocity=None, + counts_per_unit=None, + counter_offset=None, + encoder_channel=None, ): ax = self.axis(name) + if counts_per_unit is not None: + ax.counts_per_unit = float(counts_per_unit) + if counter_offset is not None: + ax.counter_offset = int(counter_offset) + if encoder_channel is not None: + self.channel_axis[int(encoder_channel)] = ax.name if initial_position is not None: ax.pos = float(initial_position) if enabled is not None: @@ -178,7 +245,9 @@ class SimA3200Socket(SimSocketBase): if velocity == 0: raise _TaskFault("Speed of zero is not allowed.") ax = self._ready_axis(name) - target = value if cmd == "MOVEABS" else ax.position() + value + start = ax.position() + target = value if cmd == "MOVEABS" else start + value + state.on_move_start(ax, start, target) ax.start_move(target, velocity) return "" if cmd == "HOMEASYNC": @@ -200,9 +269,82 @@ class SimA3200Socket(SimSocketBase): for ax in state._axes.values(): ax.fault = 0 return "" + if cmd == "RAMP": + # RAMP MODE RATE | RAMP RATE + if args[0].upper() == "RATE" and len(args) == 3: + state.axis(args[1]).ramp_rate = float(args[2]) + return "" + if _GLOBAL_RE.match(words[0]) and len(words) >= 3 and words[1] == "=": + index = int(_GLOBAL_RE.match(words[0]).group(1)) + state.globals[index] = self._evaluate(line.split("=", 1)[1].strip()) + return "" + if cmd == "PSOCONTROL": + return self._psocontrol(args) + if cmd == "PSOWINDOW": + # PSOWINDOW 1 INPUT | LOAD | RANGE , | OFF | ON + sub = args[2].upper() + if sub == "INPUT": + state.pso["channel"] = int(args[3]) + elif sub == "LOAD": + state.pso["load"] = int(args[3]) + elif sub == "RANGE": + low, high = (float(v) for v in " ".join(args[3:]).split(",")) + state.pso["range"] = (low, high) + elif sub == "OFF": + state.pso["range"] = None + return "" + if cmd == "PSOOUTPUT": + state.pso["mode"] = args[1].lower() + return "" + if cmd == "DATAACQ": + # DATAACQ 1 TRIGGER | INPUT | ARRAY , | OFF + sub = args[2].upper() + acq = state.dataacq + if sub == "OFF": + acq.update(armed=False, trigger=None, inputs=set(), n_elements=0) + elif sub == "TRIGGER": + acq["trigger"] = int(args[3]) + elif sub == "INPUT": + ch = int(args[3]) + if ch not in state.channel_axis: + raise _TaskFault(f"No simulated axis on encoder channel {ch}.") + acq["inputs"].add(ch) + elif sub == "ARRAY": + _, n_elements = (int(v) for v in " ".join(args[3:]).split(",")) + acq.update(armed=True, n_elements=n_elements) + return "" + if cmd == "ARRAY": + # ARRAY READ $global[] [FAST] + base = int(_GLOBAL_RE.match(args[2]).group(1)) + start, count = int(args[3]), int(args[4]) + for i in range(count): + state.globals[base + i] = state.drive_array[start + i] + return "" logger.warning(f"[sim a3200] {self.host}:{self.port} unhandled command '{line}'") return None + def _psocontrol(self, args): + state = self.state + mode = args[1].upper() + if mode == "RESET": + state.pso.update(armed=False, mode=None, channel=None, range=None, load=0) + elif mode == "ARM": + state.pso["armed"] = True + elif mode == "OFF": + state.pso["armed"] = False + elif mode == "FIRE": + acq = state.dataacq + if acq["armed"] and acq["trigger"] == 6: + state.latch() + acq["armed"] = False + return "" + + def _evaluate(self, expression: str) -> float: + match = _UNITSTOCOUNTS_RE.match(expression) + if match: + return float(match.group(2)) * self.state.axis(match.group(1)).counts_per_unit + return float(expression) + @staticmethod def _require_axes(args): if not args: @@ -219,6 +361,8 @@ class SimA3200Socket(SimSocketBase): def _status_item(self, item: str) -> str: parts = [p.strip() for p in item.split(",")] + if len(parts) == 1 and _GLOBAL_RE.match(parts[0]): + return f"{self.state.globals.get(int(_GLOBAL_RE.match(parts[0]).group(1)), 0.0):.6f}" if len(parts) < 2: raise ValueError(f"Unsupported status item ({item})") ax = self.state.axis(parts[0]) @@ -262,11 +406,15 @@ class SimA3200Motor(A3200Motor): egu="mm", tolerance=0.001, require_homed=True, + acceleration=None, + encoder_channel=None, device_manager=None, sim_initial_position=None, sim_enabled=True, sim_homed=True, sim_home_velocity=None, + sim_counts_per_unit=None, + sim_counter_offset=None, **kwargs, ): super().__init__( @@ -282,6 +430,8 @@ class SimA3200Motor(A3200Motor): egu=egu, tolerance=tolerance, require_homed=require_homed, + acceleration=acceleration, + encoder_channel=encoder_channel, socket_cls=SimA3200Socket, device_manager=device_manager, **kwargs, @@ -295,4 +445,42 @@ class SimA3200Motor(A3200Motor): enabled=sim_enabled, homed=sim_homed, home_velocity=sim_home_velocity, + counts_per_unit=sim_counts_per_unit, + counter_offset=sim_counter_offset, + encoder_channel=encoder_channel, ) + + +class SimA3200PositionCapture(A3200PositionCapture): + """A3200PositionCapture connected to the simulated A3200 controller.""" + + def __init__( + self, + prefix="", + *, + name, + host="localhost", + port=8000, + channels=None, + drive_axis="X", + trigger_input=0, + device_manager=None, + scan_info=None, + **kwargs, + ): + super().__init__( + prefix, + name=name, + host=host, + port=port, + channels=channels, + drive_axis=drive_axis, + trigger_input=trigger_input, + socket_cls=SimA3200Socket, + device_manager=device_manager, + scan_info=scan_info, + **kwargs, + ) + state = SimStateRegistry.get(SimA3200State, host, port) + for channel, axis in self.channels.items(): + state.channel_axis[channel] = axis.upper() diff --git a/csaxs_bec/scans/__init__.py b/csaxs_bec/scans/__init__.py index 60909788..802a5c1d 100644 --- a/csaxs_bec/scans/__init__.py +++ b/csaxs_bec/scans/__init__.py @@ -2,4 +2,5 @@ from .flomni_fermat_scan import FlomniFermatScan from .lamni_fermat_scan import LamniFermatScan from .lamni_move_to_scan_center import LamniMoveToScanCenter from .omny_fermat_scan import OmnyFermatScan +from .scans_v4.aerotech_cont_grid import AerotechContGrid from .scans_v4.cont_grid import ContGrid diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py new file mode 100644 index 00000000..338f5407 --- /dev/null +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -0,0 +1,369 @@ +""" +Continuous grid scan for Aerotech A3200 stages with a position triggered line start. + +Same geometry as cont_grid (fast axis moving continuously, step axis stepping between +lines), but the start of each line is triggered in hardware: the A3200 PSO output goes +high when the fast axis enters the line window and triggers DDG1 (EXT input, rising edge), +which runs the shutter / detector / MCS sequence. With a position capture device, the +encoder positions of the axes are latched on each detector trigger (Npaq UINT1 input) +and published per line. + +Wiring: + Npaq PSO output (J7) -> DDG1 EXT/EN + detector trigger (DDG2 AB) -> Npaq UINT1 (J12 pins 1/2, 5 V opto input) + +Timing per line: + trigger position = fast_start - velocity * shutter_open_delay, so the first exposure + starts at fast_start; the fast axis starts one ramp distance (plus margin) before the + trigger position and stops one ramp distance after the last exposure. + +Scan procedure: + - prepare_scan + - open_scan + - stage + - pre_scan + - scan_core + - at_each_point (called by scan_core for each line) + - post_scan + - unstage + - close_scan + - on_exception (called if any exception is raised during the scan) +""" + +from __future__ import annotations + +from copy import deepcopy +from typing import Annotated, TypedDict + +import numpy as np +from bec_lib.device import DeviceBase +from bec_lib.logger import bec_logger +from bec_lib.scan_args import DefaultArgType, ScanArgument +from bec_server.scan_server.errors import ScanAbortion +from bec_server.scan_server.scans import position_generators +from bec_server.scan_server.scans.scan_base import ScanBase, ScanType +from bec_server.scan_server.scans.scan_modifier import scan_hook + +from csaxs_bec.scans.scan_customization.scan_components import CsaxsBecScanComponents + +logger = bec_logger.logger + +# DDG trigger sources (csaxs_bec.devices.epics.delay_generator_csaxs.TRIGGERSOURCE) +DDG_EXT_RISING_EDGE = 1 +DDG_SINGLE_SHOT = 5 + +# Extra run-up / run-out beyond the ramp distance, as a fraction of it, and as settling time +RAMP_MARGIN_FRACTION = 0.2 +SETTLE_TIME = 0.05 + + +class AerotechLineParameter(TypedDict): + """Line geometry of the fast axis, computed in prepare_scan.""" + + original_velocity: float | None + target_velocity: float | None + acceleration: float | None + shutter_open_delay: float | None + trigger_position: float | None + exposure_end: float | None + line_start: float | None + line_end: float | None + num_lines: int | None + + +class AerotechContGrid(ScanBase): + # One DDG1 sequence per line, like cont_grid: the MCS card and DDG1 count each line as + # one point with frames_per_trigger frames. + scan_type = ScanType.SOFTWARE_TRIGGERED + scan_name = "aerotech_cont_grid" + + gui_config = { + "Scan Parameters": [ + "fast_axis", + "fast_start", + "fast_end", + "fast_step_size", + "stepper_axis", + "stepper_start", + "stepper_stop", + "stepper_step_size", + ], + "Acquisition Parameters": ["exp_time", "relative", "capture_positions"], + } + + def __init__( + # fmt: off + self, + fast_axis: Annotated[DeviceBase, ScanArgument(display_name="Fast Axis", description="A3200 axis with continuous motion and PSO line trigger.")], + fast_start: Annotated[float, ScanArgument(display_name="Fast Start", description="Start position for measurement points of the fast axis.")], + fast_end: Annotated[float, ScanArgument(display_name="Fast End", description="End position for measurement points of the fast axis.")], + fast_step_size: Annotated[float, ScanArgument(display_name="Fast Step Size", description="Distance travelled by the fast axis during one exposure.")], + stepper_axis: Annotated[DeviceBase, ScanArgument(display_name="Step Axis", description="Step axis of the grid scan, stepping through the lines.")], + stepper_start: Annotated[float, ScanArgument(display_name="Step Start", description="Start position of the step axis.")], + stepper_stop: Annotated[float, ScanArgument(display_name="Step Stop", description="End position of the step axis.")], + stepper_step_size: Annotated[float, ScanArgument(display_name="Step Step Size", description="Step size of the step axis in units of the motor.")], + exp_time: DefaultArgType.ExposureTime, + relative: DefaultArgType.Relative = False, + capture_positions: bool = True, + capture_device: str = "aerocapture", + **kwargs, + # fmt: on + ): + """ + Continuous grid scan for A3200 stages: the PSO output of the fast axis triggers + DDG1 at the start of each line, optionally the axis positions are latched at each + detector trigger. + + Args: + fast_axis (DeviceBase): A3200 axis with continuous motion (needs encoder_channel and acceleration configured). + fast_start (float): Start position for measurement points of the fast axis. + fast_end (float): End position for measurement points of the fast axis. + fast_step_size (float): Distance travelled by the fast axis during one exposure. + stepper_axis (DeviceBase): Step axis of the grid scan, stepping through the lines. + stepper_start (float): Start position of the step axis. + stepper_stop (float): End position of the step axis. + stepper_step_size (float): Step size of the step axis in units of the motor. + exp_time (float): Exposure time in seconds + relative (bool): Whether the positions are relative to the current position. Default is False. + capture_positions (bool): Latch the axis positions at each detector trigger. Default is True. + capture_device (str): Name of the A3200PositionCapture device. Default is "aerocapture". + + Returns: + ScanReport + """ + super().__init__(**kwargs) + self.components = CsaxsBecScanComponents(self) + self._baseline_readout_status = None + self.fast_axis = fast_axis + if fast_end < fast_start: # the line always runs in positive direction + fast_start, fast_end = fast_end, fast_start + self.fast_start = fast_start + self.fast_end = fast_end + self.fast_step_size = fast_step_size + self.stepper_axis = stepper_axis + self.stepper_start = stepper_start + self.stepper_stop = stepper_stop + self.stepper_step_size = stepper_step_size + self.relative = relative + self.exp_time = exp_time + self.capture_positions = capture_positions + self.ddg1 = self.device_manager.devices["ddg1"] + self.mcs = self.device_manager.devices["mcs"] + self.capture = None + if capture_positions: + if capture_device not in self.device_manager.devices: + raise ScanAbortion( + f"Position capture device '{capture_device}' not found." + " Use capture_positions=False to scan without it." + ) + self.capture = self.device_manager.devices[capture_device] + + self.motors = [fast_axis, stepper_axis] + self.frames_per_line = 0 + self._line: AerotechLineParameter = AerotechLineParameter() + self.update_scan_info(relative=relative, exp_time=exp_time) + + @scan_hook + def prepare_scan(self): + """Compute positions and line geometry, start the premove.""" + self._check_motor_inputs() + self.frames_per_line = ( + int(np.ceil(np.abs(self.fast_end - self.fast_start) / self.fast_step_size)) + 1 + ) # include last point + self._line["num_lines"] = ( + int(np.ceil(np.abs(self.stepper_stop - self.stepper_start) / self.stepper_step_size)) + + 1 + ) # include last point + positions = position_generators.nd_grid_positions( + [ + (self.fast_start, self.fast_end, self.frames_per_line), + (self.stepper_start, self.stepper_stop, self._line["num_lines"]), + ], + snaked=False, + ) + + if self.relative: + self.start_positions = self.components.get_start_positions(self.motors) + positions += self.start_positions + self.fast_start += self.start_positions[0] + self.fast_end += self.start_positions[0] + self.stepper_start += self.start_positions[1] + self.stepper_stop += self.start_positions[1] + + self._compute_line_geometry() + + # One readout per line: the end point of each line, as in cont_grid + self.positions = deepcopy(positions[(self.frames_per_line - 1) :: self.frames_per_line, :]) + + self.actions.set_device_readout_priority(self.motors, priority="monitored") + self.update_scan_info( + positions=self.positions, + num_points=len(self.positions), + frames_per_trigger=self.frames_per_line, + computed_positions=positions, + num_lines=self._line["num_lines"], + ) + + self.actions.add_scan_report_instruction_device_progress(self.mcs) + self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) + self._premove_motor_status = self.actions.set( + self.motors, [self._line["line_start"], self.stepper_start], wait=False + ) + + @scan_hook + def open_scan(self): + """Open the scan.""" + self.actions.open_scan() + + @scan_hook + def stage(self): + """Stage all devices. DDG1 is set to its default (single shot) trigger here.""" + self.actions.stage_all_devices() + + @scan_hook + def pre_scan(self): + """ + Calibrate the position capture while DDG1 still ignores external triggers (the + calibration fires the PSO output once), then switch DDG1 to the external trigger. + """ + self._premove_motor_status.wait() + if self.capture is not None: + self.capture.calibrate() + self.ddg1.set_trigger(DDG_EXT_RISING_EDGE) + self.actions.pre_scan_all_devices() + + @scan_hook + def scan_core(self): + """One continuous line per step axis position.""" + for line_index in range(self._line["num_lines"]): + status_motor = self.actions.set( + self.motors, [self._line["line_start"], self.positions[line_index][1]] + ) + status_motor.wait() + status_mcs = self.ddg1.prepare_mcs_on_trigger() + status_mcs.wait() + mcs_acquiring_status = self.mcs.get_transition_status( + signal_name="acquiring", transition=[1, 0] + ) + self.at_each_point( + motors=[self.fast_axis], + positions=np.array([self._line["line_end"]]), + status_mcs=mcs_acquiring_status, + ) + + @scan_hook + def at_each_point( + self, + motors: list[str | DeviceBase], + positions: np.ndarray, + status_mcs, + last_positions: np.ndarray | None = None, + ): + """ + Arm the line trigger (and the position capture), run the line at scan velocity, + wait for the MCS card, then read the captured positions. + """ + if self.capture is not None: + self.capture.arm(self.frames_per_line) + self.fast_axis.arm_line_trigger(self._line["trigger_position"], self._line["exposure_end"]) + self.fast_axis.motor_velocity.set(self._line["target_velocity"]).wait(timeout=5) + try: + move_status = self.actions.set(motors, positions, wait=False) + while not move_status.done: + self.actions.read_monitored_devices(wait=True) + try: + move_status.wait(timeout=0.5) + except TimeoutError: + continue + finally: + self.fast_axis.motor_velocity.set(self._line["original_velocity"]).wait(timeout=5) + self.fast_axis.disarm_line_trigger() + + try: + status_mcs.wait(timeout=3) + except TimeoutError as exc: + raise ScanAbortion( + "MCS card did not go back to DONE after the line. Was the line trigger" + " received? Check the PSO output -> DDG1 EXT wiring and the PSO interlock." + ) from exc + + if self.capture is not None: + self.capture.read_line() + + @scan_hook + def post_scan(self): + """Restore DDG1 trigger and fast axis velocity, complete devices.""" + self._restore() + status = self.actions.complete_all_devices(wait=False) + if self.relative: + self.components.move_and_wait(self.motors, self.start_positions) + status.wait() + + @scan_hook + def unstage(self): + """Unstage all devices.""" + self.actions.unstage_all_devices() + + @scan_hook + def close_scan(self): + """Close the scan.""" + if self._baseline_readout_status is not None: + self._baseline_readout_status.wait() + self.actions.close_scan() + self.actions.check_for_unchecked_statuses() + + @scan_hook + def on_exception(self, exception: Exception): + """Return to a safe state: no external DDG1 trigger, PSO off, velocity restored.""" + self._restore() + if self.relative: + self.components.move_and_wait(self.motors, self.start_positions) + + ####################################################### + ######### Helper methods for the scan logic ########### + ####################################################### + + def _check_motor_inputs(self): + if np.isclose(self.fast_start, self.fast_end, atol=self.fast_step_size): + raise ScanAbortion( + f"Fast stop {self.fast_end} and fast start {self.fast_start} positions are too close for the given step size {self.fast_step_size}." + ) + if np.isclose(self.stepper_start, self.stepper_stop, atol=self.stepper_step_size): + raise ScanAbortion( + f"Stepper stop {self.stepper_stop} and stepper start {self.stepper_start} positions are too close for the given step size {self.stepper_step_size}." + ) + + def _compute_line_geometry(self): + """Velocity, trigger position and run-up / run-out of the fast axis.""" + line = self._line + line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() + line["original_velocity"] = self.fast_axis.motor_velocity.get() + line["acceleration"] = self.fast_axis.motor_acceleration.get() + + velocity = self.fast_step_size / self.exp_time + if velocity > line["original_velocity"]: + raise ScanAbortion( + f"Requested velocity of {velocity} exceeds the velocity {line['original_velocity']}" + f" configured for {self.fast_axis.name}." + ) + if not line["acceleration"] or line["acceleration"] <= 0: + raise ScanAbortion( + f"No acceleration configured for {self.fast_axis.name}; it is needed for the" + " run-up distance of the line." + ) + ramp_distance = velocity**2 / (2 * line["acceleration"]) + margin = RAMP_MARGIN_FRACTION * ramp_distance + velocity * SETTLE_TIME + + line["target_velocity"] = velocity + line["trigger_position"] = self.fast_start - velocity * line["shutter_open_delay"] + line["exposure_end"] = self.fast_start + self.frames_per_line * self.fast_step_size + line["line_start"] = line["trigger_position"] - ramp_distance - margin + line["line_end"] = line["exposure_end"] + ramp_distance + margin + + def _restore(self): + try: + self.ddg1.set_trigger(DDG_SINGLE_SHOT) + finally: + if self._line.get("original_velocity") is not None: + self.fast_axis.motor_velocity.put(self._line["original_velocity"]) + self.fast_axis.disarm_line_trigger() diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index eada5f51..9567841c 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -10,7 +10,7 @@ from csaxs_bec.devices.aerotech.a3200 import ( A3200Motor, decode_axis_fault, ) -from csaxs_bec.devices.sim.sim_a3200 import SimA3200Motor, SimA3200State +from csaxs_bec.devices.sim.sim_a3200 import SimA3200Motor, SimA3200PositionCapture, SimA3200State from csaxs_bec.devices.sim.sim_socket import SimStateRegistry _HOST = "test-a3200.psi.ch" @@ -205,3 +205,112 @@ def test_sim_two_axes_share_one_controller(): assert x.readback.get() == pytest.approx(1.0) assert y.readback.get() == pytest.approx(2.0) x.controller.describe() + + +# --- line trigger and position capture (simulated controller) ---------------- + + +def _sim_state(): + return SimStateRegistry.get(SimA3200State, _HOST, _PORT) + + +def _capture_setup(): + x = _sim_motor( + "X", axis_nr=0, encoder_channel=0, sim_counts_per_unit=20000, sim_counter_offset=777 + ) + y = _sim_motor( + "Y", axis_nr=1, encoder_channel=1, sim_initial_position=0.3, sim_counter_offset=-55 + ) + capture = SimA3200PositionCapture( + name="aerocapture", + host=_HOST, + port=_PORT, + channels={0: "X", 1: "Y"}, + device_manager=DMMock(), + ) + capture.wait_for_connection() + return x, y, capture + + +def test_counts_per_unit_is_read_once(): + x, _, _ = _capture_setup() + assert x.controller.counts_per_unit("X") == 20000 + x.controller.command_history.clear() + assert x.controller.counts_per_unit("X") == 20000 + assert not x.controller.command_history + + +def test_ramp_rate_is_set_before_the_move(): + motor = _sim_motor(acceleration=50.0) + motor.move(1.0, wait=True) + history = list(motor.controller.command_history) + ramp = history.index("[PUT]: RAMP RATE X 50.000000") + move = next(i for i, c in enumerate(history) if c.startswith("[PUT]: MOVEABS X")) + assert ramp < move + assert _sim_state().axis("X").ramp_rate == 50.0 + + +def test_line_trigger_window_is_relative_to_current_position(): + x, _, _ = _capture_setup() + x.move(-1.0, wait=True) + x.arm_line_trigger(0.0, 1.0) + assert _sim_state().pso["range"] == (20000.0, 40000.0) + assert _sim_state().pso["armed"] + x.move(2.0, wait=True) + assert _sim_state().pso_window_entries == 1 + x.disarm_line_trigger() + assert not _sim_state().pso["armed"] + + +def test_line_trigger_needs_encoder_channel(): + motor = _sim_motor() + with pytest.raises(A3200Error, match="encoder_channel"): + motor.arm_line_trigger(0.0, 1.0) + + +def test_capture_calibration_removes_counter_offset(): + _, _, capture = _capture_setup() + offsets = capture.calibrate() + assert offsets[0] == pytest.approx(-777 / 20000) + assert offsets[1] == pytest.approx(55 / 10000) + + +def test_capture_line_publishes_positions(): + x, _, capture = _capture_setup() + capture.calibrate() + x.move(-1.0, wait=True) + capture.arm(5) + x.arm_line_trigger(0.0, 1.0) + x.move(2.0, wait=True) + + result = capture.read_line() + + assert result["enc0"] == pytest.approx([0.0, 0.2, 0.4, 0.6, 0.8]) + assert result["enc1"] == pytest.approx([0.3] * 5) + msg = capture.positions.get() + assert set(msg.signals) == {"aerocapture_positions_enc0", "aerocapture_positions_enc1"} + assert msg.metadata["async_update"] == {"type": "add", "max_shape": [None]} + + +def test_capture_reads_in_chunks(): + x, _, capture = _capture_setup() + capture.controller.array_read_chunk = 3 + x.move(-1.0, wait=True) + capture.arm(4) + x.arm_line_trigger(0.0, 1.0) + x.move(2.0, wait=True) + result = capture.read_line() + assert len(result["enc0"]) == 4 + reads = [c for c in capture.controller.command_history if "ARRAY X READ" in c] + assert reads == [ + "[PUT]: ARRAY X READ $global[0] 0 3 FAST", + "[PUT]: ARRAY X READ $global[0] 3 3 FAST", + "[PUT]: ARRAY X READ $global[0] 6 2 FAST", + ] + + +def test_capture_needs_two_channels(): + with pytest.raises(ValueError, match="two encoder channels"): + SimA3200PositionCapture( + name="aerocapture", host=_HOST, port=_PORT, channels={0: "X"}, device_manager=DMMock() + ) diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py new file mode 100644 index 00000000..2962a448 --- /dev/null +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -0,0 +1,185 @@ +from unittest import mock + +import numpy as np +import pytest +from bec_server.scan_server.errors import ScanAbortion +from bec_server.scan_server.tests.scan_fixtures import MockCustomDevice +from bec_server.scan_server.tests.scan_hook_tests import ( + DEFAULT_HOOK_TESTS, + PREMOVE_HOOK_TESTS, + run_scan_tests, +) + +AEROTECH_CONT_GRID_HOOK_TESTS = [*DEFAULT_HOOK_TESTS, *PREMOVE_HOOK_TESTS] + + +def _signal(obj_name, kind="config"): + return {"obj_name": obj_name, "kind_str": kind, "describe": {"precision": 3}} + + +def _add_devices(device_manager, capture=True, acceleration=100.0): + aerox = MockCustomDevice( + "aerox", + device_info={ + "signals": { + "readback": _signal("aerox", "hinted"), + "motor_velocity": _signal("aerox_motor_velocity"), + "motor_acceleration": _signal("aerox_motor_acceleration"), + } + }, + signal_read_values={ + "aerox": 0.0, + "aerox_motor_velocity": 10.0, + "aerox_motor_acceleration": acceleration, + }, + ) + aerox.arm_line_trigger = mock.MagicMock() + aerox.disarm_line_trigger = mock.MagicMock() + aeroy = MockCustomDevice( + "aeroy", + device_info={"signals": {"readback": _signal("aeroy", "hinted")}}, + signal_read_values={"aeroy": 0.0}, + ) + ddg1 = MockCustomDevice( + "ddg1", + device_info={"signals": {"readback": _signal("ddg1", "hinted")}}, + signal_read_values={"ddg1": 0.0}, + ) + ddg1.get_shutter_to_open_delay = mock.MagicMock(return_value=2e-3) + ddg1.set_trigger = mock.MagicMock() + mcs = MockCustomDevice( + "mcs", + device_info={"signals": {"readback": _signal("mcs", "hinted")}}, + signal_read_values={"mcs": 0.0}, + ) + devices = [aerox, aeroy, ddg1, mcs] + if capture: + aerocapture = MockCustomDevice( + "aerocapture", + device_info={"signals": {"readback": _signal("aerocapture", "hinted")}}, + signal_read_values={"aerocapture": 0.0}, + ) + for method in ("calibrate", "arm", "read_line", "disarm"): + setattr(aerocapture, method, mock.MagicMock()) + devices.append(aerocapture) + for device in devices: + device_manager.add_device(device, replace=True) + + +def _assemble(v4_scan_assembler, device_manager, **kwargs): + _add_devices( + device_manager, **{k: kwargs.pop(k) for k in ("capture", "acceleration") if k in kwargs} + ) + return v4_scan_assembler( + "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.5, "aeroy", -2.0, 2.0, 2, exp_time=0.1, **kwargs + ) + + +@pytest.mark.parametrize(("hook_name", "hook_tests"), AEROTECH_CONT_GRID_HOOK_TESTS) +def test_aerotech_cont_grid_default_hooks( + v4_scan_assembler, device_manager, nth_done_status_mock, hook_name, hook_tests +): + scan = _assemble(v4_scan_assembler, device_manager) + run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock) + + +def test_line_geometry(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + line = scan._line + + velocity = 0.5 / 0.1 + ramp = velocity**2 / (2 * 100.0) + margin = 0.2 * ramp + velocity * 0.05 + assert scan.frames_per_line == 5 + assert line["num_lines"] == 3 + assert line["target_velocity"] == pytest.approx(velocity) + # first exposure starts at fast_start: trigger one shutter delay earlier + assert line["trigger_position"] == pytest.approx(-1.0 - velocity * 2e-3) + assert line["exposure_end"] == pytest.approx(-1.0 + 5 * 0.5) + assert line["line_start"] == pytest.approx(line["trigger_position"] - ramp - margin) + assert line["line_end"] == pytest.approx(line["exposure_end"] + ramp + margin) + assert np.array_equal(scan.positions, np.array([[1.0, -2.0], [1.0, 0.0], [1.0, 2.0]])) + assert scan.scan_info.frames_per_trigger == 5 + + +def test_velocity_above_configured_velocity_aborts(v4_scan_assembler, device_manager): + _add_devices(device_manager) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -10.0, 10.0, 2.0, "aeroy", -2.0, 2.0, 2, exp_time=0.1 + ) + with pytest.raises(ScanAbortion, match="exceeds the velocity"): + scan.prepare_scan() + + +def test_missing_acceleration_aborts(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, acceleration=0.0) + with pytest.raises(ScanAbortion, match="No acceleration"): + scan.prepare_scan() + + +def test_missing_capture_device_aborts(v4_scan_assembler, device_manager): + device_manager.devices.pop("aerocapture", None) + with pytest.raises(ScanAbortion, match="aerocapture"): + _assemble(v4_scan_assembler, device_manager, capture=False) + + +def test_scan_without_capture(v4_scan_assembler, device_manager): + device_manager.devices.pop("aerocapture", None) + scan = _assemble(v4_scan_assembler, device_manager, capture=False, capture_positions=False) + assert scan.capture is None + + +def test_pre_scan_calibrates_before_ddg_goes_external( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + order = mock.MagicMock() + scan.capture = order.capture + scan.ddg1 = order.ddg1 + scan._premove_motor_status = nth_done_status_mock(resolve_after=1) + scan.actions.pre_scan_all_devices = mock.MagicMock() + + scan.pre_scan() + + assert order.mock_calls[:2] == [mock.call.capture.calibrate(), mock.call.ddg1.set_trigger(1)] + + +def test_at_each_point_arms_trigger_and_reads_capture( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + scan.capture = mock.MagicMock() + scan.fast_axis = mock.MagicMock() + scan.actions.set = mock.MagicMock(return_value=nth_done_status_mock(resolve_after=1)) + scan.actions.read_monitored_devices = mock.MagicMock() + + scan.at_each_point( + [scan.fast_axis], + np.array([scan._line["line_end"]]), + status_mcs=nth_done_status_mock(resolve_after=1), + ) + + scan.capture.arm.assert_called_once_with(5) + scan.fast_axis.arm_line_trigger.assert_called_once_with( + scan._line["trigger_position"], scan._line["exposure_end"] + ) + velocity_sets = [c.args[0] for c in scan.fast_axis.motor_velocity.set.call_args_list] + assert velocity_sets == [scan._line["target_velocity"], scan._line["original_velocity"]] + scan.fast_axis.disarm_line_trigger.assert_called_once_with() + scan.capture.read_line.assert_called_once_with() + + +def test_on_exception_restores_ddg_and_pso(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + scan.ddg1 = mock.MagicMock() + scan.fast_axis = mock.MagicMock() + + scan.on_exception(RuntimeError("test")) + + scan.ddg1.set_trigger.assert_called_once_with(5) + scan.fast_axis.disarm_line_trigger.assert_called_once_with() + scan.fast_axis.motor_velocity.put.assert_called_once_with(10.0) -- 2.54.0 From bf77d6f6fe760f194d0aa1ec36685157a6fe9c34 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Tue, 27 Oct 2026 18:53:55 +0100 Subject: [PATCH 03/52] feat(aerotech): RIO burst box as trigger source for aerotech_cont_grid - GalilRIOBurst (galil_rio_ddg.py): Galil RIO-47120 running RioDDG/rio_ddg.dmc (OMNY Galil repo) as line burst generator: burst mode switching, arm_burst, burst status, wait with pulse count check - aerotech_cont_grid: trigger_source="ddg" (beamline) | "rio" (test setup without DDG/MCS) - simulated RIO triggered by the PSO window of the simulated A3200 - configs: sastt_aerotech_test.yaml (A3200 + RIO), RIO in the simulated config - tests Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- .../device_configs/sastt_aerotech_test.yaml | 134 +++++++++++++++ .../simulated_sastt_aerotech.yaml | 18 ++ csaxs_bec/devices/aerotech/README.md | 24 ++- csaxs_bec/devices/omny/galil/galil_rio_ddg.py | 129 +++++++++++++++ csaxs_bec/devices/sim/sim_a3200.py | 11 +- csaxs_bec/devices/sim/sim_rio_ddg.py | 156 ++++++++++++++++++ .../scans/scans_v4/aerotech_cont_grid.py | 89 ++++++++-- tests/tests_devices/test_galil_rio_ddg.py | 142 ++++++++++++++++ .../test_aerotech_cont_grid_scan.py | 116 +++++++++++++ 9 files changed, 797 insertions(+), 22 deletions(-) create mode 100644 csaxs_bec/device_configs/sastt_aerotech_test.yaml create mode 100644 csaxs_bec/devices/omny/galil/galil_rio_ddg.py create mode 100644 csaxs_bec/devices/sim/sim_rio_ddg.py create mode 100644 tests/tests_devices/test_galil_rio_ddg.py diff --git a/csaxs_bec/device_configs/sastt_aerotech_test.yaml b/csaxs_bec/device_configs/sastt_aerotech_test.yaml new file mode 100644 index 00000000..c98735e1 --- /dev/null +++ b/csaxs_bec/device_configs/sastt_aerotech_test.yaml @@ -0,0 +1,134 @@ +############################################################ +# SAXS-TT Aerotech stages, TEST SETUP without beamline timing (no DDG / MCS / shutter): +# Galil RIO-47120 trigger box (program RioDDG/rio_ddg.dmc, OMNY Galil repo) as line +# burst generator. Scan: aerotech_cont_grid(..., trigger_source="rio") +# +# Wiring: Npaq PSO output (J7) -> RIO analog input 1 (TTL ok, threshold 3 V) +# RIO DO1 (12-24 V, series resistor!) or AO5 (0/5 V) -> Npaq UINT1 (J12) +# +# A3200 setup: CommandSetup = 0x00010004 (enabled + Always Send EOS), CommandPort = 8000. +# TODO: host names of the A3200 PC and the RIO, axis names, encoder channels, +# velocities (= maximum scan velocity), accelerations, limits. +############################################################ + +aerox: + description: Aerotech X, ALS25020 linear stage + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: X + axis_nr: 0 + encoder_channel: 0 + acceleration: 100.0 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 10.0 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroy: + description: Aerotech Y, AVSI125 vertical lift + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: Y + axis_nr: 1 + encoder_channel: 1 + acceleration: 20.0 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 0.5 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroz: + description: Aerotech Z, ATS20010 linear stage + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: Z + axis_nr: 2 + encoder_channel: 2 + acceleration: 100.0 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 10.0 + egu: mm + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aerorot: + description: Aerotech U, ABRT-200 rotation stage (UU follows on the controller) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: U + axis_nr: 3 + encoder_channel: 3 + acceleration: 360.0 + host: a3200-saxstt.psi.ch + port: 8000 + velocity: 10.0 + egu: deg + tolerance: 0.001 + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aerocapture: + description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture + deviceConfig: + host: a3200-saxstt.psi.ch + port: 8000 + drive_axis: X + channels: + 0: X + 1: Y + 2: Z + 3: U + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: async + deviceTags: + - sastt_aerotech +rioddg: + description: Galil RIO-47120 trigger box, line burst generator (rio_ddg.dmc) + deviceClass: csaxs_bec.devices.omny.galil.galil_rio_ddg.GalilRIOBurst + deviceConfig: + host: rio-saxstt.psi.ch + port: 23 + min_period_ms: 20 + threshold: 3.0 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index d18b2971..80839527 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -1,6 +1,8 @@ ############################################################ # Simulated SAXS-TT Aerotech stages (sim_a3200), same devices as sastt_aerotech.yaml. # Axes start enabled and homed (sim_enabled / sim_homed in deviceConfig to change). +# rioddg: simulated RIO burst box, triggered by the PSO output of the simulated A3200. +# Scan without hardware: aerotech_cont_grid(..., trigger_source="rio") ############################################################ aerox: @@ -110,3 +112,19 @@ aerocapture: readoutPriority: async deviceTags: - sastt_aerotech +rioddg: + description: Galil RIO-47120 trigger box, line burst generator (simulated) + deviceClass: csaxs_bec.devices.sim.sim_rio_ddg.SimGalilRIOBurst + deviceConfig: + host: sim-rio-saxstt + port: 23 + min_period_ms: 20 + threshold: 3.0 + sim_trigger_host: sim-a3200-saxstt + sim_trigger_port: 8000 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 347dae79..b678f199 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -13,8 +13,13 @@ A3200 ASCII command interface on the Windows PC (TCP, default port 8000). | - | UU | ADRS-150 | slipring follower of U, handled on the controller | | aerocapture | - | - | position capture (DATAACQ) of X, Y, Z, U | -Configs: `device_configs/sastt_aerotech.yaml` (hardware), -`device_configs/simulated_omny/simulated_sastt_aerotech.yaml` (simulation, `sim_a3200.py`). +Configs: + +| Config | Use | +|--------|-----| +| `device_configs/sastt_aerotech.yaml` | beamline: A3200 + position capture, timing by DDG1/DDG2/MCS | +| `device_configs/sastt_aerotech_test.yaml` | test setup: A3200 + position capture + Galil RIO burst box (`rioddg`) | +| `device_configs/simulated_omny/simulated_sastt_aerotech.yaml` | simulation of the test setup (`sim_a3200.py`, `sim_rio_ddg.py`) | Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface"), copy at `~/gitea/SAXS-TT_docs` on the workstation. @@ -79,3 +84,18 @@ Commissioning of the scan: as scratch variable. 6. Check one line with the scope: PSO edge at the trigger position, number of UINT1 edges = frames per line, captured positions vs. requested positions. + +## Test setup without beamline timing: trigger_source="rio" + +`aerotech_cont_grid(..., trigger_source="rio")` replaces DDG1/DDG2/MCS by the Galil +RIO-47120 trigger box (device `rioddg`, class `GalilRIOBurst`, program +`RioDDG/rio_ddg.dmc` in the OMNY Galil repository, branch `feat/rio-ddg`): + +- pre_scan: capture calibration, then the RIO is switched to burst mode (`HX0`, `XQ#BURST,0`) +- per line: `arm_burst(frames_per_line, exp_time in ms)`, PSO line trigger -> RIO analog + input 1 -> pulses on DO1 / AO5 -> Npaq UINT1 (position capture); BEC waits until the + RIO reports all pulses sent +- post_scan / exception: RIO back to the legacy delay generator (`XQ#DDG,0`) +- exp_time in whole ms, >= 20 ms (opto outputs ~50 Hz); no shutter delay compensation + +In the simulation the RIO is triggered by the PSO window of the simulated A3200. diff --git a/csaxs_bec/devices/omny/galil/galil_rio_ddg.py b/csaxs_bec/devices/omny/galil/galil_rio_ddg.py new file mode 100644 index 00000000..dec38755 --- /dev/null +++ b/csaxs_bec/devices/omny/galil/galil_rio_ddg.py @@ -0,0 +1,129 @@ +""" +Galil RIO-47120 trigger box used as a line burst generator (stand-in for DDG1/DDG2 in +test setups without the beamline timing). + +Runs the program RioDDG/rio_ddg.dmc of the OMNY Galil repository. Thread 0 is either in +the legacy delay generator mode (#DDG, default after power up) or in the burst mode +(#BURST). In burst mode, per line BEC sets the number of pulses, period and width and arms +the box; on the rising edge of analog input 1 (line trigger, e.g. the Aerotech PSO output) +the program sends the pulses on digital output 1 and analog output 5. +""" + +from __future__ import annotations + +import time + +from bec_lib.logger import bec_logger + +from csaxs_bec.devices.omny.galil.galil_rio import GalilRIO + +logger = bec_logger.logger + + +class GalilRIOBurstError(Exception): + pass + + +class GalilRIOBurst(GalilRIO): + """ + Galil RIO trigger box with the line burst generator of rio_ddg.dmc. + + Args: + min_period_ms: shortest allowed pulse period; the opto outputs (DO1) have ~50 Hz + bandwidth, so 20 ms by default. + threshold: trigger threshold of analog input 1 in V. + """ + + USER_ACCESS = [ + "start_burst_mode", + "stop_burst_mode", + "arm_burst", + "disarm_burst", + "burst_status", + "burst_done", + "wait_burst_done", + ] + + def __init__( + self, + *, + name: str, + host: str, + device_manager, + port: int | None = None, + min_period_ms: int = 20, + threshold: float = 3.0, + **kwargs, + ): + super().__init__(name=name, host=host, device_manager=device_manager, port=port, **kwargs) + self.min_period_ms = int(min_period_ms) + self.threshold = float(threshold) + + def _get_var(self, var: str) -> float: + return float(self.controller.socket_put_and_receive(f"MG {var}").strip()) + + def _set_var(self, var: str, value) -> None: + self.controller.socket_put_confirmed(f"{var}={value}") + + def start_burst_mode(self) -> None: + """Switch thread 0 from the legacy delay generator to the burst generator.""" + self.controller.socket_put_confirmed("HX0") + self.controller.socket_put_confirmed("XQ#BURST,0") + time.sleep(0.05) + + def stop_burst_mode(self) -> None: + """Back to the legacy delay generator (#DDG).""" + self.controller.socket_put_confirmed("HX0") + self.controller.socket_put_confirmed("XQ#DDG,0") + + def arm_burst(self, n_pulses: int, period_ms: int, width_ms: int | None = None) -> None: + """Arm one burst of n_pulses, started by the next rising edge of the trigger input.""" + period_ms = int(round(period_ms)) + if width_ms is None: + width_ms = period_ms // 2 + width_ms = int(round(width_ms)) + if period_ms < self.min_period_ms: + raise GalilRIOBurstError( + f"Pulse period {period_ms} ms is below the minimum of {self.min_period_ms} ms." + ) + if not 1 <= width_ms < period_ms: + raise GalilRIOBurstError(f"Pulse width {width_ms} ms must be in [1, {period_ms}).") + if n_pulses < 1: + raise GalilRIOBurstError(f"Number of pulses must be at least 1, got {n_pulses}.") + if self._get_var("busy"): + raise GalilRIOBurstError("RIO is still sending the pulses of the previous line.") + self._set_var("nfr", int(n_pulses)) + self._set_var("per", period_ms) + self._set_var("wid", width_ms) + self._set_var("thr", self.threshold) + self._set_var("armed", 1) + + def disarm_burst(self) -> None: + self._set_var("armed", 0) + + def burst_status(self) -> dict: + reply = self.controller.socket_put_and_receive("MG armed, busy, nsent, nline") + armed, busy, nsent, nline = (float(v) for v in reply.split()) + return {"armed": bool(armed), "busy": bool(busy), "nsent": int(nsent), "nline": int(nline)} + + def burst_done(self) -> bool: + """True once the armed burst was triggered and all its pulses are sent.""" + status = self.burst_status() + return not status["armed"] and not status["busy"] + + def wait_burst_done(self, n_pulses: int, timeout: float) -> dict: + """Wait for the burst to finish, check that n_pulses were sent.""" + start = time.time() + while True: + status = self.burst_status() + if not status["armed"] and not status["busy"]: + break + if time.time() - start > timeout: + raise GalilRIOBurstError( + f"Burst not finished within {timeout} s: {status}. Was the line trigger" + " received on analog input 1?" + ) + time.sleep(0.05) + if status["nsent"] != n_pulses: + raise GalilRIOBurstError(f"Sent {status['nsent']} pulses instead of {n_pulses}.") + return status diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index 18f37910..52890a89 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -123,6 +123,8 @@ class SimA3200State: self.drive_array = [0.0] * 4096 self.pso = {"armed": False, "mode": None, "channel": None, "range": None, "load": 0} self.pso_window_entries = 0 # rising edges of the PSO window output (line triggers) + # callables(trigger_time) receiving the PSO output edge, e.g. a simulated trigger box + self.line_trigger_listeners = [] self.dataacq = {"armed": False, "trigger": None, "inputs": set(), "n_elements": 0} def counter(self, channel: int) -> int: @@ -140,7 +142,9 @@ class SimA3200State: value = self.counter(ch) self.drive_array[index * len(inputs) + i] = float(value) - def on_move_start(self, ax: _SimA3200Axis, start: float, target: float) -> None: + def on_move_start( + self, ax: _SimA3200Axis, start: float, target: float, velocity: float + ) -> None: """Simulate the line trigger and the detector exposures of a scan line.""" pso, acq = self.pso, self.dataacq if not (pso["armed"] and pso["mode"] == "window" and pso["range"] is not None): @@ -152,6 +156,9 @@ class SimA3200State: if not (min(start, target) <= low and high <= max(start, target)): return self.pso_window_entries += 1 + trigger_time = time.time() + abs(low - start) / abs(velocity) + for listener in list(self.line_trigger_listeners): + listener(trigger_time) if acq["armed"] and acq["trigger"] == 0 and acq["inputs"]: n_triggers = acq["n_elements"] // len(acq["inputs"]) step = (high - low) / max(n_triggers, 1) @@ -247,7 +254,7 @@ class SimA3200Socket(SimSocketBase): ax = self._ready_axis(name) start = ax.position() target = value if cmd == "MOVEABS" else start + value - state.on_move_start(ax, start, target) + state.on_move_start(ax, start, target, velocity) ax.start_move(target, velocity) return "" if cmd == "HOMEASYNC": diff --git a/csaxs_bec/devices/sim/sim_rio_ddg.py b/csaxs_bec/devices/sim/sim_rio_ddg.py new file mode 100644 index 00000000..33b563b1 --- /dev/null +++ b/csaxs_bec/devices/sim/sim_rio_ddg.py @@ -0,0 +1,156 @@ +""" +Simulated Galil RIO trigger box running RioDDG/rio_ddg.dmc (OMNY Galil repository). + +Implements the commands used by `GalilRIOBurst` and `GalilRIO`: variable assignments, +"MG var, var, ...", HX0, XQ#BURST,0, XQ#DDG,0, SB/CB, MG@OUT[n], MG@AN[...]. + +The line trigger (analog input 1) comes from a simulated A3200 controller: the device is +registered as listener of the PSO output of `SimA3200State` (host/port given in the +device config). A burst is evaluated lazily from the trigger time: armed is cleared at the +trigger, nsent counts the periods elapsed since then, busy is set until nfr * per ms. +""" + +from __future__ import annotations + +import re +import threading +import time + +from bec_lib.logger import bec_logger + +from csaxs_bec.devices.omny.galil.galil_rio_ddg import GalilRIOBurst +from csaxs_bec.devices.sim.sim_a3200 import SimA3200State +from csaxs_bec.devices.sim.sim_socket import SimSocketBase, SimStateRegistry + +logger = bec_logger.logger + +_ASSIGNMENT_RE = re.compile(r"^([A-Za-z][A-Za-z0-9]*)\s*=\s*(-?[0-9.]+)$") + + +class SimRioDDGState: + """Shared state of one simulated RIO with the rio_ddg.dmc program.""" + + def __init__(self, host, port): + self.host = host + self.port = port + self._lock = threading.RLock() + self.mode = "ddg" + self.vars = {} + self.digital_outputs = [0.0] * 16 + self.analog_inputs = [0.0] * 8 + self._burst = None # (trigger_time, nfr, per) + self._reset_burst_vars() + self.vars.update(nfr=1.0, per=100.0, wid=10.0, thr=3.0, delay=100.0) + + def _reset_burst_vars(self): + self.vars.update(armed=0.0, busy=0.0, nsent=0.0, nline=0.0) + self._burst = None + + def start(self, label: str): + with self._lock: + self.mode = "burst" if label == "#BURST" else "ddg" + if self.mode == "burst": + self._reset_burst_vars() + + def halt(self): + with self._lock: + self.mode = None + + def line_trigger(self, trigger_time: float): + """Rising edge on analog input 1 at trigger_time.""" + with self._lock: + self._refresh() + if self.mode != "burst" or not self.vars["armed"] or self._burst is not None: + return + self._burst = (trigger_time, int(self.vars["nfr"]), self.vars["per"]) + + def _refresh(self): + if self._burst is None: + return + trigger_time, nfr, per = self._burst + elapsed_ms = (time.time() - trigger_time) * 1000.0 + if elapsed_ms < 0: + return + self.vars["armed"] = 0.0 + if elapsed_ms >= nfr * per: + self.vars.update(busy=0.0, nsent=float(nfr), nline=self.vars["nline"] + 1) + self._burst = None + else: + self.vars.update(busy=1.0, nsent=float(min(nfr, int(elapsed_ms // per) + 1))) + + def get(self, var: str) -> float: + with self._lock: + self._refresh() + return self.vars.get(var, 0.0) + + def set(self, var: str, value: float): + with self._lock: + self._refresh() + self.vars[var] = value + + +class SimRioDDGSocket(SimSocketBase): + state_cls = SimRioDDGState + + def handle_command(self, line: str): + state: SimRioDDGState = self.state + if line == "HX0": + state.halt() + return ":" + if line.startswith("XQ"): + state.start(line[2:].split(",")[0].strip()) + return ":" + if line.startswith("SB") or line.startswith("CB"): + state.digital_outputs[int(line[2:])] = 1.0 if line.startswith("SB") else 0.0 + return ":" + if line.startswith("MG"): + items = [item.strip() for item in line[2:].split(",")] + values = [] + for item in items: + if item.startswith("@OUT["): + values.append(state.digital_outputs[int(item[5:].split("]")[0])]) + elif item.startswith("@AN["): + values.append(state.analog_inputs[int(item[4:].split("]")[0])]) + else: + values.append(state.get(item)) + return " ".join(f"{v:.4f}" for v in values) + match = _ASSIGNMENT_RE.match(line) + if match: + state.set(match.group(1), float(match.group(2))) + return ":" + logger.warning(f"[sim rio ddg] {self.host}:{self.port} unhandled command '{line}'") + return "?" + + +class SimGalilRIOBurst(GalilRIOBurst): + """GalilRIOBurst connected to a simulated RIO; its trigger input follows the PSO output + of the simulated A3200 controller at (sim_trigger_host, sim_trigger_port).""" + + def __init__( + self, + *, + name: str, + host: str, + device_manager, + port: int | None = None, + min_period_ms: int = 20, + threshold: float = 3.0, + sim_trigger_host: str | None = None, + sim_trigger_port: int = 8000, + **kwargs, + ): + super().__init__( + name=name, + host=host, + device_manager=device_manager, + port=port, + min_period_ms=min_period_ms, + threshold=threshold, + socket_cls=SimRioDDGSocket, + **kwargs, + ) + state = SimStateRegistry.get(SimRioDDGState, host, port or 23) + if sim_trigger_host is not None: + a3200 = SimStateRegistry.get(SimA3200State, sim_trigger_host, sim_trigger_port) + if state.line_trigger not in a3200.line_trigger_listeners: + a3200.line_trigger_listeners.append(state.line_trigger) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 338f5407..ba854088 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -8,10 +8,16 @@ which runs the shutter / detector / MCS sequence. With a position capture device encoder positions of the axes are latched on each detector trigger (Npaq UINT1 input) and published per line. -Wiring: +Wiring (trigger_source="ddg", beamline): Npaq PSO output (J7) -> DDG1 EXT/EN detector trigger (DDG2 AB) -> Npaq UINT1 (J12 pins 1/2, 5 V opto input) +Wiring (trigger_source="rio", test setup without beamline timing): + Npaq PSO output (J7) -> RIO analog input 1 + RIO digital output 1 / analog output 5 -> Npaq UINT1 (and detector, if any) + The RIO (program RioDDG/rio_ddg.dmc) sends frames_per_line pulses with period exp_time + (1 ms steps, >= 20 ms) on each line trigger; no DDG, MCS or shutter is used. + Timing per line: trigger position = fast_start - velocity * shutter_open_delay, so the first exposure starts at fast_start; the fast axis starts one ramp distance (plus margin) before the @@ -52,6 +58,8 @@ logger = bec_logger.logger DDG_EXT_RISING_EDGE = 1 DDG_SINGLE_SHOT = 5 +TRIGGER_SOURCES = ("ddg", "rio") + # Extra run-up / run-out beyond the ramp distance, as a fraction of it, and as settling time RAMP_MARGIN_FRACTION = 0.2 SETTLE_TIME = 0.05 @@ -64,6 +72,7 @@ class AerotechLineParameter(TypedDict): target_velocity: float | None acceleration: float | None shutter_open_delay: float | None + rio_period_ms: int | None trigger_position: float | None exposure_end: float | None line_start: float | None @@ -88,7 +97,7 @@ class AerotechContGrid(ScanBase): "stepper_stop", "stepper_step_size", ], - "Acquisition Parameters": ["exp_time", "relative", "capture_positions"], + "Acquisition Parameters": ["exp_time", "relative", "capture_positions", "trigger_source"], } def __init__( @@ -106,6 +115,8 @@ class AerotechContGrid(ScanBase): relative: DefaultArgType.Relative = False, capture_positions: bool = True, capture_device: str = "aerocapture", + trigger_source: str = "ddg", + rio_device: str = "rioddg", **kwargs, # fmt: on ): @@ -127,6 +138,8 @@ class AerotechContGrid(ScanBase): relative (bool): Whether the positions are relative to the current position. Default is False. capture_positions (bool): Latch the axis positions at each detector trigger. Default is True. capture_device (str): Name of the A3200PositionCapture device. Default is "aerocapture". + trigger_source (str): "ddg" (DDG1/DDG2 + MCS, beamline) or "rio" (RIO burst box, test setup). Default is "ddg". + rio_device (str): Name of the GalilRIOBurst device for trigger_source="rio". Default is "rioddg". Returns: ScanReport @@ -147,8 +160,19 @@ class AerotechContGrid(ScanBase): self.relative = relative self.exp_time = exp_time self.capture_positions = capture_positions - self.ddg1 = self.device_manager.devices["ddg1"] - self.mcs = self.device_manager.devices["mcs"] + if trigger_source not in TRIGGER_SOURCES: + raise ScanAbortion( + f"Unknown trigger_source '{trigger_source}', use one of {TRIGGER_SOURCES}." + ) + self.trigger_source = trigger_source + self.ddg1 = self.mcs = self.rio = None + if trigger_source == "ddg": + self.ddg1 = self.device_manager.devices["ddg1"] + self.mcs = self.device_manager.devices["mcs"] + else: + if rio_device not in self.device_manager.devices: + raise ScanAbortion(f"RIO burst device '{rio_device}' not found.") + self.rio = self.device_manager.devices[rio_device] self.capture = None if capture_positions: if capture_device not in self.device_manager.devices: @@ -204,7 +228,8 @@ class AerotechContGrid(ScanBase): num_lines=self._line["num_lines"], ) - self.actions.add_scan_report_instruction_device_progress(self.mcs) + if self.mcs is not None: + self.actions.add_scan_report_instruction_device_progress(self.mcs) self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) self._premove_motor_status = self.actions.set( self.motors, [self._line["line_start"], self.stepper_start], wait=False @@ -223,13 +248,16 @@ class AerotechContGrid(ScanBase): @scan_hook def pre_scan(self): """ - Calibrate the position capture while DDG1 still ignores external triggers (the - calibration fires the PSO output once), then switch DDG1 to the external trigger. + Calibrate the position capture while DDG1 / the RIO still ignore the line trigger + (the calibration fires the PSO output once), then enable the trigger receiver. """ self._premove_motor_status.wait() if self.capture is not None: self.capture.calibrate() - self.ddg1.set_trigger(DDG_EXT_RISING_EDGE) + if self.rio is not None: + self.rio.start_burst_mode() + else: + self.ddg1.set_trigger(DDG_EXT_RISING_EDGE) self.actions.pre_scan_all_devices() @scan_hook @@ -240,11 +268,13 @@ class AerotechContGrid(ScanBase): self.motors, [self._line["line_start"], self.positions[line_index][1]] ) status_motor.wait() - status_mcs = self.ddg1.prepare_mcs_on_trigger() - status_mcs.wait() - mcs_acquiring_status = self.mcs.get_transition_status( - signal_name="acquiring", transition=[1, 0] - ) + mcs_acquiring_status = None + if self.ddg1 is not None: + status_mcs = self.ddg1.prepare_mcs_on_trigger() + status_mcs.wait() + mcs_acquiring_status = self.mcs.get_transition_status( + signal_name="acquiring", transition=[1, 0] + ) self.at_each_point( motors=[self.fast_axis], positions=np.array([self._line["line_end"]]), @@ -265,6 +295,8 @@ class AerotechContGrid(ScanBase): """ if self.capture is not None: self.capture.arm(self.frames_per_line) + if self.rio is not None: + self.rio.arm_burst(self.frames_per_line, self._line["rio_period_ms"]) self.fast_axis.arm_line_trigger(self._line["trigger_position"], self._line["exposure_end"]) self.fast_axis.motor_velocity.set(self._line["target_velocity"]).wait(timeout=5) try: @@ -279,6 +311,18 @@ class AerotechContGrid(ScanBase): self.fast_axis.motor_velocity.set(self._line["original_velocity"]).wait(timeout=5) self.fast_axis.disarm_line_trigger() + if self.rio is not None: + self.rio.wait_burst_done( + self.frames_per_line, + timeout=3 + self.frames_per_line * self._line["rio_period_ms"] / 1000, + ) + else: + self._wait_mcs(status_mcs) + + if self.capture is not None: + self.capture.read_line() + + def _wait_mcs(self, status_mcs): try: status_mcs.wait(timeout=3) except TimeoutError as exc: @@ -287,9 +331,6 @@ class AerotechContGrid(ScanBase): " received? Check the PSO output -> DDG1 EXT wiring and the PSO interlock." ) from exc - if self.capture is not None: - self.capture.read_line() - @scan_hook def post_scan(self): """Restore DDG1 trigger and fast axis velocity, complete devices.""" @@ -336,7 +377,16 @@ class AerotechContGrid(ScanBase): def _compute_line_geometry(self): """Velocity, trigger position and run-up / run-out of the fast axis.""" line = self._line - line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() + line["shutter_open_delay"] = ( + self.ddg1.get_shutter_to_open_delay() if self.ddg1 is not None else 0.0 + ) + if self.rio is not None: + period_ms = self.exp_time * 1000 + if not np.isclose(period_ms, round(period_ms)): + raise ScanAbortion( + f"With the RIO trigger box exp_time must be a multiple of 1 ms, got {self.exp_time}." + ) + line["rio_period_ms"] = int(round(period_ms)) line["original_velocity"] = self.fast_axis.motor_velocity.get() line["acceleration"] = self.fast_axis.motor_acceleration.get() @@ -362,7 +412,10 @@ class AerotechContGrid(ScanBase): def _restore(self): try: - self.ddg1.set_trigger(DDG_SINGLE_SHOT) + if self.rio is not None: + self.rio.stop_burst_mode() + else: + self.ddg1.set_trigger(DDG_SINGLE_SHOT) finally: if self._line.get("original_velocity") is not None: self.fast_axis.motor_velocity.put(self._line["original_velocity"]) diff --git a/tests/tests_devices/test_galil_rio_ddg.py b/tests/tests_devices/test_galil_rio_ddg.py new file mode 100644 index 00000000..04e7b323 --- /dev/null +++ b/tests/tests_devices/test_galil_rio_ddg.py @@ -0,0 +1,142 @@ +import time + +import pytest +from bec_server.device_server.tests.utils import DMMock +from ophyd_devices.tests.utils import SocketMock + +from csaxs_bec.devices.aerotech.a3200 import A3200Controller +from csaxs_bec.devices.omny.galil.galil_rio import GalilRIOController +from csaxs_bec.devices.omny.galil.galil_rio_ddg import GalilRIOBurst, GalilRIOBurstError +from csaxs_bec.devices.sim.sim_a3200 import SimA3200Motor +from csaxs_bec.devices.sim.sim_rio_ddg import SimGalilRIOBurst +from csaxs_bec.devices.sim.sim_socket import SimStateRegistry + + +@pytest.fixture(autouse=True) +def _reset(): + GalilRIOController._reset_controller() + A3200Controller._reset_controller() + SimStateRegistry.reset() + yield + SimStateRegistry.reset() + GalilRIOController._reset_controller() + A3200Controller._reset_controller() + + +@pytest.fixture +def rio(): + device = GalilRIOBurst( + name="rioddg", host="rio", device_manager=DMMock(), socket_cls=SocketMock + ) + device.wait_for_connection() + device.controller.sock.flush_buffer() + return device + + +def test_arm_burst_sets_variables_in_order(rio): + rio.controller.sock.buffer_recv = [b" 0.0000\r\n", b":", b":", b":", b":", b":"] + rio.arm_burst(10, 50) + assert rio.controller.sock.buffer_put == [ + b"MG busy\r", + b"nfr=10\r", + b"per=50\r", + b"wid=25\r", + b"thr=3.0\r", + b"armed=1\r", + ] + + +@pytest.mark.parametrize( + "n_pulses, period, width, match", + [(5, 10, None, "below the minimum"), (5, 40, 40, "width"), (0, 40, None, "at least 1")], +) +def test_arm_burst_rejects_bad_parameters(rio, n_pulses, period, width, match): + with pytest.raises(GalilRIOBurstError, match=match): + rio.arm_burst(n_pulses, period, width) + + +def test_arm_burst_refused_while_busy(rio): + rio.controller.sock.buffer_recv = [b" 1.0000\r\n"] + with pytest.raises(GalilRIOBurstError, match="still sending"): + rio.arm_burst(5, 40) + + +def test_burst_mode_switching(rio): + rio.controller.sock.buffer_recv = [b":", b":", b":", b":"] + rio.start_burst_mode() + rio.stop_burst_mode() + assert rio.controller.sock.buffer_put == [b"HX0\r", b"XQ#BURST,0\r", b"HX0\r", b"XQ#DDG,0\r"] + + +def test_wait_burst_done_checks_pulse_count(rio): + rio.controller.sock.buffer_recv = [b" 0.0000 0.0000 4.0000 1.0000\r\n"] + with pytest.raises(GalilRIOBurstError, match="Sent 4 pulses instead of 5"): + rio.wait_burst_done(5, timeout=1) + + +def test_wait_burst_done_times_out_without_trigger(rio): + rio.controller.sock.buffer_recv = b" 1.0000 0.0000 0.0000 0.0000\r\n" + with pytest.raises(GalilRIOBurstError, match="line trigger"): + rio.wait_burst_done(5, timeout=0.1) + + +def test_sim_burst_triggered_by_aerotech_pso(): + dm = DMMock() + x = SimA3200Motor( + "X", + name="aerox", + host="a3200", + port=8000, + device_manager=dm, + velocity=10, + encoder_channel=0, + ) + rio = SimGalilRIOBurst( + name="rioddg", + host="rio", + device_manager=dm, + sim_trigger_host="a3200", + sim_trigger_port=8000, + ) + x.wait_for_connection() + rio.wait_for_connection() + rio.start_burst_mode() + x.move(-0.5, wait=True) + + rio.arm_burst(5, 20) + x.arm_line_trigger(0.0, 1.0) + start = time.time() + status = x.move(2.0, wait=False) + result = rio.wait_burst_done(5, timeout=5) + status.wait(timeout=5) + + # trigger after 0.5 mm at 10 mm/s, then 5 x 20 ms: cannot be done earlier + assert 0.14 <= time.time() - start < 2.0 + assert result == {"armed": False, "busy": False, "nsent": 5, "nline": 1} + + +def test_sim_burst_not_armed_ignores_trigger(): + dm = DMMock() + x = SimA3200Motor( + "X", + name="aerox", + host="a3200", + port=8000, + device_manager=dm, + velocity=10, + encoder_channel=0, + ) + rio = SimGalilRIOBurst( + name="rioddg", + host="rio", + device_manager=dm, + sim_trigger_host="a3200", + sim_trigger_port=8000, + ) + x.wait_for_connection() + rio.wait_for_connection() + rio.start_burst_mode() + x.move(-0.5, wait=True) + x.arm_line_trigger(0.0, 1.0) + x.move(2.0, wait=True) + assert rio.burst_status() == {"armed": False, "busy": False, "nsent": 0, "nline": 0} diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 2962a448..4f64c350 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -183,3 +183,119 @@ def test_on_exception_restores_ddg_and_pso(v4_scan_assembler, device_manager): scan.ddg1.set_trigger.assert_called_once_with(5) scan.fast_axis.disarm_line_trigger.assert_called_once_with() scan.fast_axis.motor_velocity.put.assert_called_once_with(10.0) + + +# --- trigger_source="rio" (RIO burst box instead of DDG1/DDG2 + MCS) ---------- + + +def _add_rio(device_manager): + rio = MockCustomDevice( + "rioddg", + device_info={"signals": {"readback": _signal("rioddg", "hinted")}}, + signal_read_values={"rioddg": 0.0}, + ) + for method in ("start_burst_mode", "stop_burst_mode", "arm_burst", "wait_burst_done"): + setattr(rio, method, mock.MagicMock()) + device_manager.add_device(rio, replace=True) + + +def _assemble_rio(v4_scan_assembler, device_manager, exp_time=0.1): + _add_devices(device_manager) + _add_rio(device_manager) + return v4_scan_assembler( + "aerotech_cont_grid", + "aerox", + -1.0, + 1.0, + 0.5, + "aeroy", + -2.0, + 2.0, + 2, + exp_time=exp_time, + trigger_source="rio", + ) + + +@pytest.mark.parametrize(("hook_name", "hook_tests"), AEROTECH_CONT_GRID_HOOK_TESTS) +def test_aerotech_cont_grid_rio_default_hooks( + v4_scan_assembler, device_manager, nth_done_status_mock, hook_name, hook_tests +): + scan = _assemble_rio(v4_scan_assembler, device_manager) + run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock) + + +def test_rio_geometry_has_no_shutter_delay(v4_scan_assembler, device_manager): + scan = _assemble_rio(v4_scan_assembler, device_manager) + scan.prepare_scan() + assert scan.ddg1 is None and scan.mcs is None + assert scan._line["shutter_open_delay"] == 0.0 + assert scan._line["trigger_position"] == pytest.approx(-1.0) + assert scan._line["rio_period_ms"] == 100 + + +def test_rio_exp_time_must_be_whole_ms(v4_scan_assembler, device_manager): + scan = _assemble_rio(v4_scan_assembler, device_manager, exp_time=0.0255) + with pytest.raises(ScanAbortion, match="multiple of 1 ms"): + scan.prepare_scan() + + +def test_unknown_trigger_source_aborts(v4_scan_assembler, device_manager): + _add_devices(device_manager) + with pytest.raises(ScanAbortion, match="trigger_source"): + v4_scan_assembler( + "aerotech_cont_grid", + "aerox", + -1.0, + 1.0, + 0.5, + "aeroy", + -2.0, + 2.0, + 2, + exp_time=0.1, + trigger_source="laser", + ) + + +def test_rio_pre_scan_calibrates_before_burst_mode( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _assemble_rio(v4_scan_assembler, device_manager) + scan.prepare_scan() + order = mock.MagicMock() + scan.capture = order.capture + scan.rio = order.rio + scan._premove_motor_status = nth_done_status_mock(resolve_after=1) + scan.actions.pre_scan_all_devices = mock.MagicMock() + + scan.pre_scan() + + assert order.mock_calls[:2] == [mock.call.capture.calibrate(), mock.call.rio.start_burst_mode()] + + +def test_rio_at_each_point_arms_and_waits_for_burst( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _assemble_rio(v4_scan_assembler, device_manager) + scan.prepare_scan() + scan.capture = mock.MagicMock() + scan.rio = mock.MagicMock() + scan.fast_axis = mock.MagicMock() + scan.actions.set = mock.MagicMock(return_value=nth_done_status_mock(resolve_after=1)) + scan.actions.read_monitored_devices = mock.MagicMock() + + scan.at_each_point([scan.fast_axis], np.array([scan._line["line_end"]]), status_mcs=None) + + scan.rio.arm_burst.assert_called_once_with(5, 100) + scan.rio.wait_burst_done.assert_called_once_with(5, timeout=3 + 5 * 0.1) + scan.capture.read_line.assert_called_once_with() + + +def test_rio_on_exception_stops_burst_mode(v4_scan_assembler, device_manager): + scan = _assemble_rio(v4_scan_assembler, device_manager) + scan.prepare_scan() + scan.rio = mock.MagicMock() + scan.fast_axis = mock.MagicMock() + scan.on_exception(RuntimeError("test")) + scan.rio.stop_burst_mode.assert_called_once_with() -- 2.54.0 From 669a7ea02b925c263419a7c6be3637f5b6568f0e Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 06:16:04 +0100 Subject: [PATCH 04/52] revert(aerotech): remove the RIO burst box trigger source This reverts commit bf77d6f6fe760f194d0aa1ec36685157a6fe9c34. The RIO timing (1 ms steps, ~50 Hz opto outputs) has too much jitter to judge whether the position sampling works, so the test setup will not use it. The DMC program stays in the OMNY Galil repository (RioDDG). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- .../device_configs/sastt_aerotech_test.yaml | 134 --------------- .../simulated_sastt_aerotech.yaml | 18 -- csaxs_bec/devices/aerotech/README.md | 24 +-- csaxs_bec/devices/omny/galil/galil_rio_ddg.py | 129 --------------- csaxs_bec/devices/sim/sim_a3200.py | 11 +- csaxs_bec/devices/sim/sim_rio_ddg.py | 156 ------------------ .../scans/scans_v4/aerotech_cont_grid.py | 89 ++-------- tests/tests_devices/test_galil_rio_ddg.py | 142 ---------------- .../test_aerotech_cont_grid_scan.py | 116 ------------- 9 files changed, 22 insertions(+), 797 deletions(-) delete mode 100644 csaxs_bec/device_configs/sastt_aerotech_test.yaml delete mode 100644 csaxs_bec/devices/omny/galil/galil_rio_ddg.py delete mode 100644 csaxs_bec/devices/sim/sim_rio_ddg.py delete mode 100644 tests/tests_devices/test_galil_rio_ddg.py diff --git a/csaxs_bec/device_configs/sastt_aerotech_test.yaml b/csaxs_bec/device_configs/sastt_aerotech_test.yaml deleted file mode 100644 index c98735e1..00000000 --- a/csaxs_bec/device_configs/sastt_aerotech_test.yaml +++ /dev/null @@ -1,134 +0,0 @@ -############################################################ -# SAXS-TT Aerotech stages, TEST SETUP without beamline timing (no DDG / MCS / shutter): -# Galil RIO-47120 trigger box (program RioDDG/rio_ddg.dmc, OMNY Galil repo) as line -# burst generator. Scan: aerotech_cont_grid(..., trigger_source="rio") -# -# Wiring: Npaq PSO output (J7) -> RIO analog input 1 (TTL ok, threshold 3 V) -# RIO DO1 (12-24 V, series resistor!) or AO5 (0/5 V) -> Npaq UINT1 (J12) -# -# A3200 setup: CommandSetup = 0x00010004 (enabled + Always Send EOS), CommandPort = 8000. -# TODO: host names of the A3200 PC and the RIO, axis names, encoder channels, -# velocities (= maximum scan velocity), accelerations, limits. -############################################################ - -aerox: - description: Aerotech X, ALS25020 linear stage - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor - deviceConfig: - axis_Id: X - axis_nr: 0 - encoder_channel: 0 - acceleration: 100.0 - host: a3200-saxstt.psi.ch - port: 8000 - velocity: 10.0 - egu: mm - limits: - - 0 - - 0 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech -aeroy: - description: Aerotech Y, AVSI125 vertical lift - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor - deviceConfig: - axis_Id: Y - axis_nr: 1 - encoder_channel: 1 - acceleration: 20.0 - host: a3200-saxstt.psi.ch - port: 8000 - velocity: 0.5 - egu: mm - limits: - - 0 - - 0 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech -aeroz: - description: Aerotech Z, ATS20010 linear stage - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor - deviceConfig: - axis_Id: Z - axis_nr: 2 - encoder_channel: 2 - acceleration: 100.0 - host: a3200-saxstt.psi.ch - port: 8000 - velocity: 10.0 - egu: mm - limits: - - 0 - - 0 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech -aerorot: - description: Aerotech U, ABRT-200 rotation stage (UU follows on the controller) - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor - deviceConfig: - axis_Id: U - axis_nr: 3 - encoder_channel: 3 - acceleration: 360.0 - host: a3200-saxstt.psi.ch - port: 8000 - velocity: 10.0 - egu: deg - tolerance: 0.001 - limits: - - 0 - - 0 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech -aerocapture: - description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture - deviceConfig: - host: a3200-saxstt.psi.ch - port: 8000 - drive_axis: X - channels: - 0: X - 1: Y - 2: Z - 3: U - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: async - deviceTags: - - sastt_aerotech -rioddg: - description: Galil RIO-47120 trigger box, line burst generator (rio_ddg.dmc) - deviceClass: csaxs_bec.devices.omny.galil.galil_rio_ddg.GalilRIOBurst - deviceConfig: - host: rio-saxstt.psi.ch - port: 23 - min_period_ms: 20 - threshold: 3.0 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index 80839527..d18b2971 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -1,8 +1,6 @@ ############################################################ # Simulated SAXS-TT Aerotech stages (sim_a3200), same devices as sastt_aerotech.yaml. # Axes start enabled and homed (sim_enabled / sim_homed in deviceConfig to change). -# rioddg: simulated RIO burst box, triggered by the PSO output of the simulated A3200. -# Scan without hardware: aerotech_cont_grid(..., trigger_source="rio") ############################################################ aerox: @@ -112,19 +110,3 @@ aerocapture: readoutPriority: async deviceTags: - sastt_aerotech -rioddg: - description: Galil RIO-47120 trigger box, line burst generator (simulated) - deviceClass: csaxs_bec.devices.sim.sim_rio_ddg.SimGalilRIOBurst - deviceConfig: - host: sim-rio-saxstt - port: 23 - min_period_ms: 20 - threshold: 3.0 - sim_trigger_host: sim-a3200-saxstt - sim_trigger_port: 8000 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index b678f199..347dae79 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -13,13 +13,8 @@ A3200 ASCII command interface on the Windows PC (TCP, default port 8000). | - | UU | ADRS-150 | slipring follower of U, handled on the controller | | aerocapture | - | - | position capture (DATAACQ) of X, Y, Z, U | -Configs: - -| Config | Use | -|--------|-----| -| `device_configs/sastt_aerotech.yaml` | beamline: A3200 + position capture, timing by DDG1/DDG2/MCS | -| `device_configs/sastt_aerotech_test.yaml` | test setup: A3200 + position capture + Galil RIO burst box (`rioddg`) | -| `device_configs/simulated_omny/simulated_sastt_aerotech.yaml` | simulation of the test setup (`sim_a3200.py`, `sim_rio_ddg.py`) | +Configs: `device_configs/sastt_aerotech.yaml` (hardware), +`device_configs/simulated_omny/simulated_sastt_aerotech.yaml` (simulation, `sim_a3200.py`). Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface"), copy at `~/gitea/SAXS-TT_docs` on the workstation. @@ -84,18 +79,3 @@ Commissioning of the scan: as scratch variable. 6. Check one line with the scope: PSO edge at the trigger position, number of UINT1 edges = frames per line, captured positions vs. requested positions. - -## Test setup without beamline timing: trigger_source="rio" - -`aerotech_cont_grid(..., trigger_source="rio")` replaces DDG1/DDG2/MCS by the Galil -RIO-47120 trigger box (device `rioddg`, class `GalilRIOBurst`, program -`RioDDG/rio_ddg.dmc` in the OMNY Galil repository, branch `feat/rio-ddg`): - -- pre_scan: capture calibration, then the RIO is switched to burst mode (`HX0`, `XQ#BURST,0`) -- per line: `arm_burst(frames_per_line, exp_time in ms)`, PSO line trigger -> RIO analog - input 1 -> pulses on DO1 / AO5 -> Npaq UINT1 (position capture); BEC waits until the - RIO reports all pulses sent -- post_scan / exception: RIO back to the legacy delay generator (`XQ#DDG,0`) -- exp_time in whole ms, >= 20 ms (opto outputs ~50 Hz); no shutter delay compensation - -In the simulation the RIO is triggered by the PSO window of the simulated A3200. diff --git a/csaxs_bec/devices/omny/galil/galil_rio_ddg.py b/csaxs_bec/devices/omny/galil/galil_rio_ddg.py deleted file mode 100644 index dec38755..00000000 --- a/csaxs_bec/devices/omny/galil/galil_rio_ddg.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Galil RIO-47120 trigger box used as a line burst generator (stand-in for DDG1/DDG2 in -test setups without the beamline timing). - -Runs the program RioDDG/rio_ddg.dmc of the OMNY Galil repository. Thread 0 is either in -the legacy delay generator mode (#DDG, default after power up) or in the burst mode -(#BURST). In burst mode, per line BEC sets the number of pulses, period and width and arms -the box; on the rising edge of analog input 1 (line trigger, e.g. the Aerotech PSO output) -the program sends the pulses on digital output 1 and analog output 5. -""" - -from __future__ import annotations - -import time - -from bec_lib.logger import bec_logger - -from csaxs_bec.devices.omny.galil.galil_rio import GalilRIO - -logger = bec_logger.logger - - -class GalilRIOBurstError(Exception): - pass - - -class GalilRIOBurst(GalilRIO): - """ - Galil RIO trigger box with the line burst generator of rio_ddg.dmc. - - Args: - min_period_ms: shortest allowed pulse period; the opto outputs (DO1) have ~50 Hz - bandwidth, so 20 ms by default. - threshold: trigger threshold of analog input 1 in V. - """ - - USER_ACCESS = [ - "start_burst_mode", - "stop_burst_mode", - "arm_burst", - "disarm_burst", - "burst_status", - "burst_done", - "wait_burst_done", - ] - - def __init__( - self, - *, - name: str, - host: str, - device_manager, - port: int | None = None, - min_period_ms: int = 20, - threshold: float = 3.0, - **kwargs, - ): - super().__init__(name=name, host=host, device_manager=device_manager, port=port, **kwargs) - self.min_period_ms = int(min_period_ms) - self.threshold = float(threshold) - - def _get_var(self, var: str) -> float: - return float(self.controller.socket_put_and_receive(f"MG {var}").strip()) - - def _set_var(self, var: str, value) -> None: - self.controller.socket_put_confirmed(f"{var}={value}") - - def start_burst_mode(self) -> None: - """Switch thread 0 from the legacy delay generator to the burst generator.""" - self.controller.socket_put_confirmed("HX0") - self.controller.socket_put_confirmed("XQ#BURST,0") - time.sleep(0.05) - - def stop_burst_mode(self) -> None: - """Back to the legacy delay generator (#DDG).""" - self.controller.socket_put_confirmed("HX0") - self.controller.socket_put_confirmed("XQ#DDG,0") - - def arm_burst(self, n_pulses: int, period_ms: int, width_ms: int | None = None) -> None: - """Arm one burst of n_pulses, started by the next rising edge of the trigger input.""" - period_ms = int(round(period_ms)) - if width_ms is None: - width_ms = period_ms // 2 - width_ms = int(round(width_ms)) - if period_ms < self.min_period_ms: - raise GalilRIOBurstError( - f"Pulse period {period_ms} ms is below the minimum of {self.min_period_ms} ms." - ) - if not 1 <= width_ms < period_ms: - raise GalilRIOBurstError(f"Pulse width {width_ms} ms must be in [1, {period_ms}).") - if n_pulses < 1: - raise GalilRIOBurstError(f"Number of pulses must be at least 1, got {n_pulses}.") - if self._get_var("busy"): - raise GalilRIOBurstError("RIO is still sending the pulses of the previous line.") - self._set_var("nfr", int(n_pulses)) - self._set_var("per", period_ms) - self._set_var("wid", width_ms) - self._set_var("thr", self.threshold) - self._set_var("armed", 1) - - def disarm_burst(self) -> None: - self._set_var("armed", 0) - - def burst_status(self) -> dict: - reply = self.controller.socket_put_and_receive("MG armed, busy, nsent, nline") - armed, busy, nsent, nline = (float(v) for v in reply.split()) - return {"armed": bool(armed), "busy": bool(busy), "nsent": int(nsent), "nline": int(nline)} - - def burst_done(self) -> bool: - """True once the armed burst was triggered and all its pulses are sent.""" - status = self.burst_status() - return not status["armed"] and not status["busy"] - - def wait_burst_done(self, n_pulses: int, timeout: float) -> dict: - """Wait for the burst to finish, check that n_pulses were sent.""" - start = time.time() - while True: - status = self.burst_status() - if not status["armed"] and not status["busy"]: - break - if time.time() - start > timeout: - raise GalilRIOBurstError( - f"Burst not finished within {timeout} s: {status}. Was the line trigger" - " received on analog input 1?" - ) - time.sleep(0.05) - if status["nsent"] != n_pulses: - raise GalilRIOBurstError(f"Sent {status['nsent']} pulses instead of {n_pulses}.") - return status diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index 52890a89..18f37910 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -123,8 +123,6 @@ class SimA3200State: self.drive_array = [0.0] * 4096 self.pso = {"armed": False, "mode": None, "channel": None, "range": None, "load": 0} self.pso_window_entries = 0 # rising edges of the PSO window output (line triggers) - # callables(trigger_time) receiving the PSO output edge, e.g. a simulated trigger box - self.line_trigger_listeners = [] self.dataacq = {"armed": False, "trigger": None, "inputs": set(), "n_elements": 0} def counter(self, channel: int) -> int: @@ -142,9 +140,7 @@ class SimA3200State: value = self.counter(ch) self.drive_array[index * len(inputs) + i] = float(value) - def on_move_start( - self, ax: _SimA3200Axis, start: float, target: float, velocity: float - ) -> None: + def on_move_start(self, ax: _SimA3200Axis, start: float, target: float) -> None: """Simulate the line trigger and the detector exposures of a scan line.""" pso, acq = self.pso, self.dataacq if not (pso["armed"] and pso["mode"] == "window" and pso["range"] is not None): @@ -156,9 +152,6 @@ class SimA3200State: if not (min(start, target) <= low and high <= max(start, target)): return self.pso_window_entries += 1 - trigger_time = time.time() + abs(low - start) / abs(velocity) - for listener in list(self.line_trigger_listeners): - listener(trigger_time) if acq["armed"] and acq["trigger"] == 0 and acq["inputs"]: n_triggers = acq["n_elements"] // len(acq["inputs"]) step = (high - low) / max(n_triggers, 1) @@ -254,7 +247,7 @@ class SimA3200Socket(SimSocketBase): ax = self._ready_axis(name) start = ax.position() target = value if cmd == "MOVEABS" else start + value - state.on_move_start(ax, start, target, velocity) + state.on_move_start(ax, start, target) ax.start_move(target, velocity) return "" if cmd == "HOMEASYNC": diff --git a/csaxs_bec/devices/sim/sim_rio_ddg.py b/csaxs_bec/devices/sim/sim_rio_ddg.py deleted file mode 100644 index 33b563b1..00000000 --- a/csaxs_bec/devices/sim/sim_rio_ddg.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Simulated Galil RIO trigger box running RioDDG/rio_ddg.dmc (OMNY Galil repository). - -Implements the commands used by `GalilRIOBurst` and `GalilRIO`: variable assignments, -"MG var, var, ...", HX0, XQ#BURST,0, XQ#DDG,0, SB/CB, MG@OUT[n], MG@AN[...]. - -The line trigger (analog input 1) comes from a simulated A3200 controller: the device is -registered as listener of the PSO output of `SimA3200State` (host/port given in the -device config). A burst is evaluated lazily from the trigger time: armed is cleared at the -trigger, nsent counts the periods elapsed since then, busy is set until nfr * per ms. -""" - -from __future__ import annotations - -import re -import threading -import time - -from bec_lib.logger import bec_logger - -from csaxs_bec.devices.omny.galil.galil_rio_ddg import GalilRIOBurst -from csaxs_bec.devices.sim.sim_a3200 import SimA3200State -from csaxs_bec.devices.sim.sim_socket import SimSocketBase, SimStateRegistry - -logger = bec_logger.logger - -_ASSIGNMENT_RE = re.compile(r"^([A-Za-z][A-Za-z0-9]*)\s*=\s*(-?[0-9.]+)$") - - -class SimRioDDGState: - """Shared state of one simulated RIO with the rio_ddg.dmc program.""" - - def __init__(self, host, port): - self.host = host - self.port = port - self._lock = threading.RLock() - self.mode = "ddg" - self.vars = {} - self.digital_outputs = [0.0] * 16 - self.analog_inputs = [0.0] * 8 - self._burst = None # (trigger_time, nfr, per) - self._reset_burst_vars() - self.vars.update(nfr=1.0, per=100.0, wid=10.0, thr=3.0, delay=100.0) - - def _reset_burst_vars(self): - self.vars.update(armed=0.0, busy=0.0, nsent=0.0, nline=0.0) - self._burst = None - - def start(self, label: str): - with self._lock: - self.mode = "burst" if label == "#BURST" else "ddg" - if self.mode == "burst": - self._reset_burst_vars() - - def halt(self): - with self._lock: - self.mode = None - - def line_trigger(self, trigger_time: float): - """Rising edge on analog input 1 at trigger_time.""" - with self._lock: - self._refresh() - if self.mode != "burst" or not self.vars["armed"] or self._burst is not None: - return - self._burst = (trigger_time, int(self.vars["nfr"]), self.vars["per"]) - - def _refresh(self): - if self._burst is None: - return - trigger_time, nfr, per = self._burst - elapsed_ms = (time.time() - trigger_time) * 1000.0 - if elapsed_ms < 0: - return - self.vars["armed"] = 0.0 - if elapsed_ms >= nfr * per: - self.vars.update(busy=0.0, nsent=float(nfr), nline=self.vars["nline"] + 1) - self._burst = None - else: - self.vars.update(busy=1.0, nsent=float(min(nfr, int(elapsed_ms // per) + 1))) - - def get(self, var: str) -> float: - with self._lock: - self._refresh() - return self.vars.get(var, 0.0) - - def set(self, var: str, value: float): - with self._lock: - self._refresh() - self.vars[var] = value - - -class SimRioDDGSocket(SimSocketBase): - state_cls = SimRioDDGState - - def handle_command(self, line: str): - state: SimRioDDGState = self.state - if line == "HX0": - state.halt() - return ":" - if line.startswith("XQ"): - state.start(line[2:].split(",")[0].strip()) - return ":" - if line.startswith("SB") or line.startswith("CB"): - state.digital_outputs[int(line[2:])] = 1.0 if line.startswith("SB") else 0.0 - return ":" - if line.startswith("MG"): - items = [item.strip() for item in line[2:].split(",")] - values = [] - for item in items: - if item.startswith("@OUT["): - values.append(state.digital_outputs[int(item[5:].split("]")[0])]) - elif item.startswith("@AN["): - values.append(state.analog_inputs[int(item[4:].split("]")[0])]) - else: - values.append(state.get(item)) - return " ".join(f"{v:.4f}" for v in values) - match = _ASSIGNMENT_RE.match(line) - if match: - state.set(match.group(1), float(match.group(2))) - return ":" - logger.warning(f"[sim rio ddg] {self.host}:{self.port} unhandled command '{line}'") - return "?" - - -class SimGalilRIOBurst(GalilRIOBurst): - """GalilRIOBurst connected to a simulated RIO; its trigger input follows the PSO output - of the simulated A3200 controller at (sim_trigger_host, sim_trigger_port).""" - - def __init__( - self, - *, - name: str, - host: str, - device_manager, - port: int | None = None, - min_period_ms: int = 20, - threshold: float = 3.0, - sim_trigger_host: str | None = None, - sim_trigger_port: int = 8000, - **kwargs, - ): - super().__init__( - name=name, - host=host, - device_manager=device_manager, - port=port, - min_period_ms=min_period_ms, - threshold=threshold, - socket_cls=SimRioDDGSocket, - **kwargs, - ) - state = SimStateRegistry.get(SimRioDDGState, host, port or 23) - if sim_trigger_host is not None: - a3200 = SimStateRegistry.get(SimA3200State, sim_trigger_host, sim_trigger_port) - if state.line_trigger not in a3200.line_trigger_listeners: - a3200.line_trigger_listeners.append(state.line_trigger) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index ba854088..338f5407 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -8,16 +8,10 @@ which runs the shutter / detector / MCS sequence. With a position capture device encoder positions of the axes are latched on each detector trigger (Npaq UINT1 input) and published per line. -Wiring (trigger_source="ddg", beamline): +Wiring: Npaq PSO output (J7) -> DDG1 EXT/EN detector trigger (DDG2 AB) -> Npaq UINT1 (J12 pins 1/2, 5 V opto input) -Wiring (trigger_source="rio", test setup without beamline timing): - Npaq PSO output (J7) -> RIO analog input 1 - RIO digital output 1 / analog output 5 -> Npaq UINT1 (and detector, if any) - The RIO (program RioDDG/rio_ddg.dmc) sends frames_per_line pulses with period exp_time - (1 ms steps, >= 20 ms) on each line trigger; no DDG, MCS or shutter is used. - Timing per line: trigger position = fast_start - velocity * shutter_open_delay, so the first exposure starts at fast_start; the fast axis starts one ramp distance (plus margin) before the @@ -58,8 +52,6 @@ logger = bec_logger.logger DDG_EXT_RISING_EDGE = 1 DDG_SINGLE_SHOT = 5 -TRIGGER_SOURCES = ("ddg", "rio") - # Extra run-up / run-out beyond the ramp distance, as a fraction of it, and as settling time RAMP_MARGIN_FRACTION = 0.2 SETTLE_TIME = 0.05 @@ -72,7 +64,6 @@ class AerotechLineParameter(TypedDict): target_velocity: float | None acceleration: float | None shutter_open_delay: float | None - rio_period_ms: int | None trigger_position: float | None exposure_end: float | None line_start: float | None @@ -97,7 +88,7 @@ class AerotechContGrid(ScanBase): "stepper_stop", "stepper_step_size", ], - "Acquisition Parameters": ["exp_time", "relative", "capture_positions", "trigger_source"], + "Acquisition Parameters": ["exp_time", "relative", "capture_positions"], } def __init__( @@ -115,8 +106,6 @@ class AerotechContGrid(ScanBase): relative: DefaultArgType.Relative = False, capture_positions: bool = True, capture_device: str = "aerocapture", - trigger_source: str = "ddg", - rio_device: str = "rioddg", **kwargs, # fmt: on ): @@ -138,8 +127,6 @@ class AerotechContGrid(ScanBase): relative (bool): Whether the positions are relative to the current position. Default is False. capture_positions (bool): Latch the axis positions at each detector trigger. Default is True. capture_device (str): Name of the A3200PositionCapture device. Default is "aerocapture". - trigger_source (str): "ddg" (DDG1/DDG2 + MCS, beamline) or "rio" (RIO burst box, test setup). Default is "ddg". - rio_device (str): Name of the GalilRIOBurst device for trigger_source="rio". Default is "rioddg". Returns: ScanReport @@ -160,19 +147,8 @@ class AerotechContGrid(ScanBase): self.relative = relative self.exp_time = exp_time self.capture_positions = capture_positions - if trigger_source not in TRIGGER_SOURCES: - raise ScanAbortion( - f"Unknown trigger_source '{trigger_source}', use one of {TRIGGER_SOURCES}." - ) - self.trigger_source = trigger_source - self.ddg1 = self.mcs = self.rio = None - if trigger_source == "ddg": - self.ddg1 = self.device_manager.devices["ddg1"] - self.mcs = self.device_manager.devices["mcs"] - else: - if rio_device not in self.device_manager.devices: - raise ScanAbortion(f"RIO burst device '{rio_device}' not found.") - self.rio = self.device_manager.devices[rio_device] + self.ddg1 = self.device_manager.devices["ddg1"] + self.mcs = self.device_manager.devices["mcs"] self.capture = None if capture_positions: if capture_device not in self.device_manager.devices: @@ -228,8 +204,7 @@ class AerotechContGrid(ScanBase): num_lines=self._line["num_lines"], ) - if self.mcs is not None: - self.actions.add_scan_report_instruction_device_progress(self.mcs) + self.actions.add_scan_report_instruction_device_progress(self.mcs) self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) self._premove_motor_status = self.actions.set( self.motors, [self._line["line_start"], self.stepper_start], wait=False @@ -248,16 +223,13 @@ class AerotechContGrid(ScanBase): @scan_hook def pre_scan(self): """ - Calibrate the position capture while DDG1 / the RIO still ignore the line trigger - (the calibration fires the PSO output once), then enable the trigger receiver. + Calibrate the position capture while DDG1 still ignores external triggers (the + calibration fires the PSO output once), then switch DDG1 to the external trigger. """ self._premove_motor_status.wait() if self.capture is not None: self.capture.calibrate() - if self.rio is not None: - self.rio.start_burst_mode() - else: - self.ddg1.set_trigger(DDG_EXT_RISING_EDGE) + self.ddg1.set_trigger(DDG_EXT_RISING_EDGE) self.actions.pre_scan_all_devices() @scan_hook @@ -268,13 +240,11 @@ class AerotechContGrid(ScanBase): self.motors, [self._line["line_start"], self.positions[line_index][1]] ) status_motor.wait() - mcs_acquiring_status = None - if self.ddg1 is not None: - status_mcs = self.ddg1.prepare_mcs_on_trigger() - status_mcs.wait() - mcs_acquiring_status = self.mcs.get_transition_status( - signal_name="acquiring", transition=[1, 0] - ) + status_mcs = self.ddg1.prepare_mcs_on_trigger() + status_mcs.wait() + mcs_acquiring_status = self.mcs.get_transition_status( + signal_name="acquiring", transition=[1, 0] + ) self.at_each_point( motors=[self.fast_axis], positions=np.array([self._line["line_end"]]), @@ -295,8 +265,6 @@ class AerotechContGrid(ScanBase): """ if self.capture is not None: self.capture.arm(self.frames_per_line) - if self.rio is not None: - self.rio.arm_burst(self.frames_per_line, self._line["rio_period_ms"]) self.fast_axis.arm_line_trigger(self._line["trigger_position"], self._line["exposure_end"]) self.fast_axis.motor_velocity.set(self._line["target_velocity"]).wait(timeout=5) try: @@ -311,18 +279,6 @@ class AerotechContGrid(ScanBase): self.fast_axis.motor_velocity.set(self._line["original_velocity"]).wait(timeout=5) self.fast_axis.disarm_line_trigger() - if self.rio is not None: - self.rio.wait_burst_done( - self.frames_per_line, - timeout=3 + self.frames_per_line * self._line["rio_period_ms"] / 1000, - ) - else: - self._wait_mcs(status_mcs) - - if self.capture is not None: - self.capture.read_line() - - def _wait_mcs(self, status_mcs): try: status_mcs.wait(timeout=3) except TimeoutError as exc: @@ -331,6 +287,9 @@ class AerotechContGrid(ScanBase): " received? Check the PSO output -> DDG1 EXT wiring and the PSO interlock." ) from exc + if self.capture is not None: + self.capture.read_line() + @scan_hook def post_scan(self): """Restore DDG1 trigger and fast axis velocity, complete devices.""" @@ -377,16 +336,7 @@ class AerotechContGrid(ScanBase): def _compute_line_geometry(self): """Velocity, trigger position and run-up / run-out of the fast axis.""" line = self._line - line["shutter_open_delay"] = ( - self.ddg1.get_shutter_to_open_delay() if self.ddg1 is not None else 0.0 - ) - if self.rio is not None: - period_ms = self.exp_time * 1000 - if not np.isclose(period_ms, round(period_ms)): - raise ScanAbortion( - f"With the RIO trigger box exp_time must be a multiple of 1 ms, got {self.exp_time}." - ) - line["rio_period_ms"] = int(round(period_ms)) + line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() line["original_velocity"] = self.fast_axis.motor_velocity.get() line["acceleration"] = self.fast_axis.motor_acceleration.get() @@ -412,10 +362,7 @@ class AerotechContGrid(ScanBase): def _restore(self): try: - if self.rio is not None: - self.rio.stop_burst_mode() - else: - self.ddg1.set_trigger(DDG_SINGLE_SHOT) + self.ddg1.set_trigger(DDG_SINGLE_SHOT) finally: if self._line.get("original_velocity") is not None: self.fast_axis.motor_velocity.put(self._line["original_velocity"]) diff --git a/tests/tests_devices/test_galil_rio_ddg.py b/tests/tests_devices/test_galil_rio_ddg.py deleted file mode 100644 index 04e7b323..00000000 --- a/tests/tests_devices/test_galil_rio_ddg.py +++ /dev/null @@ -1,142 +0,0 @@ -import time - -import pytest -from bec_server.device_server.tests.utils import DMMock -from ophyd_devices.tests.utils import SocketMock - -from csaxs_bec.devices.aerotech.a3200 import A3200Controller -from csaxs_bec.devices.omny.galil.galil_rio import GalilRIOController -from csaxs_bec.devices.omny.galil.galil_rio_ddg import GalilRIOBurst, GalilRIOBurstError -from csaxs_bec.devices.sim.sim_a3200 import SimA3200Motor -from csaxs_bec.devices.sim.sim_rio_ddg import SimGalilRIOBurst -from csaxs_bec.devices.sim.sim_socket import SimStateRegistry - - -@pytest.fixture(autouse=True) -def _reset(): - GalilRIOController._reset_controller() - A3200Controller._reset_controller() - SimStateRegistry.reset() - yield - SimStateRegistry.reset() - GalilRIOController._reset_controller() - A3200Controller._reset_controller() - - -@pytest.fixture -def rio(): - device = GalilRIOBurst( - name="rioddg", host="rio", device_manager=DMMock(), socket_cls=SocketMock - ) - device.wait_for_connection() - device.controller.sock.flush_buffer() - return device - - -def test_arm_burst_sets_variables_in_order(rio): - rio.controller.sock.buffer_recv = [b" 0.0000\r\n", b":", b":", b":", b":", b":"] - rio.arm_burst(10, 50) - assert rio.controller.sock.buffer_put == [ - b"MG busy\r", - b"nfr=10\r", - b"per=50\r", - b"wid=25\r", - b"thr=3.0\r", - b"armed=1\r", - ] - - -@pytest.mark.parametrize( - "n_pulses, period, width, match", - [(5, 10, None, "below the minimum"), (5, 40, 40, "width"), (0, 40, None, "at least 1")], -) -def test_arm_burst_rejects_bad_parameters(rio, n_pulses, period, width, match): - with pytest.raises(GalilRIOBurstError, match=match): - rio.arm_burst(n_pulses, period, width) - - -def test_arm_burst_refused_while_busy(rio): - rio.controller.sock.buffer_recv = [b" 1.0000\r\n"] - with pytest.raises(GalilRIOBurstError, match="still sending"): - rio.arm_burst(5, 40) - - -def test_burst_mode_switching(rio): - rio.controller.sock.buffer_recv = [b":", b":", b":", b":"] - rio.start_burst_mode() - rio.stop_burst_mode() - assert rio.controller.sock.buffer_put == [b"HX0\r", b"XQ#BURST,0\r", b"HX0\r", b"XQ#DDG,0\r"] - - -def test_wait_burst_done_checks_pulse_count(rio): - rio.controller.sock.buffer_recv = [b" 0.0000 0.0000 4.0000 1.0000\r\n"] - with pytest.raises(GalilRIOBurstError, match="Sent 4 pulses instead of 5"): - rio.wait_burst_done(5, timeout=1) - - -def test_wait_burst_done_times_out_without_trigger(rio): - rio.controller.sock.buffer_recv = b" 1.0000 0.0000 0.0000 0.0000\r\n" - with pytest.raises(GalilRIOBurstError, match="line trigger"): - rio.wait_burst_done(5, timeout=0.1) - - -def test_sim_burst_triggered_by_aerotech_pso(): - dm = DMMock() - x = SimA3200Motor( - "X", - name="aerox", - host="a3200", - port=8000, - device_manager=dm, - velocity=10, - encoder_channel=0, - ) - rio = SimGalilRIOBurst( - name="rioddg", - host="rio", - device_manager=dm, - sim_trigger_host="a3200", - sim_trigger_port=8000, - ) - x.wait_for_connection() - rio.wait_for_connection() - rio.start_burst_mode() - x.move(-0.5, wait=True) - - rio.arm_burst(5, 20) - x.arm_line_trigger(0.0, 1.0) - start = time.time() - status = x.move(2.0, wait=False) - result = rio.wait_burst_done(5, timeout=5) - status.wait(timeout=5) - - # trigger after 0.5 mm at 10 mm/s, then 5 x 20 ms: cannot be done earlier - assert 0.14 <= time.time() - start < 2.0 - assert result == {"armed": False, "busy": False, "nsent": 5, "nline": 1} - - -def test_sim_burst_not_armed_ignores_trigger(): - dm = DMMock() - x = SimA3200Motor( - "X", - name="aerox", - host="a3200", - port=8000, - device_manager=dm, - velocity=10, - encoder_channel=0, - ) - rio = SimGalilRIOBurst( - name="rioddg", - host="rio", - device_manager=dm, - sim_trigger_host="a3200", - sim_trigger_port=8000, - ) - x.wait_for_connection() - rio.wait_for_connection() - rio.start_burst_mode() - x.move(-0.5, wait=True) - x.arm_line_trigger(0.0, 1.0) - x.move(2.0, wait=True) - assert rio.burst_status() == {"armed": False, "busy": False, "nsent": 0, "nline": 0} diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 4f64c350..2962a448 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -183,119 +183,3 @@ def test_on_exception_restores_ddg_and_pso(v4_scan_assembler, device_manager): scan.ddg1.set_trigger.assert_called_once_with(5) scan.fast_axis.disarm_line_trigger.assert_called_once_with() scan.fast_axis.motor_velocity.put.assert_called_once_with(10.0) - - -# --- trigger_source="rio" (RIO burst box instead of DDG1/DDG2 + MCS) ---------- - - -def _add_rio(device_manager): - rio = MockCustomDevice( - "rioddg", - device_info={"signals": {"readback": _signal("rioddg", "hinted")}}, - signal_read_values={"rioddg": 0.0}, - ) - for method in ("start_burst_mode", "stop_burst_mode", "arm_burst", "wait_burst_done"): - setattr(rio, method, mock.MagicMock()) - device_manager.add_device(rio, replace=True) - - -def _assemble_rio(v4_scan_assembler, device_manager, exp_time=0.1): - _add_devices(device_manager) - _add_rio(device_manager) - return v4_scan_assembler( - "aerotech_cont_grid", - "aerox", - -1.0, - 1.0, - 0.5, - "aeroy", - -2.0, - 2.0, - 2, - exp_time=exp_time, - trigger_source="rio", - ) - - -@pytest.mark.parametrize(("hook_name", "hook_tests"), AEROTECH_CONT_GRID_HOOK_TESTS) -def test_aerotech_cont_grid_rio_default_hooks( - v4_scan_assembler, device_manager, nth_done_status_mock, hook_name, hook_tests -): - scan = _assemble_rio(v4_scan_assembler, device_manager) - run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock) - - -def test_rio_geometry_has_no_shutter_delay(v4_scan_assembler, device_manager): - scan = _assemble_rio(v4_scan_assembler, device_manager) - scan.prepare_scan() - assert scan.ddg1 is None and scan.mcs is None - assert scan._line["shutter_open_delay"] == 0.0 - assert scan._line["trigger_position"] == pytest.approx(-1.0) - assert scan._line["rio_period_ms"] == 100 - - -def test_rio_exp_time_must_be_whole_ms(v4_scan_assembler, device_manager): - scan = _assemble_rio(v4_scan_assembler, device_manager, exp_time=0.0255) - with pytest.raises(ScanAbortion, match="multiple of 1 ms"): - scan.prepare_scan() - - -def test_unknown_trigger_source_aborts(v4_scan_assembler, device_manager): - _add_devices(device_manager) - with pytest.raises(ScanAbortion, match="trigger_source"): - v4_scan_assembler( - "aerotech_cont_grid", - "aerox", - -1.0, - 1.0, - 0.5, - "aeroy", - -2.0, - 2.0, - 2, - exp_time=0.1, - trigger_source="laser", - ) - - -def test_rio_pre_scan_calibrates_before_burst_mode( - v4_scan_assembler, device_manager, nth_done_status_mock -): - scan = _assemble_rio(v4_scan_assembler, device_manager) - scan.prepare_scan() - order = mock.MagicMock() - scan.capture = order.capture - scan.rio = order.rio - scan._premove_motor_status = nth_done_status_mock(resolve_after=1) - scan.actions.pre_scan_all_devices = mock.MagicMock() - - scan.pre_scan() - - assert order.mock_calls[:2] == [mock.call.capture.calibrate(), mock.call.rio.start_burst_mode()] - - -def test_rio_at_each_point_arms_and_waits_for_burst( - v4_scan_assembler, device_manager, nth_done_status_mock -): - scan = _assemble_rio(v4_scan_assembler, device_manager) - scan.prepare_scan() - scan.capture = mock.MagicMock() - scan.rio = mock.MagicMock() - scan.fast_axis = mock.MagicMock() - scan.actions.set = mock.MagicMock(return_value=nth_done_status_mock(resolve_after=1)) - scan.actions.read_monitored_devices = mock.MagicMock() - - scan.at_each_point([scan.fast_axis], np.array([scan._line["line_end"]]), status_mcs=None) - - scan.rio.arm_burst.assert_called_once_with(5, 100) - scan.rio.wait_burst_done.assert_called_once_with(5, timeout=3 + 5 * 0.1) - scan.capture.read_line.assert_called_once_with() - - -def test_rio_on_exception_stops_burst_mode(v4_scan_assembler, device_manager): - scan = _assemble_rio(v4_scan_assembler, device_manager) - scan.prepare_scan() - scan.rio = mock.MagicMock() - scan.fast_axis = mock.MagicMock() - scan.on_exception(RuntimeError("test")) - scan.rio.stop_burst_mode.assert_called_once_with() -- 2.54.0 From 3008e226d51aad62f31d6dd6b96f9edd14d1338d Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 15:01:19 +0100 Subject: [PATCH 05/52] feat(aerotech): SAXS-TT axis and channel setup from the A3200 parameter file From 153722-A-1-1.prma: A3200 PC pc14643.psi.ch, axis indices X 0, Z 1, Y 2, U 3 (UU gantry slave, ZZ encoder input used by Z), limits, default speeds and ramp rates. PSO input and DATAACQ source are now configured separately, they have different numberings on the Npaq: motor pso_input (0-5 local counter, 6-8 MXR emulated quadrature) + pso_counts_divider (EmulatedQuadratureDivider), capture channels are DATAACQ sources 0-11 (6-11 = MXR channel 0-5). GlobalDoubles is 255 on SAXS-TT, so the scratch variable moves to $global[254]. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/device_configs/sastt_aerotech.yaml | 89 ++++++++++--------- .../simulated_sastt_aerotech.yaml | 61 +++++++------ csaxs_bec/devices/aerotech/README.md | 53 +++++++---- csaxs_bec/devices/aerotech/a3200.py | 63 ++++++++----- csaxs_bec/devices/sim/sim_a3200.py | 27 +++--- .../scans/scans_v4/aerotech_cont_grid.py | 2 +- tests/tests_devices/test_a3200.py | 53 +++++++---- 7 files changed, 214 insertions(+), 134 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index 72e2dca9..077a42c4 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -1,13 +1,20 @@ ############################################################ -# SAXS-TT Aerotech stages: Npaq-B-40B-40B/ULTRA, A3200 software on a Windows PC -# (FireWire to the Npaq). BEC connects to the A3200 ASCII command interface on that PC. -# A3200 setup: CommandSetup = 0x00010004 (enabled + Always Send EOS), CommandPort = 8000. -# -# TODO commissioning: host name of the A3200 PC, A3200 axis names, encoder channels, -# velocities (= maximum scan velocity), accelerations, limits. -# Wiring for aerotech_cont_grid: PSO output (J7) -> DDG1 EXT, detector trigger -> UINT1 (J12). -# UU (ADRS-150 slipring follower) follows U on the controller and is not a BEC device. -# Limits [0, 0] = no BEC soft limits; the controller's SoftwareLimitLow/High still apply. +# SAXS-TT Aerotech stages: Npaq-B-40B-40B/ULTRA, A3200 6.04.004 on the Windows PC +# pc14643.psi.ch (FireWire to the Npaq). BEC connects to the A3200 ASCII command interface. +# Values from the parameter file 153722-A-1-1.prma (2026-09-25): +# CommandSetup = 0x00010004, CommandPort = 8000, GlobalDoubles = 255 +# Npaq axis index: X 0, Z 1, Y 2, U 3, UU 4 (gantry slave of U), ZZ 5 +# feedback: X, U, UU, ZZ sine (MXR), Y square wave (local counter 2), +# Z position from MXR channel 5 (ZZ input), velocity from local counter 1 +# PSO: X -> emulated quadrature 6 (divider 1), U -> 7 (divider 40), Y -> local counter 2; +# Z has no emulated quadrature output, so no PSO line trigger on Z. +# PSO interlock defeated (IOSetup 0x80) on all axes. +# controller soft limits: X -50..150, Z -100..100, Y -10..10 mm (active after home) +# velocity / acceleration below = DefaultSpeed / DefaultRampRate (MaxSpeedClamp: +# X 200, Z 10, Y 10 mm/s, U 720 deg/s) +# Wiring for aerotech_cont_grid: PSO output (J7 "PSO INTERFACE") -> DDG1 EXT, +# detector trigger -> UINT1 (J12 "MISC I/O"). +# UU (ADRS-150 slipring follower) follows U as gantry slave and is not a BEC device. ############################################################ aerox: @@ -16,15 +23,16 @@ aerox: deviceConfig: axis_Id: X axis_nr: 0 - encoder_channel: 0 - acceleration: 100.0 - host: a3200-saxstt.psi.ch + pso_input: 6 + pso_counts_divider: 1 + acceleration: 10.0 + host: pc14643.psi.ch port: 8000 - velocity: 10.0 + velocity: 5.0 egu: mm limits: - - 0 - - 0 + - -50 + - 150 sign: 1 enabled: true onFailure: buffer @@ -37,16 +45,16 @@ aeroy: deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor deviceConfig: axis_Id: Y - axis_nr: 1 - encoder_channel: 1 - acceleration: 20.0 - host: a3200-saxstt.psi.ch + axis_nr: 2 + pso_input: 2 + acceleration: 10.0 + host: pc14643.psi.ch port: 8000 - velocity: 0.5 + velocity: 5.0 egu: mm limits: - - 0 - - 0 + - -10 + - 10 sign: 1 enabled: true onFailure: buffer @@ -55,20 +63,19 @@ aeroy: deviceTags: - sastt_aerotech aeroz: - description: Aerotech Z, ATS20010 linear stage + description: Aerotech Z, ATS20010 linear stage (no PSO input) deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor deviceConfig: axis_Id: Z - axis_nr: 2 - encoder_channel: 2 - acceleration: 100.0 - host: a3200-saxstt.psi.ch + axis_nr: 1 + acceleration: 10.0 + host: pc14643.psi.ch port: 8000 - velocity: 10.0 + velocity: 5.0 egu: mm limits: - - 0 - - 0 + - -100 + - 100 sign: 1 enabled: true onFailure: buffer @@ -77,16 +84,17 @@ aeroz: deviceTags: - sastt_aerotech aerorot: - description: Aerotech U, ABRT-200 rotation stage (UU follows on the controller) + description: Aerotech U, ABRT-200 rotation stage (UU follows as gantry slave) deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor deviceConfig: axis_Id: U axis_nr: 3 - encoder_channel: 3 - acceleration: 360.0 - host: a3200-saxstt.psi.ch + pso_input: 7 + pso_counts_divider: 40 + acceleration: 2000.0 + host: pc14643.psi.ch port: 8000 - velocity: 10.0 + velocity: 30.0 egu: deg tolerance: 0.001 limits: @@ -103,14 +111,15 @@ aerocapture: description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture deviceConfig: - host: a3200-saxstt.psi.ch + host: pc14643.psi.ch port: 8000 drive_axis: X + # DATAACQ source -> axis: 0-5 local encoder counter, 6-11 MXR channel 0-5 (max. 4 kHz) channels: - 0: X - 1: Y - 2: Z - 3: U + 6: X + 11: Z + 2: Y + 9: U enabled: true onFailure: buffer readOnly: false diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index d18b2971..eb66414d 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -1,5 +1,6 @@ ############################################################ -# Simulated SAXS-TT Aerotech stages (sim_a3200), same devices as sastt_aerotech.yaml. +# Simulated SAXS-TT Aerotech stages (sim_a3200), same devices and numbering as +# sastt_aerotech.yaml (CountsPerUnit from the parameter file). # Axes start enabled and homed (sim_enabled / sim_homed in deviceConfig to change). ############################################################ @@ -9,16 +10,18 @@ aerox: deviceConfig: axis_Id: X axis_nr: 0 - encoder_channel: 0 - acceleration: 100.0 + pso_input: 6 + pso_counts_divider: 1 + acceleration: 10.0 host: sim-a3200-saxstt port: 8000 - velocity: 10.0 + velocity: 5.0 egu: mm limits: - - 0 - - 0 + - -50 + - 150 sign: 1 + sim_counts_per_unit: 256000 enabled: true onFailure: buffer readOnly: false @@ -30,17 +33,18 @@ aeroy: deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor deviceConfig: axis_Id: Y - axis_nr: 1 - encoder_channel: 1 - acceleration: 20.0 + axis_nr: 2 + pso_input: 2 + acceleration: 10.0 host: sim-a3200-saxstt port: 8000 - velocity: 0.5 + velocity: 5.0 egu: mm limits: - - 0 - - 0 + - -10 + - 10 sign: 1 + sim_counts_per_unit: 4716.981132075472 enabled: true onFailure: buffer readOnly: false @@ -48,21 +52,21 @@ aeroy: deviceTags: - sastt_aerotech aeroz: - description: Aerotech Z, ATS20010 linear stage + description: Aerotech Z, ATS20010 linear stage (no PSO input) deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor deviceConfig: axis_Id: Z - axis_nr: 2 - encoder_channel: 2 - acceleration: 100.0 + axis_nr: 1 + acceleration: 10.0 host: sim-a3200-saxstt port: 8000 - velocity: 10.0 + velocity: 5.0 egu: mm limits: - - 0 - - 0 + - -100 + - 100 sign: 1 + sim_counts_per_unit: 5000 enabled: true onFailure: buffer readOnly: false @@ -70,22 +74,24 @@ aeroz: deviceTags: - sastt_aerotech aerorot: - description: Aerotech U, ABRT-200 rotation stage (UU follows on the controller) + description: Aerotech U, ABRT-200 rotation stage (UU follows as gantry slave) deviceClass: csaxs_bec.devices.sim.sim_a3200.SimA3200Motor deviceConfig: axis_Id: U axis_nr: 3 - encoder_channel: 3 - acceleration: 360.0 + pso_input: 7 + pso_counts_divider: 40 + acceleration: 2000.0 host: sim-a3200-saxstt port: 8000 - velocity: 10.0 + velocity: 30.0 egu: deg tolerance: 0.001 limits: - 0 - 0 sign: 1 + sim_counts_per_unit: 560000 enabled: true onFailure: buffer readOnly: false @@ -99,11 +105,12 @@ aerocapture: host: sim-a3200-saxstt port: 8000 drive_axis: X + # DATAACQ source -> axis: 0-5 local encoder counter, 6-11 MXR channel 0-5 (max. 4 kHz) channels: - 0: X - 1: Y - 2: Z - 3: U + 6: X + 11: Z + 2: Y + 9: U enabled: true onFailure: buffer readOnly: false diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 347dae79..86d65c7f 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -4,14 +4,26 @@ Hardware: Npaq-B-40B-40B/ULTRA drive rack, commanded over FireWire by the A3200 (SMC) on a Windows PC. The Npaq Ethernet port is for Modbus I/O only, so BEC talks to the A3200 ASCII command interface on the Windows PC (TCP, default port 8000). -| BEC device | A3200 axis | Stage | Type | -|------------|------------|----------|------------------------------------------------| -| aerox | X | ALS25020 | linear | -| aeroy | Y | AVSI125 | vertical lift, 25 mm | -| aeroz | Z | ATS20010 | linear | -| aerorot | U | ABRT-200 | rotation | -| - | UU | ADRS-150 | slipring follower of U, handled on the controller | -| aerocapture | - | - | position capture (DATAACQ) of X, Y, Z, U | +A3200 PC: `pc14643.psi.ch`, A3200 6.04.004. Parameter file `153722-A-1-1.prma` +(2026-09-25), copy in `~/gitea/SAXS-TT_docs/prm`. + +| BEC device | Axis | Index | Stage | Feedback | PSO input | DATAACQ source | +|------------|------|-------|----------|----------|-----------|----------------| +| aerox | X | 0 | ALS25020 | sine (MXR 0) | 6 (emulated quadrature, divider 1) | 6 (MXR 0) | +| aeroz | Z | 1 | ATS20010 | position MXR 5 (ZZ input), velocity counter 1 | - | 11 (MXR 5) | +| aeroy | Y | 2 | AVSI125 | square wave (counter 2) | 2 | 2 | +| aerorot | U | 3 | ABRT-200 | sine (MXR 3) | 7 (emulated quadrature, divider 40) | 9 (MXR 3) | +| - | UU | 4 | ADRS-150 | sine | - | - | +| - | ZZ | 5 | - | encoder input used by Z | - | - | + +UU is the gantry slave of U (`GantryMasterAxis = 3`, `GantrySetup = 1`), not a BEC device. +`aerocapture` latches X, Z, Y, U. + +Channel numbering on the Npaq: feedback channel = axis index in the rack unless +`PositionFeedbackChannel` says otherwise. PSO inputs 0-5 are the local encoder counters +(square wave), 6-8 the MXR emulated quadrature outputs (`EmulatedQuadratureChannel`, PSO +counts = CountsPerUnit / `EmulatedQuadratureDivider` -> `pso_counts_divider`). DATAACQ +sources 0-5 are the local counters, 6-11 the MXR channels 0-5 (max. 4 kHz trigger rate). Configs: `device_configs/sastt_aerotech.yaml` (hardware), `device_configs/simulated_omny/simulated_sastt_aerotech.yaml` (simulation, `sim_a3200.py`). @@ -36,9 +48,10 @@ Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface" Always Send EOS), `CommandPort = 8000`, reset the controller. 4. Windows firewall: allow inbound TCP 8000 from the BEC host. 5. Check with `nc 8000`, send `~VERSION`, expect `%6.04...`. -6. Set host, axis names, velocities and limits in `sastt_aerotech.yaml`. +6. Host, axes, velocities and limits in `sastt_aerotech.yaml` are taken from the + parameter file; update them when the parameters change. 7. Check `ProgramPositionFeedback` matches the position shown in Motion Composer. -8. Check how UU follows U (gantry, slave or program) before rotating. +8. UU follows U as gantry slave: check the slip ring cabling before the first rotation. ## Scan: aerotech_cont_grid @@ -54,8 +67,8 @@ scans.aerotech_cont_grid(dev.aerox, 0, 2, 0.01, dev.aeroy, 0, 1, 0.05, exp_time= so the first exposure starts at `fast_start` - run-up / run-out = ramp distance `v^2 / (2 * acceleration)` + 20 % + 50 ms at speed - `capture_positions=True` (default): device `aerocapture` latches the encoder counters of - all configured channels on each detector trigger (DATAACQ on UINT1, up to 16 kHz, drive - array of 2M elements). After each line they are read back, converted to user units and + all configured channels on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz + with MXR sources, drive array of 2M elements). After each line they are read back, converted to user units and published as async data `aerocapture_positions_enc`. - the counter-to-position offset is calibrated in pre_scan with one software PSO fire, before DDG1 is switched to the external trigger @@ -64,18 +77,20 @@ Wiring: | Signal | From | To | |--------|------|----| -| line trigger | Npaq PSO output (J7, differential / opto / open collector) | DDG1 EXT | +| line trigger | Npaq J7 "PSO INTERFACE" (rear, User I/O, 25-pin D-sub): pin 8 open collector (TTL, active low) + pin 24 common, or pins 6/7 opto output, or pins 1/2 RS-422 | DDG1 EXT | | exposure trigger | detector trigger (DDG2 AB) | Npaq UINT1 (J12 pins 1+/2-, 5 V opto, 5-15 mA) | Commissioning of the scan: -1. Encoder channel of each axis (`encoder_channel`, `aerocapture.channels`); check that - the counter direction matches the axis direction (calibration only fixes the offset). -2. PSO output: select the output used (`PSOOUTPUT CONTROL`), PSO interlock driven or - disabled via the `IOSetup` axis parameter (Npaq manual), polarity (JP16). +1. Channels (`pso_input`, `pso_counts_divider`, `aerocapture.channels`, see the table + above); check that the counter direction and scale match the axis (calibration only + fixes the offset). +2. PSO output: select the output used (`PSOOUTPUT CONTROL`), polarity (JP16, opto output + only; open collector and differential are active low). The PSO interlock is defeated + (`IOSetup` 0x80). 3. `acceleration` of the fast axis = the ramp rate BEC sets before each move. 4. `velocity` of the fast axis is the maximum scan velocity. -5. `GlobalDoubles` >= 256: BEC uses `$global[0..199]` for array reads and `$global[255]` - as scratch variable. +5. `GlobalDoubles` >= 255 (SAXS-TT: 255): BEC uses `$global[0..199]` for array reads and + `$global[254]` as scratch variable. Another client must not use them during a scan. 6. Check one line with the scope: PSO edge at the trigger position, number of UINT1 edges = frames per line, captured positions vs. requested positions. diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index 6d5a84dc..97190b24 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -133,8 +133,9 @@ class A3200Controller(Controller): ] # $global variables used by BEC: one scratch variable for evaluate() and a block - # for drive array reads. The controller has GlobalDoubles (default 256) of them. - scratch_global = 255 + # for drive array reads. The controller has GlobalDoubles of them (255 on SAXS-TT, + # i.e. $global[0..254]). + scratch_global = 254 array_global_base = 0 array_read_chunk = 200 @@ -336,20 +337,23 @@ class A3200Controller(Controller): # --- PSO (position synchronized output) ----------------------------------- - def pso_arm_window(self, axis: str, encoder_channel: int, low: float, high: float) -> None: + def pso_arm_window( + self, axis: str, pso_input: int, low: float, high: float, counts_divider: float = 1 + ) -> None: """ PSO output high while the axis is inside [low, high] (user units, absolute). The window counter is loaded with the current position (axis at rest), so the - range is given relative to it in encoder counts. + range is given relative to it in counts of the PSO input: CountsPerUnit, divided by + the EmulatedQuadratureDivider for the MXR emulated quadrature inputs 6-8. """ - cpu = self.counts_per_unit(axis) + cpu = self.counts_per_unit(axis) / counts_divider current = self.get_position(axis) low_counts, high_counts = sorted( (int(round((low - current) * cpu)), int(round((high - current) * cpu))) ) self.command(f"PSOCONTROL {axis} RESET") - self.command(f"PSOWINDOW {axis} 1 INPUT {encoder_channel}") + self.command(f"PSOWINDOW {axis} 1 INPUT {pso_input}") self.command(f"PSOWINDOW {axis} 1 LOAD 0") self.command(f"PSOWINDOW {axis} 1 RANGE {low_counts}, {high_counts}") self.command(f"PSOOUTPUT {axis} WINDOW") @@ -363,8 +367,9 @@ class A3200Controller(Controller): def dataacq_arm(self, axis: str, trigger: int, channels: list[int], n_triggers: int) -> None: """ - Capture the encoder counters of `channels` on each trigger into the drive array, + Capture the source signals `channels` on each trigger into the drive array, starting at index 0. Npaq trigger 0 = Input 12 (UINT1) rising edge, 6 = PSO firing. + Npaq sources: 0-5 local encoder counter, 6-11 encoder multiplier (MXR) channel 0-5. """ self.command(f"DATAACQ {axis} 1 OFF") self.command(f"DATAACQ {axis} 1 TRIGGER {trigger}") @@ -486,8 +491,11 @@ class A3200Motor(Device, PositionerBase): require_homed: refuse moves while the axis is not homed. acceleration: ramp rate for moves in user units/s^2; None keeps the controller's ramp settings (DefaultRampRate). - encoder_channel: Npaq local encoder counter of this axis, needed for the PSO line - trigger (arm_line_trigger). + pso_input: PSO input of this axis, needed for the PSO line trigger + (arm_line_trigger). Npaq: 0-5 local encoder counter (square wave encoders), + 6-8 MXR emulated quadrature (sine encoders, see EmulatedQuadratureChannel). + pso_counts_divider: EmulatedQuadratureDivider of the axis when pso_input is 6-8, + PSO counts = position * CountsPerUnit / pso_counts_divider. """ USER_ACCESS = [ @@ -533,7 +541,8 @@ class A3200Motor(Device, PositionerBase): tolerance=0.001, require_homed=True, acceleration=None, - encoder_channel=None, + pso_input=None, + pso_counts_divider=1, socket_cls=SocketIO, device_manager=None, **kwargs, @@ -547,7 +556,8 @@ class A3200Motor(Device, PositionerBase): self.sign = sign self.tolerance = tolerance self.require_homed = require_homed - self.encoder_channel = encoder_channel + self.pso_input = pso_input + self.pso_counts_divider = pso_counts_divider self._egu = egu self.device_manager = device_manager @@ -676,10 +686,14 @@ class A3200Motor(Device, PositionerBase): leaves it. The rising edge starts the exposure sequence of a scan line. The axis must be at rest outside the window. """ - if self.encoder_channel is None: - raise A3200Error(f"{self.name}: encoder_channel is not configured, no PSO trigger.") + if self.pso_input is None: + raise A3200Error(f"{self.name}: pso_input is not configured, no PSO trigger.") self.controller.pso_arm_window( - self.axis_Id, self.encoder_channel, start * self.sign, end * self.sign + self.axis_Id, + self.pso_input, + start * self.sign, + end * self.sign, + counts_divider=self.pso_counts_divider, ) def disarm_line_trigger(self) -> None: @@ -689,7 +703,9 @@ class A3200Motor(Device, PositionerBase): return self.controller.axis_status(self.axis_Id) -NPAQ_ENCODER_CHANNELS = 6 +# Npaq DATAACQ position sources: 0-5 local encoder counter, 6-11 encoder multiplier (MXR) +# channel 0-5 (max. 4 kHz trigger rate); 12-15 are analog inputs, not used here. +DATAACQ_POSITION_SOURCES = 12 DATAACQ_TRIGGER_UINT1_RISING = 0 DATAACQ_TRIGGER_PSO_FIRING_EVENT = 6 @@ -701,11 +717,14 @@ class A3200PositionCapture(PSIDeviceBase): Each rising edge on the trigger input (default: Input 12 / UINT1, the detector exposure trigger) stores the encoder counters of the configured channels in the drive array. After a scan line, read_line() reads them back, converts them to user - units and publishes them as async data (signals enc0 ... enc5). + units (CountsPerUnit of the axis) and publishes them as async data (signals enc). Args: - channels: Npaq local encoder counter -> A3200 axis name, e.g. {0: "X", 1: "Y"}. - Values are in controller coordinates (the BEC motor sign is not applied). + channels: DATAACQ source -> A3200 axis name, e.g. {6: "X", 2: "Y"}. Npaq sources: + 0-5 local encoder counter (square wave encoders), 6-11 encoder multiplier (MXR) + channel 0-5 (sine encoders, trigger rate max. 4 kHz). The channel of an axis is + its PositionFeedbackChannel, by default its axis index in the Npaq. Values are + in controller coordinates (the BEC motor sign is not applied). drive_axis: any axis of the Npaq, used to address the drive (shared array). trigger_input: DATAACQ trigger, 0 = Input 12 (UINT1) rising edge. """ @@ -715,7 +734,7 @@ class A3200PositionCapture(PSIDeviceBase): positions = Cpt( AsyncMultiSignal, name="positions", - signals=[f"enc{i}" for i in range(NPAQ_ENCODER_CHANNELS)], + signals=[f"enc{i}" for i in range(DATAACQ_POSITION_SOURCES)], ndim=1, async_update={"type": "add", "max_shape": [None]}, max_size=1000, @@ -743,8 +762,10 @@ class A3200PositionCapture(PSIDeviceBase): ) self.channels = {int(ch): str(axis) for ch, axis in (channels or {}).items()} for ch in self.channels: - if not 0 <= ch < NPAQ_ENCODER_CHANNELS: - raise ValueError(f"Encoder channel {ch} out of range 0..{NPAQ_ENCODER_CHANNELS-1}") + if not 0 <= ch < DATAACQ_POSITION_SOURCES: + raise ValueError( + f"DATAACQ source {ch} out of range 0..{DATAACQ_POSITION_SOURCES - 1}" + ) self.drive_axis = drive_axis self.trigger_input = trigger_input self._offsets = {ch: 0.0 for ch in self.channels} diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index 18f37910..20c6b9e6 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -119,7 +119,8 @@ class SimA3200State: self._axes = {} self._lock = threading.RLock() self.globals = {} - self.channel_axis = {} # Npaq encoder channel -> axis name + self.channel_axis = {} # DATAACQ source -> axis name + self.pso_input_axis = {} # PSO input -> (axis name, counts divider) self.drive_array = [0.0] * 4096 self.pso = {"armed": False, "mode": None, "channel": None, "range": None, "load": 0} self.pso_window_entries = 0 # rising edges of the PSO window output (line triggers) @@ -145,10 +146,12 @@ class SimA3200State: pso, acq = self.pso, self.dataacq if not (pso["armed"] and pso["mode"] == "window" and pso["range"] is not None): return - if self.channel_axis.get(pso["channel"]) != ax.name: + pso_axis, divider = self.pso_input_axis.get(pso["channel"], (None, 1)) + if pso_axis != ax.name: return - low = start + pso["range"][0] / ax.counts_per_unit - high = start + pso["range"][1] / ax.counts_per_unit + pso_counts_per_unit = ax.counts_per_unit / divider + low = start + pso["range"][0] / pso_counts_per_unit + high = start + pso["range"][1] / pso_counts_per_unit if not (min(start, target) <= low and high <= max(start, target)): return self.pso_window_entries += 1 @@ -175,15 +178,16 @@ class SimA3200State: home_velocity=None, counts_per_unit=None, counter_offset=None, - encoder_channel=None, + pso_input=None, + pso_counts_divider=1, ): ax = self.axis(name) if counts_per_unit is not None: ax.counts_per_unit = float(counts_per_unit) if counter_offset is not None: ax.counter_offset = int(counter_offset) - if encoder_channel is not None: - self.channel_axis[int(encoder_channel)] = ax.name + if pso_input is not None: + self.pso_input_axis[int(pso_input)] = (ax.name, float(pso_counts_divider)) if initial_position is not None: ax.pos = float(initial_position) if enabled is not None: @@ -407,7 +411,8 @@ class SimA3200Motor(A3200Motor): tolerance=0.001, require_homed=True, acceleration=None, - encoder_channel=None, + pso_input=None, + pso_counts_divider=1, device_manager=None, sim_initial_position=None, sim_enabled=True, @@ -431,7 +436,8 @@ class SimA3200Motor(A3200Motor): tolerance=tolerance, require_homed=require_homed, acceleration=acceleration, - encoder_channel=encoder_channel, + pso_input=pso_input, + pso_counts_divider=pso_counts_divider, socket_cls=SimA3200Socket, device_manager=device_manager, **kwargs, @@ -447,7 +453,8 @@ class SimA3200Motor(A3200Motor): home_velocity=sim_home_velocity, counts_per_unit=sim_counts_per_unit, counter_offset=sim_counter_offset, - encoder_channel=encoder_channel, + pso_input=pso_input, + pso_counts_divider=pso_counts_divider, ) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 338f5407..02353f02 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -115,7 +115,7 @@ class AerotechContGrid(ScanBase): detector trigger. Args: - fast_axis (DeviceBase): A3200 axis with continuous motion (needs encoder_channel and acceleration configured). + fast_axis (DeviceBase): A3200 axis with continuous motion (needs pso_input and acceleration configured). fast_start (float): Start position for measurement points of the fast axis. fast_end (float): End position for measurement points of the fast axis. fast_step_size (float): Distance travelled by the fast axis during one exposure. diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index 9567841c..7fcd7ceb 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -215,17 +215,15 @@ def _sim_state(): def _capture_setup(): - x = _sim_motor( - "X", axis_nr=0, encoder_channel=0, sim_counts_per_unit=20000, sim_counter_offset=777 - ) - y = _sim_motor( - "Y", axis_nr=1, encoder_channel=1, sim_initial_position=0.3, sim_counter_offset=-55 - ) + # SAXS-TT numbering: X sine encoder (MXR channel 0, emulated quadrature to PSO input 6), + # Y square wave encoder (local encoder counter 2) + x = _sim_motor("X", axis_nr=0, pso_input=6, sim_counts_per_unit=20000, sim_counter_offset=777) + y = _sim_motor("Y", axis_nr=2, pso_input=2, sim_initial_position=0.3, sim_counter_offset=-55) capture = SimA3200PositionCapture( name="aerocapture", host=_HOST, port=_PORT, - channels={0: "X", 1: "Y"}, + channels={6: "X", 2: "Y"}, device_manager=DMMock(), ) capture.wait_for_connection() @@ -262,17 +260,29 @@ def test_line_trigger_window_is_relative_to_current_position(): assert not _sim_state().pso["armed"] -def test_line_trigger_needs_encoder_channel(): +def test_line_trigger_uses_emulated_quadrature_divider(): + u = _sim_motor( + "U", axis_nr=3, egu="deg", pso_input=7, pso_counts_divider=40, sim_counts_per_unit=560000 + ) + u.move(-1.0, wait=True) + u.arm_line_trigger(0.0, 1.0) + assert "[PUT]: PSOWINDOW U 1 INPUT 7" in u.controller.command_history + assert _sim_state().pso["range"] == (14000.0, 28000.0) + u.move(2.0, wait=True) + assert _sim_state().pso_window_entries == 1 + + +def test_line_trigger_needs_pso_input(): motor = _sim_motor() - with pytest.raises(A3200Error, match="encoder_channel"): + with pytest.raises(A3200Error, match="pso_input"): motor.arm_line_trigger(0.0, 1.0) def test_capture_calibration_removes_counter_offset(): _, _, capture = _capture_setup() offsets = capture.calibrate() - assert offsets[0] == pytest.approx(-777 / 20000) - assert offsets[1] == pytest.approx(55 / 10000) + assert offsets[6] == pytest.approx(-777 / 20000) + assert offsets[2] == pytest.approx(55 / 10000) def test_capture_line_publishes_positions(): @@ -285,10 +295,10 @@ def test_capture_line_publishes_positions(): result = capture.read_line() - assert result["enc0"] == pytest.approx([0.0, 0.2, 0.4, 0.6, 0.8]) - assert result["enc1"] == pytest.approx([0.3] * 5) + assert result["enc6"] == pytest.approx([0.0, 0.2, 0.4, 0.6, 0.8]) + assert result["enc2"] == pytest.approx([0.3] * 5) msg = capture.positions.get() - assert set(msg.signals) == {"aerocapture_positions_enc0", "aerocapture_positions_enc1"} + assert set(msg.signals) == {"aerocapture_positions_enc2", "aerocapture_positions_enc6"} assert msg.metadata["async_update"] == {"type": "add", "max_shape": [None]} @@ -300,7 +310,7 @@ def test_capture_reads_in_chunks(): x.arm_line_trigger(0.0, 1.0) x.move(2.0, wait=True) result = capture.read_line() - assert len(result["enc0"]) == 4 + assert len(result["enc6"]) == 4 reads = [c for c in capture.controller.command_history if "ARRAY X READ" in c] assert reads == [ "[PUT]: ARRAY X READ $global[0] 0 3 FAST", @@ -312,5 +322,16 @@ def test_capture_reads_in_chunks(): def test_capture_needs_two_channels(): with pytest.raises(ValueError, match="two encoder channels"): SimA3200PositionCapture( - name="aerocapture", host=_HOST, port=_PORT, channels={0: "X"}, device_manager=DMMock() + name="aerocapture", host=_HOST, port=_PORT, channels={6: "X"}, device_manager=DMMock() + ) + + +def test_capture_rejects_analog_sources(): + with pytest.raises(ValueError, match="out of range"): + SimA3200PositionCapture( + name="aerocapture", + host=_HOST, + port=_PORT, + channels={6: "X", 12: "Y"}, + device_manager=DMMock(), ) -- 2.54.0 From 9c48bc76269c306cc5d02cb3a530e2e183be77f4 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 15:19:20 +0100 Subject: [PATCH 06/52] config(aerotech): position capture disabled for the first commissioning steps Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/device_configs/sastt_aerotech.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index 077a42c4..b64ebb0a 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -108,6 +108,7 @@ aerorot: deviceTags: - sastt_aerotech aerocapture: + # disabled until the position sampling stage (moves -> continuous scans -> trigger -> capture) description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture deviceConfig: @@ -120,7 +121,7 @@ aerocapture: 11: Z 2: Y 9: U - enabled: true + enabled: false onFailure: buffer readOnly: false readoutPriority: async -- 2.54.0 From 763c8e0c2735ca7b99c563859514f0be66a81e7b Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 16:12:01 +0100 Subject: [PATCH 07/52] feat(panda): PandABox line trigger as DDG stand-in for the SAXS-TT Aerotech tests Device PandaLineTrigger (control port only): on the first line trigger edge (TTLIN3 or the software trigger BITS.A) after arm_line(), PULSE3 -> TTLOUT2 sends a shutter pulse and PULSE4 -> TTLOUT3 n detector pulses; COUNTER8 counts them, SRGATE1 is a one-shot arm gate. PULSE parameters are written only when they change: on PandA FPGA 4.1 an ENABLE edge or a parameter write often replays the previous train without a trigger. Design SASTT_line_trigger saved on omny-panda, copy in layouts/sastt_line_trigger.ini. Config sastt_aerotech_panda.yaml = sastt_aerotech.yaml + pandatrig. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- .../device_configs/sastt_aerotech_panda.yaml | 133 +++ csaxs_bec/devices/aerotech/README.md | 30 + .../panda_box/layouts/sastt_line_trigger.ini | 879 ++++++++++++++++++ .../devices/panda_box/panda_line_trigger.py | 328 +++++++ .../tests_devices/test_panda_line_trigger.py | 166 ++++ 5 files changed, 1536 insertions(+) create mode 100644 csaxs_bec/device_configs/sastt_aerotech_panda.yaml create mode 100644 csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini create mode 100644 csaxs_bec/devices/panda_box/panda_line_trigger.py create mode 100644 tests/tests_devices/test_panda_line_trigger.py diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml new file mode 100644 index 00000000..5bfcfe7a --- /dev/null +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -0,0 +1,133 @@ +############################################################ +# SAXS-TT Aerotech test setup without beamline timing: sastt_aerotech.yaml + PandABox +# omny-panda as line trigger delay generator (device pandatrig, layout +# devices/panda_box/layouts/sastt_line_trigger.ini, written by the device at connect). +# Wiring: Npaq J7 opto output (J7-25 +5 V -> J7-6, J7-7 -> TTLIN3, J7-24 -> TTLIN3 ground) +# TTLOUT2 "shutter" (trigger edge to end of last exposure), TTLOUT3 "detector" pulses +# (-> scope, later Npaq J12 pin 1 +, pin 2 -). +# Keep the device list below in sync with sastt_aerotech.yaml. +############################################################ + +aerox: + description: Aerotech X, ALS25020 linear stage + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: X + axis_nr: 0 + pso_input: 6 + pso_counts_divider: 1 + acceleration: 10.0 + host: pc14643.psi.ch + port: 8000 + velocity: 5.0 + egu: mm + limits: + - -50 + - 150 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroy: + description: Aerotech Y, AVSI125 vertical lift + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: Y + axis_nr: 2 + pso_input: 2 + acceleration: 10.0 + host: pc14643.psi.ch + port: 8000 + velocity: 5.0 + egu: mm + limits: + - -10 + - 10 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aeroz: + description: Aerotech Z, ATS20010 linear stage (no PSO input) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: Z + axis_nr: 1 + acceleration: 10.0 + host: pc14643.psi.ch + port: 8000 + velocity: 5.0 + egu: mm + limits: + - -100 + - 100 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aerorot: + description: Aerotech U, ABRT-200 rotation stage (UU follows as gantry slave) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor + deviceConfig: + axis_Id: U + axis_nr: 3 + pso_input: 7 + pso_counts_divider: 40 + acceleration: 2000.0 + host: pc14643.psi.ch + port: 8000 + velocity: 30.0 + egu: deg + tolerance: 0.001 + limits: + - 0 + - 0 + sign: 1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech +aerocapture: + # disabled until the position sampling stage (moves -> continuous scans -> trigger -> capture) + description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) + deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture + deviceConfig: + host: pc14643.psi.ch + port: 8000 + drive_axis: X + # DATAACQ source -> axis: 0-5 local encoder counter, 6-11 MXR channel 0-5 (max. 4 kHz) + channels: + 6: X + 11: Z + 2: Y + 9: U + enabled: false + onFailure: buffer + readOnly: false + readoutPriority: async + deviceTags: + - sastt_aerotech +pandatrig: + description: PandABox line trigger (shutter + detector pulses on the line trigger edge) + deviceClass: csaxs_bec.devices.panda_box.panda_line_trigger.PandaLineTrigger + deviceConfig: + host: omny-panda.psi.ch + port: 8888 + input_termination: 50-Ohm + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline + deviceTags: + - sastt_aerotech diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 86d65c7f..5ef3ab79 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -94,3 +94,33 @@ Commissioning of the scan: `$global[254]` as scratch variable. Another client must not use them during a scan. 6. Check one line with the scope: PSO edge at the trigger position, number of UINT1 edges = frames per line, captured positions vs. requested positions. + +## Test setup without beamline timing: PandABox line trigger + +Config `device_configs/sastt_aerotech_panda.yaml` = `sastt_aerotech.yaml` + device +`pandatrig` (`devices/panda_box/panda_line_trigger.py`, PandA `omny-panda.psi.ch`). The PandA +replaces DDG1/DDG2: on the line trigger edge it sends a shutter pulse and n detector pulses, +8 ns resolution. + +| PandA | Use | Wiring | +|-------|-----|--------| +| TTLIN3 (50 Ω) | line trigger | Npaq J7 opto output: J7-25 (+5 V) -> J7-6, J7-7 -> TTLIN3, J7-24 -> TTLIN3 ground | +| TTLOUT2 | "shutter": trigger edge to end of the last exposure | scope | +| TTLOUT3 | "detector": n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope, later Npaq J12 pin 1 (+) / pin 2 (-) for position sampling | + +Blocks: LUT8 = TTLIN3 | BITS.A (software trigger), SRGATE1 one-shot arm gate (set by +`arm_line`, reset by the first edge), LUT7 = LUT8 & SRGATE1 -> PULSE3 (shutter), PULSE4 +(detector), COUNTER8 counts the PULSE4 pulses. Design `SASTT_line_trigger` on the PandA web +interface, copy in `devices/panda_box/layouts/sastt_line_trigger.ini`; the device also writes +the wiring at connect. + +```python +dev.pandatrig.arm_line(n_pulses=100, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) +dev.pandatrig.software_trigger() # or the PSO edge on TTLIN3 +dev.pandatrig.wait_line_done(100, timeout=5) +``` + +PandA FPGA 4.1 PULSE block: an ENABLE edge or a parameter write often replays the previous +pulse train without a trigger. The device keeps the PULSE blocks enabled and writes their +parameters only when they change (once per scan), then waits until such a train is over; +during that wait pulses can appear on TTLOUT2/3. diff --git a/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini b/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini new file mode 100644 index 00000000..af2554ca --- /dev/null +++ b/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini @@ -0,0 +1,879 @@ +OUTENC1.DATA.DELAY=0 +OUTENC2.DATA.DELAY=0 +OUTENC3.DATA.DELAY=0 +OUTENC4.DATA.DELAY=0 +OUTENC1.A.DELAY=0 +OUTENC2.A.DELAY=0 +OUTENC3.A.DELAY=0 +OUTENC4.A.DELAY=0 +OUTENC1.QPERIOD.UNITS=s +OUTENC2.QPERIOD.UNITS=s +OUTENC3.QPERIOD.UNITS=s +OUTENC4.QPERIOD.UNITS=s +OUTENC1.Z.DELAY=0 +OUTENC2.Z.DELAY=0 +OUTENC3.Z.DELAY=0 +OUTENC4.Z.DELAY=0 +OUTENC1.B.DELAY=0 +OUTENC2.B.DELAY=0 +OUTENC3.B.DELAY=0 +OUTENC4.B.DELAY=0 +OUTENC1.ENABLE.DELAY=0 +OUTENC2.ENABLE.DELAY=0 +OUTENC3.ENABLE.DELAY=0 +OUTENC4.ENABLE.DELAY=0 +PCOMP1.ENABLE.DELAY=0 +PCOMP2.ENABLE.DELAY=0 +TTLOUT1.VAL.DELAY=0 +TTLOUT2.VAL.DELAY=0 +TTLOUT3.VAL.DELAY=0 +TTLOUT4.VAL.DELAY=0 +TTLOUT5.VAL.DELAY=0 +TTLOUT6.VAL.DELAY=0 +TTLOUT7.VAL.DELAY=0 +TTLOUT8.VAL.DELAY=0 +TTLOUT9.VAL.DELAY=0 +TTLOUT10.VAL.DELAY=0 +PCAP.TS_END.CAPTURE=No +PCAP.GATE.DELAY=1 +PCAP.TS_START.CAPTURE=No +PCAP.ENABLE.DELAY=0 +PCAP.BITS2.CAPTURE=No +PCAP.BITS0.CAPTURE=Value +PCAP.BITS3.CAPTURE=No +PCAP.BITS1.CAPTURE=No +PCAP.GATE_DURATION.CAPTURE=Value +PCAP.TS_TRIG.CAPTURE=No +PCAP.TRIG.DELAY=1 +SFP3_SYNC_IN.POS3.UNITS= +SFP3_SYNC_IN.POS3.OFFSET=0 +SFP3_SYNC_IN.POS3.SCALE=1 +SFP3_SYNC_IN.POS3.CAPTURE=No +SFP3_SYNC_IN.POS4.UNITS= +SFP3_SYNC_IN.POS4.OFFSET=0 +SFP3_SYNC_IN.POS4.SCALE=1 +SFP3_SYNC_IN.POS4.CAPTURE=No +SFP3_SYNC_IN.POS2.UNITS= +SFP3_SYNC_IN.POS2.OFFSET=0 +SFP3_SYNC_IN.POS2.SCALE=1 +SFP3_SYNC_IN.POS2.CAPTURE=No +SFP3_SYNC_IN.POS1.UNITS= +SFP3_SYNC_IN.POS1.OFFSET=0 +SFP3_SYNC_IN.POS1.SCALE=1 +SFP3_SYNC_IN.POS1.CAPTURE=No +DIV1.INP.DELAY=0 +DIV2.INP.DELAY=0 +DIV1.ENABLE.DELAY=0 +DIV2.ENABLE.DELAY=0 +INENC1.CLK.DELAY=0 +INENC2.CLK.DELAY=0 +INENC3.CLK.DELAY=0 +INENC4.CLK.DELAY=0 +INENC1.VAL.UNITS= +INENC2.VAL.UNITS= +INENC3.VAL.UNITS= +INENC4.VAL.UNITS= +INENC1.VAL.OFFSET=0 +INENC2.VAL.OFFSET=0 +INENC3.VAL.OFFSET=0 +INENC4.VAL.OFFSET=0 +INENC1.VAL.SCALE=1 +INENC2.VAL.SCALE=1 +INENC3.VAL.SCALE=1 +INENC4.VAL.SCALE=1 +INENC1.VAL.CAPTURE=Value +INENC2.VAL.CAPTURE=Value +INENC3.VAL.CAPTURE=Value +INENC4.VAL.CAPTURE=Value +INENC1.CLK_PERIOD.UNITS=s +INENC2.CLK_PERIOD.UNITS=s +INENC3.CLK_PERIOD.UNITS=s +INENC4.CLK_PERIOD.UNITS=s +INENC1.FRAME_PERIOD.UNITS=s +INENC2.FRAME_PERIOD.UNITS=s +INENC3.FRAME_PERIOD.UNITS=s +INENC4.FRAME_PERIOD.UNITS=s +SFP3_SYNC_OUT.BIT8.DELAY=0 +SFP3_SYNC_OUT.BIT1.DELAY=0 +SFP3_SYNC_OUT.BIT3.DELAY=0 +SFP3_SYNC_OUT.BIT2.DELAY=0 +SFP3_SYNC_OUT.BIT5.DELAY=0 +SFP3_SYNC_OUT.BIT4.DELAY=0 +SFP3_SYNC_OUT.BIT7.DELAY=0 +SFP3_SYNC_OUT.BIT6.DELAY=0 +SEQ1.ENABLE.DELAY=0 +SEQ2.ENABLE.DELAY=0 +SEQ1.PRESCALE.UNITS=s +SEQ2.PRESCALE.UNITS=s +SEQ1.BITA.DELAY=0 +SEQ2.BITA.DELAY=0 +SEQ1.BITC.DELAY=0 +SEQ2.BITC.DELAY=0 +SEQ1.BITB.DELAY=0 +SEQ2.BITB.DELAY=0 +SEQ1.TABLE.QUEUED_LINES=0 +SEQ2.TABLE.QUEUED_LINES=0 +SRGATE1.SET.DELAY=0 +SRGATE2.SET.DELAY=0 +SRGATE3.SET.DELAY=0 +SRGATE4.SET.DELAY=0 +SRGATE1.ENABLE.DELAY=0 +SRGATE2.ENABLE.DELAY=0 +SRGATE3.ENABLE.DELAY=0 +SRGATE4.ENABLE.DELAY=0 +SRGATE1.RST.DELAY=0 +SRGATE2.RST.DELAY=0 +SRGATE3.RST.DELAY=0 +SRGATE4.RST.DELAY=0 +LUT1.INPB.DELAY=0 +LUT2.INPB.DELAY=0 +LUT3.INPB.DELAY=0 +LUT4.INPB.DELAY=0 +LUT5.INPB.DELAY=0 +LUT6.INPB.DELAY=0 +LUT7.INPB.DELAY=0 +LUT8.INPB.DELAY=0 +LUT1.INPC.DELAY=0 +LUT2.INPC.DELAY=0 +LUT3.INPC.DELAY=0 +LUT4.INPC.DELAY=0 +LUT5.INPC.DELAY=0 +LUT6.INPC.DELAY=0 +LUT7.INPC.DELAY=0 +LUT8.INPC.DELAY=0 +LUT1.INPA.DELAY=0 +LUT2.INPA.DELAY=0 +LUT3.INPA.DELAY=0 +LUT4.INPA.DELAY=0 +LUT5.INPA.DELAY=0 +LUT6.INPA.DELAY=0 +LUT7.INPA.DELAY=0 +LUT8.INPA.DELAY=0 +LUT1.INPD.DELAY=0 +LUT2.INPD.DELAY=0 +LUT3.INPD.DELAY=0 +LUT4.INPD.DELAY=0 +LUT5.INPD.DELAY=0 +LUT6.INPD.DELAY=0 +LUT7.INPD.DELAY=0 +LUT8.INPD.DELAY=0 +LUT1.INPE.DELAY=0 +LUT2.INPE.DELAY=0 +LUT3.INPE.DELAY=0 +LUT4.INPE.DELAY=0 +LUT5.INPE.DELAY=0 +LUT6.INPE.DELAY=0 +LUT7.INPE.DELAY=0 +LUT8.INPE.DELAY=0 +CALC1.OUT.UNITS= +CALC2.OUT.UNITS= +CALC1.OUT.OFFSET=0 +CALC2.OUT.OFFSET=0 +CALC1.OUT.SCALE=1 +CALC2.OUT.SCALE=1 +CALC1.OUT.CAPTURE=No +CALC2.OUT.CAPTURE=No +LVDSOUT1.VAL.DELAY=0 +LVDSOUT2.VAL.DELAY=0 +CLOCK1.ENABLE.DELAY=0 +CLOCK2.ENABLE.DELAY=0 +CLOCK1.PERIOD.UNITS=s +CLOCK2.PERIOD.UNITS=s +CLOCK1.WIDTH.UNITS=s +CLOCK2.WIDTH.UNITS=us +COUNTER1.OUT.UNITS= +COUNTER2.OUT.UNITS= +COUNTER3.OUT.UNITS= +COUNTER4.OUT.UNITS= +COUNTER5.OUT.UNITS= +COUNTER6.OUT.UNITS= +COUNTER7.OUT.UNITS= +COUNTER8.OUT.UNITS= +COUNTER1.OUT.OFFSET=0 +COUNTER2.OUT.OFFSET=0 +COUNTER3.OUT.OFFSET=0 +COUNTER4.OUT.OFFSET=0 +COUNTER5.OUT.OFFSET=0 +COUNTER6.OUT.OFFSET=0 +COUNTER7.OUT.OFFSET=0 +COUNTER8.OUT.OFFSET=0 +COUNTER1.OUT.SCALE=1 +COUNTER2.OUT.SCALE=1 +COUNTER3.OUT.SCALE=1 +COUNTER4.OUT.SCALE=1 +COUNTER5.OUT.SCALE=1 +COUNTER6.OUT.SCALE=1 +COUNTER7.OUT.SCALE=1 +COUNTER8.OUT.SCALE=1 +COUNTER1.OUT.CAPTURE=Value +COUNTER2.OUT.CAPTURE=No +COUNTER3.OUT.CAPTURE=No +COUNTER4.OUT.CAPTURE=No +COUNTER5.OUT.CAPTURE=No +COUNTER6.OUT.CAPTURE=No +COUNTER7.OUT.CAPTURE=No +COUNTER8.OUT.CAPTURE=No +COUNTER1.DIR.DELAY=0 +COUNTER2.DIR.DELAY=0 +COUNTER3.DIR.DELAY=0 +COUNTER4.DIR.DELAY=0 +COUNTER5.DIR.DELAY=0 +COUNTER6.DIR.DELAY=0 +COUNTER7.DIR.DELAY=0 +COUNTER8.DIR.DELAY=0 +COUNTER1.ENABLE.DELAY=0 +COUNTER2.ENABLE.DELAY=0 +COUNTER3.ENABLE.DELAY=0 +COUNTER4.ENABLE.DELAY=0 +COUNTER5.ENABLE.DELAY=0 +COUNTER6.ENABLE.DELAY=0 +COUNTER7.ENABLE.DELAY=0 +COUNTER8.ENABLE.DELAY=0 +COUNTER1.TRIG.DELAY=0 +COUNTER2.TRIG.DELAY=0 +COUNTER3.TRIG.DELAY=0 +COUNTER4.TRIG.DELAY=0 +COUNTER5.TRIG.DELAY=0 +COUNTER6.TRIG.DELAY=0 +COUNTER7.TRIG.DELAY=0 +COUNTER8.TRIG.DELAY=0 +PULSE1.ENABLE.DELAY=0 +PULSE2.ENABLE.DELAY=0 +PULSE3.ENABLE.DELAY=0 +PULSE4.ENABLE.DELAY=0 +PULSE1.DELAY.UNITS=s +PULSE2.DELAY.UNITS=s +PULSE3.DELAY.UNITS=s +PULSE4.DELAY.UNITS=s +PULSE1.TRIG.DELAY=0 +PULSE2.TRIG.DELAY=0 +PULSE3.TRIG.DELAY=0 +PULSE4.TRIG.DELAY=0 +PULSE1.WIDTH.UNITS=s +PULSE2.WIDTH.UNITS=s +PULSE3.WIDTH.UNITS=s +PULSE4.WIDTH.UNITS=s +PULSE1.STEP.UNITS=s +PULSE2.STEP.UNITS=s +PULSE3.STEP.UNITS=s +PULSE4.STEP.UNITS=s +FILTER1.OUT.UNITS= +FILTER2.OUT.UNITS= +FILTER1.OUT.OFFSET=0 +FILTER2.OUT.OFFSET=0 +FILTER1.OUT.SCALE=1 +FILTER2.OUT.SCALE=1 +FILTER1.OUT.CAPTURE=No +FILTER2.OUT.CAPTURE=No +FILTER1.ENABLE.DELAY=0 +FILTER2.ENABLE.DELAY=0 +FILTER1.TRIG.DELAY=0 +FILTER2.TRIG.DELAY=0 +FMC_IN.VAL8.UNITS=V +FMC_IN.VAL8.OFFSET=0 +FMC_IN.VAL8.SCALE=4.65661287e-09 +FMC_IN.VAL8.CAPTURE=No +FMC_IN.VAL1.UNITS=V +FMC_IN.VAL1.OFFSET=0 +FMC_IN.VAL1.SCALE=4.65661287e-09 +FMC_IN.VAL1.CAPTURE=Value +FMC_IN.VAL3.UNITS=V +FMC_IN.VAL3.OFFSET=0 +FMC_IN.VAL3.SCALE=4.65661287e-09 +FMC_IN.VAL3.CAPTURE=No +FMC_IN.VAL2.UNITS=V +FMC_IN.VAL2.OFFSET=0 +FMC_IN.VAL2.SCALE=4.65661287e-09 +FMC_IN.VAL2.CAPTURE=Value +FMC_IN.VAL5.UNITS=V +FMC_IN.VAL5.OFFSET=0 +FMC_IN.VAL5.SCALE=4.65661287e-09 +FMC_IN.VAL5.CAPTURE=No +FMC_IN.VAL4.UNITS=V +FMC_IN.VAL4.OFFSET=0 +FMC_IN.VAL4.SCALE=4.65661287e-09 +FMC_IN.VAL4.CAPTURE=No +FMC_IN.VAL7.UNITS=V +FMC_IN.VAL7.OFFSET=0 +FMC_IN.VAL7.SCALE=4.65661287e-09 +FMC_IN.VAL7.CAPTURE=No +FMC_IN.VAL6.UNITS=V +FMC_IN.VAL6.OFFSET=0 +FMC_IN.VAL6.SCALE=4.65661287e-09 +FMC_IN.VAL6.CAPTURE=No +PGEN1.OUT.UNITS= +PGEN2.OUT.UNITS= +PGEN1.OUT.OFFSET=0 +PGEN2.OUT.OFFSET=0 +PGEN1.OUT.SCALE=1 +PGEN2.OUT.SCALE=1 +PGEN1.OUT.CAPTURE=No +PGEN2.OUT.CAPTURE=No +PGEN1.ENABLE.DELAY=0 +PGEN2.ENABLE.DELAY=0 +PGEN1.TABLE.QUEUED_LINES=0 +PGEN2.TABLE.QUEUED_LINES=0 +PGEN1.TRIG.DELAY=0 +PGEN2.TRIG.DELAY=0 +TTLIN1.TERM=High-Z +TTLIN2.TERM=High-Z +TTLIN3.TERM=50-Ohm +TTLIN4.TERM=High-Z +TTLIN5.TERM=High-Z +TTLIN6.TERM=High-Z +OUTENC1.DATA=ZERO +OUTENC2.DATA=ZERO +OUTENC3.DATA=ZERO +OUTENC4.DATA=ZERO +OUTENC1.A=ZERO +OUTENC2.A=ZERO +OUTENC3.A=ZERO +OUTENC4.A=ZERO +OUTENC1.ENCODING=Unsigned Binary +OUTENC2.ENCODING=Unsigned Binary +OUTENC3.ENCODING=Unsigned Binary +OUTENC4.ENCODING=Unsigned Binary +OUTENC1.QPERIOD=0 +OUTENC2.QPERIOD=0 +OUTENC3.QPERIOD=0 +OUTENC4.QPERIOD=0 +OUTENC1.Z=ZERO +OUTENC2.Z=ZERO +OUTENC3.Z=ZERO +OUTENC4.Z=ZERO +OUTENC1.B=ZERO +OUTENC2.B=ZERO +OUTENC3.B=ZERO +OUTENC4.B=ZERO +OUTENC1.VAL=ZERO +OUTENC2.VAL=ZERO +OUTENC3.VAL=ZERO +OUTENC4.VAL=ZERO +OUTENC1.PROTOCOL=Quadrature +OUTENC2.PROTOCOL=Quadrature +OUTENC3.PROTOCOL=Quadrature +OUTENC4.PROTOCOL=Quadrature +OUTENC1.ENABLE=ZERO +OUTENC2.ENABLE=ZERO +OUTENC3.ENABLE=ZERO +OUTENC4.ENABLE=ZERO +OUTENC1.GENERATOR_ERROR=No +OUTENC2.GENERATOR_ERROR=No +OUTENC3.GENERATOR_ERROR=No +OUTENC4.GENERATOR_ERROR=No +OUTENC1.BITS=0 +OUTENC2.BITS=0 +OUTENC3.BITS=0 +OUTENC4.BITS=0 +PCOMP1.PULSES=0 +PCOMP2.PULSES=0 +PCOMP1.PRE_START=0 +PCOMP2.PRE_START=0 +PCOMP1.ENABLE=ZERO +PCOMP2.ENABLE=ZERO +PCOMP1.DIR=Positive +PCOMP2.DIR=Positive +PCOMP1.START=0 +PCOMP2.START=0 +PCOMP1.WIDTH=0 +PCOMP2.WIDTH=0 +PCOMP1.INP=ZERO +PCOMP2.INP=ZERO +PCOMP1.RELATIVE=Absolute +PCOMP2.RELATIVE=Absolute +PCOMP1.STEP=0 +PCOMP2.STEP=0 +TTLOUT1.FINE_DELAY=0 +TTLOUT2.FINE_DELAY=0 +TTLOUT3.FINE_DELAY=0 +TTLOUT4.FINE_DELAY=0 +TTLOUT5.FINE_DELAY=0 +TTLOUT6.FINE_DELAY=0 +TTLOUT7.FINE_DELAY=0 +TTLOUT8.FINE_DELAY=0 +TTLOUT9.FINE_DELAY=0 +TTLOUT10.FINE_DELAY=0 +TTLOUT1.QUARTER_DELAY=0 +TTLOUT2.QUARTER_DELAY=0 +TTLOUT3.QUARTER_DELAY=0 +TTLOUT4.QUARTER_DELAY=0 +TTLOUT5.QUARTER_DELAY=0 +TTLOUT6.QUARTER_DELAY=0 +TTLOUT7.QUARTER_DELAY=0 +TTLOUT8.QUARTER_DELAY=0 +TTLOUT9.QUARTER_DELAY=0 +TTLOUT10.QUARTER_DELAY=0 +TTLOUT1.VAL=ZERO +TTLOUT2.VAL=PULSE3.OUT +TTLOUT3.VAL=PULSE4.OUT +TTLOUT4.VAL=ZERO +TTLOUT5.VAL=ZERO +TTLOUT6.VAL=ZERO +TTLOUT7.VAL=ZERO +TTLOUT8.VAL=ZERO +TTLOUT9.VAL=ZERO +TTLOUT10.VAL=ZERO +PCAP.GATE=ZERO +PCAP.ENABLE=ZERO +PCAP.SHIFT_SUM=0 +PCAP.TRIG_EDGE=Falling +PCAP.TRIG=ZERO +DIV1.DIVISOR=0 +DIV2.DIVISOR=0 +DIV1.FIRST_PULSE=OutN +DIV2.FIRST_PULSE=OutN +DIV1.INP=ZERO +DIV2.INP=ZERO +DIV1.ENABLE=ZERO +DIV2.ENABLE=ZERO +INENC1.ENCODING=Unsigned Binary +INENC2.ENCODING=Unsigned Binary +INENC3.ENCODING=Unsigned Binary +INENC4.ENCODING=Unsigned Binary +INENC1.CLK=ZERO +INENC2.CLK=ZERO +INENC3.CLK=ZERO +INENC4.CLK=ZERO +INENC1.MSB_DISCARD=0 +INENC2.MSB_DISCARD=0 +INENC3.MSB_DISCARD=0 +INENC4.MSB_DISCARD=0 +INENC1.LSB_DISCARD=0 +INENC2.LSB_DISCARD=0 +INENC3.LSB_DISCARD=0 +INENC4.LSB_DISCARD=0 +INENC1.CLK_SRC=Internally Generated +INENC2.CLK_SRC=Internally Generated +INENC3.CLK_SRC=Internally Generated +INENC4.CLK_SRC=Internally Generated +INENC1.PROTOCOL=ZMI +INENC2.PROTOCOL=ZMI +INENC3.PROTOCOL=ZMI +INENC4.PROTOCOL=ZMI +INENC1.RST_ON_Z=0 +INENC2.RST_ON_Z=0 +INENC3.RST_ON_Z=0 +INENC4.RST_ON_Z=0 +INENC1.CLK_PERIOD=0.2 +INENC2.CLK_PERIOD=0 +INENC3.CLK_PERIOD=0 +INENC4.CLK_PERIOD=0 +INENC1.BITS=0 +INENC2.BITS=0 +INENC3.BITS=0 +INENC4.BITS=0 +INENC1.FRAME_PERIOD=0.2 +INENC2.FRAME_PERIOD=0 +INENC3.FRAME_PERIOD=0 +INENC4.FRAME_PERIOD=0 +BITS.A=0 +BITS.C=0 +BITS.D=0 +BITS.B=0 +SFP3_SYNC_OUT.POS4=ZERO +SFP3_SYNC_OUT.POS2=ZERO +SFP3_SYNC_OUT.POS3=ZERO +SFP3_SYNC_OUT.POS1=ZERO +SFP3_SYNC_OUT.BIT8=ZERO +SFP3_SYNC_OUT.BIT1=ZERO +SFP3_SYNC_OUT.BIT3=ZERO +SFP3_SYNC_OUT.BIT2=ZERO +SFP3_SYNC_OUT.BIT5=ZERO +SFP3_SYNC_OUT.BIT4=ZERO +SFP3_SYNC_OUT.BIT7=ZERO +SFP3_SYNC_OUT.BIT6=ZERO +SEQ1.ENABLE=ZERO +SEQ2.ENABLE=ZERO +SEQ1.PRESCALE=0 +SEQ2.PRESCALE=0 +SEQ1.BITA=ZERO +SEQ2.BITA=ZERO +SEQ1.BITC=ZERO +SEQ2.BITC=ZERO +SEQ1.BITB=ZERO +SEQ2.BITB=ZERO +SEQ1.REPEATS=0 +SEQ2.REPEATS=0 +SEQ1.POSB=ZERO +SEQ2.POSB=ZERO +SEQ1.POSC=ZERO +SEQ2.POSC=ZERO +SEQ1.POSA=ZERO +SEQ2.POSA=ZERO +SYSTEM.CLOCK_SOURCE=int clock +SYSTEM.TIMESTAMP_SOURCE=None +SRGATE1.WHEN_DISABLED=Set output low +SRGATE2.WHEN_DISABLED=Set output low +SRGATE3.WHEN_DISABLED=Set output low +SRGATE4.WHEN_DISABLED=Set output low +SRGATE1.RST_EDGE=Rising +SRGATE2.RST_EDGE=Rising +SRGATE3.RST_EDGE=Rising +SRGATE4.RST_EDGE=Rising +SRGATE1.SET=ZERO +SRGATE2.SET=ZERO +SRGATE3.SET=ZERO +SRGATE4.SET=ZERO +SRGATE1.SET_EDGE=Rising +SRGATE2.SET_EDGE=Rising +SRGATE3.SET_EDGE=Rising +SRGATE4.SET_EDGE=Rising +SRGATE1.ENABLE=ONE +SRGATE2.ENABLE=ZERO +SRGATE3.ENABLE=ZERO +SRGATE4.ENABLE=ZERO +SRGATE1.RST=LUT8.OUT +SRGATE2.RST=ZERO +SRGATE3.RST=ZERO +SRGATE4.RST=ZERO +FMC_OUT.GAIN4=5V +FMC_OUT.GAIN1=5V +FMC_OUT.GAIN3=5V +FMC_OUT.GAIN2=5V +FMC_OUT.VAL1=ZERO +FMC_OUT.VAL3=ZERO +FMC_OUT.VAL2=ZERO +FMC_OUT.VAL4=ZERO +LUT1.INPB=ZERO +LUT2.INPB=ZERO +LUT3.INPB=ZERO +LUT4.INPB=ZERO +LUT5.INPB=ZERO +LUT6.INPB=ZERO +LUT7.INPB=SRGATE1.OUT +LUT8.INPB=BITS.OUTA +LUT1.INPC=ZERO +LUT2.INPC=ZERO +LUT3.INPC=ZERO +LUT4.INPC=ZERO +LUT5.INPC=ZERO +LUT6.INPC=ZERO +LUT7.INPC=ZERO +LUT8.INPC=ZERO +LUT1.INPA=ZERO +LUT2.INPA=ZERO +LUT3.INPA=ZERO +LUT4.INPA=ZERO +LUT5.INPA=ZERO +LUT6.INPA=ZERO +LUT7.INPA=LUT8.OUT +LUT8.INPA=TTLIN3.VAL +LUT1.INPD=ZERO +LUT2.INPD=ZERO +LUT3.INPD=ZERO +LUT4.INPD=ZERO +LUT5.INPD=ZERO +LUT6.INPD=ZERO +LUT7.INPD=ZERO +LUT8.INPD=ZERO +LUT1.INPE=ZERO +LUT2.INPE=ZERO +LUT3.INPE=ZERO +LUT4.INPE=ZERO +LUT5.INPE=ZERO +LUT6.INPE=ZERO +LUT7.INPE=ZERO +LUT8.INPE=ZERO +LUT1.FUNC=0x00000000 +LUT2.FUNC=0x00000000 +LUT3.FUNC=0x00000000 +LUT4.FUNC=0x00000000 +LUT5.FUNC=0x00000000 +LUT6.FUNC=0x00000000 +LUT7.FUNC=A&B +LUT8.FUNC=A|B +LUT1.TYPEA=Input-Level +LUT2.TYPEA=Input-Level +LUT3.TYPEA=Input-Level +LUT4.TYPEA=Input-Level +LUT5.TYPEA=Input-Level +LUT6.TYPEA=Input-Level +LUT7.TYPEA=Input-Level +LUT8.TYPEA=Input-Level +LUT1.TYPEC=Input-Level +LUT2.TYPEC=Input-Level +LUT3.TYPEC=Input-Level +LUT4.TYPEC=Input-Level +LUT5.TYPEC=Input-Level +LUT6.TYPEC=Input-Level +LUT7.TYPEC=Input-Level +LUT8.TYPEC=Input-Level +LUT1.TYPEB=Input-Level +LUT2.TYPEB=Input-Level +LUT3.TYPEB=Input-Level +LUT4.TYPEB=Input-Level +LUT5.TYPEB=Input-Level +LUT6.TYPEB=Input-Level +LUT7.TYPEB=Input-Level +LUT8.TYPEB=Input-Level +LUT1.TYPEE=Input-Level +LUT2.TYPEE=Input-Level +LUT3.TYPEE=Input-Level +LUT4.TYPEE=Input-Level +LUT5.TYPEE=Input-Level +LUT6.TYPEE=Input-Level +LUT7.TYPEE=Input-Level +LUT8.TYPEE=Input-Level +LUT1.TYPED=Input-Level +LUT2.TYPED=Input-Level +LUT3.TYPED=Input-Level +LUT4.TYPED=Input-Level +LUT5.TYPED=Input-Level +LUT6.TYPED=Input-Level +LUT7.TYPED=Input-Level +LUT8.TYPED=Input-Level +CALC1.INPC=ZERO +CALC2.INPC=ZERO +CALC1.INPA=ZERO +CALC2.INPA=ZERO +CALC1.SHIFT=0 +CALC2.SHIFT=0 +CALC1.INPB=ZERO +CALC2.INPB=ZERO +CALC1.INPD=ZERO +CALC2.INPD=ZERO +CALC1.TYPEA=Value +CALC2.TYPEA=Value +CALC1.TYPEC=Value +CALC2.TYPEC=Value +CALC1.TYPEB=Value +CALC2.TYPEB=Value +CALC1.TYPED=Value +CALC2.TYPED=Value +LVDSOUT1.FINE_DELAY=0 +LVDSOUT2.FINE_DELAY=0 +LVDSOUT1.QUARTER_DELAY=0 +LVDSOUT2.QUARTER_DELAY=0 +LVDSOUT1.VAL=ZERO +LVDSOUT2.VAL=ZERO +CLOCK1.ENABLE=ZERO +CLOCK2.ENABLE=ZERO +CLOCK1.PERIOD=0.0001 +CLOCK2.PERIOD=0.2 +CLOCK1.WIDTH=3e-05 +CLOCK2.WIDTH=5 +COUNTER1.OUT_MODE=On-Change +COUNTER2.OUT_MODE=On-Change +COUNTER3.OUT_MODE=On-Change +COUNTER4.OUT_MODE=On-Change +COUNTER5.OUT_MODE=On-Change +COUNTER6.OUT_MODE=On-Change +COUNTER7.OUT_MODE=On-Change +COUNTER8.OUT_MODE=On-Change +COUNTER1.SET=0 +COUNTER2.SET=0 +COUNTER3.SET=0 +COUNTER4.SET=0 +COUNTER5.SET=0 +COUNTER6.SET=0 +COUNTER7.SET=0 +COUNTER8.SET=0 +COUNTER1.DIR=ZERO +COUNTER2.DIR=ZERO +COUNTER3.DIR=ZERO +COUNTER4.DIR=ZERO +COUNTER5.DIR=ZERO +COUNTER6.DIR=ZERO +COUNTER7.DIR=ZERO +COUNTER8.DIR=ZERO +COUNTER1.ENABLE=ZERO +COUNTER2.ENABLE=ZERO +COUNTER3.ENABLE=ZERO +COUNTER4.ENABLE=ZERO +COUNTER5.ENABLE=ZERO +COUNTER6.ENABLE=ZERO +COUNTER7.ENABLE=ONE +COUNTER8.ENABLE=ONE +COUNTER1.TRIG=ZERO +COUNTER2.TRIG=ZERO +COUNTER3.TRIG=ZERO +COUNTER4.TRIG=ZERO +COUNTER5.TRIG=ZERO +COUNTER6.TRIG=ZERO +COUNTER7.TRIG=PULSE4.OUT +COUNTER8.TRIG=PULSE4.OUT +COUNTER1.TRIG_EDGE=Falling +COUNTER2.TRIG_EDGE=Rising +COUNTER3.TRIG_EDGE=Rising +COUNTER4.TRIG_EDGE=Rising +COUNTER5.TRIG_EDGE=Rising +COUNTER6.TRIG_EDGE=Rising +COUNTER7.TRIG_EDGE=Rising +COUNTER8.TRIG_EDGE=Rising +COUNTER1.MIN=0 +COUNTER2.MIN=0 +COUNTER3.MIN=0 +COUNTER4.MIN=0 +COUNTER5.MIN=0 +COUNTER6.MIN=0 +COUNTER7.MIN=0 +COUNTER8.MIN=0 +COUNTER1.MAX=0 +COUNTER2.MAX=0 +COUNTER3.MAX=0 +COUNTER4.MAX=0 +COUNTER5.MAX=0 +COUNTER6.MAX=0 +COUNTER7.MAX=0 +COUNTER8.MAX=0 +COUNTER1.START=0 +COUNTER2.START=0 +COUNTER3.START=0 +COUNTER4.START=0 +COUNTER5.START=0 +COUNTER6.START=0 +COUNTER7.START=0 +COUNTER8.START=0 +COUNTER1.STEP=1 +COUNTER2.STEP=1 +COUNTER3.STEP=0 +COUNTER4.STEP=0 +COUNTER5.STEP=0 +COUNTER6.STEP=1 +COUNTER7.STEP=1 +COUNTER8.STEP=1 +PULSE1.PULSES=0 +PULSE2.PULSES=5 +PULSE3.PULSES=1 +PULSE4.PULSES=10 +PULSE1.ENABLE=ONE +PULSE2.ENABLE=ZERO +PULSE3.ENABLE=ONE +PULSE4.ENABLE=ONE +PULSE1.DELAY=0 +PULSE2.DELAY=0 +PULSE3.DELAY=0 +PULSE4.DELAY=0.001 +PULSE1.TRIG=ZERO +PULSE2.TRIG=ZERO +PULSE3.TRIG=LUT7.OUT +PULSE4.TRIG=LUT7.OUT +PULSE1.TRIG_EDGE=Rising +PULSE2.TRIG_EDGE=Rising +PULSE3.TRIG_EDGE=Rising +PULSE4.TRIG_EDGE=Rising +PULSE1.WIDTH=0.001 +PULSE2.WIDTH=0.002 +PULSE3.WIDTH=0.0108 +PULSE4.WIDTH=0.0008 +PULSE1.STEP=0 +PULSE2.STEP=0.0025 +PULSE3.STEP=0 +PULSE4.STEP=0.001 +FILTER1.INP=ZERO +FILTER2.INP=ZERO +FILTER1.MODE=average +FILTER2.MODE=difference +FILTER1.ENABLE=ZERO +FILTER2.ENABLE=ZERO +FILTER1.TRIG=ZERO +FILTER2.TRIG=ZERO +FMC_IN.GAIN5=10V +FMC_IN.GAIN4=10V +FMC_IN.GAIN7=10V +FMC_IN.GAIN6=10V +FMC_IN.GAIN1=10V +FMC_IN.GAIN3=10V +FMC_IN.GAIN2=10V +FMC_IN.GAIN8=10V +PGEN1.REPEATS=0 +PGEN2.REPEATS=0 +PGEN1.ENABLE=ZERO +PGEN2.ENABLE=ZERO +PGEN1.TRIG=ZERO +PGEN2.TRIG=ZERO +*METADATA.LABEL_CALC1= +*METADATA.LABEL_FILTER1= +*METADATA.LABEL_CALC2= +*METADATA.LABEL_SRGATE4= +*METADATA.LABEL_FMC_IN1= +*METADATA.LABEL_TTLIN6= +*METADATA.LABEL_SRGATE1= +*METADATA.LABEL_TTLIN4= +*METADATA.LABEL_TTLIN5= +*METADATA.LABEL_TTLIN2=TTL input 2 +*METADATA.LABEL_TTLIN3= +*METADATA.LABEL_CLOCK1= +*METADATA.LABEL_TTLIN1=TTL input 1 +*METADATA.LABEL_PCOMP1= +*METADATA.LABEL_LUT8= +*METADATA.LABEL_BITS1=Soft inputs and constant bits +*METADATA.LABEL_COUNTER8= +*METADATA.LABEL_COUNTER7= +*METADATA.LABEL_COUNTER6= +*METADATA.LABEL_COUNTER5= +*METADATA.LABEL_COUNTER4= +*METADATA.LABEL_COUNTER3= +*METADATA.LABEL_COUNTER2= +*METADATA.LABEL_COUNTER1= +*METADATA.LABEL_TTLOUT3=TTL output 3 +*METADATA.LABEL_OUTENC2= +*METADATA.LABEL_OUTENC3= +*METADATA.LABEL_SYSTEM1=System control FPGA +*METADATA.LABEL_OUTENC1= +*METADATA.LABEL_PGEN2= +*METADATA.LABEL_OUTENC4= +*METADATA.LABEL_PGEN1= +*METADATA.LABEL_LUT2= +*METADATA.LABEL_LUT3= +*METADATA.LABEL_LUT1=Lookup table 1 +*METADATA.LABEL_LUT6= +*METADATA.LABEL_LUT7= +*METADATA.LABEL_LUT4= +*METADATA.LABEL_LUT5= +*METADATA.LABEL_LVDSOUT2= +*METADATA.LABEL_LVDSOUT1= +*METADATA.LABEL_SFP3_SYNC_OUT1= +*METADATA.LABEL_PULSE3= +*METADATA.LABEL_TTLOUT7= +*METADATA.LABEL_SRGATE2= +*METADATA.LABEL_LVDSIN2= +*METADATA.LABEL_LVDSIN1= +*METADATA.LABEL_SEQ1= +*METADATA.LABEL_CLOCK2= +*METADATA.LABEL_PULSE4= +*METADATA.LABEL_INENC1= +*METADATA.LABEL_INENC2= +*METADATA.LABEL_INENC3= +*METADATA.LABEL_INENC4= +*METADATA.LABEL_PULSE1= +*METADATA.LABEL_PULSE2= +*METADATA.LABEL_FILTER2= +*METADATA.LABEL_TTLOUT10= +*METADATA.LABEL_SEQ2= +*METADATA.LABEL_PCOMP2= +*METADATA.LABEL_DIV1= +*METADATA.LABEL_SRGATE3= +*METADATA.LABEL_FMC_OUT1= +*METADATA.LABEL_DIV2= +*METADATA.LABEL_SFP3_SYNC_IN1= +*METADATA.LABEL_TTLOUT8= +*METADATA.LABEL_TTLOUT9= +*METADATA.LABEL_TTLOUT6= +*METADATA.LABEL_PCAP1=Position capture control +*METADATA.LABEL_TTLOUT4=TTL output 4 +*METADATA.LABEL_TTLOUT5= +*METADATA.LABEL_TTLOUT2=TTL output 2 +*METADATA.DESIGN= +*METADATA.LABEL_TTLOUT1=TTL output 1 +SEQ1.TABLE TTLOUT2: "shutter", one pulse from the trigger to the end of the last exposure +- PULSE4 -> TTLOUT3: "detector", n pulses of width exp_time - readout_time every exp_time, + the first one shutter_delay after the trigger +COUNTER8 counts the PULSE4 pulses of the line. + +Trigger path: +- LUT8 = TTLIN3 | BITS.A: raw line trigger, BITS.A is the software trigger +- SRGATE1: one-shot arm gate, set by arm_line() (FORCE_SET), reset by the first raw + trigger edge +- LUT7 = LUT8 & SRGATE1: only the first edge after arming reaches PULSE3/PULSE4 + +PULSE block problem (PandA FPGA 4.1, seen 2026-10-28): a rising edge of ENABLE or a +parameter write often replays a pulse train without any trigger (typically the previous +train). With unchanged parameters and ENABLE kept high, 200 triggered lines in a row were +correct. So the PULSE blocks stay enabled, their parameters are only written when they +change (once per scan), followed by a wait until a replayed train is over, and per line +only the counter is reset and the one-shot gate armed. Replayed trains during a parameter +change do appear on TTLOUT2/3 (the gate only protects the trigger path). + +Only the control port (8888) is used: the fields are written by setup_layout() at connect, +the PCAP data capture is not touched. Timing resolution 8 ns (125 MHz FPGA clock). +""" + +from __future__ import annotations + +import socket +import threading +import time + +from bec_lib.logger import bec_logger +from ophyd import Device + +logger = bec_logger.logger + + +class PandaLineTriggerError(Exception): + pass + + +class PandaControl: + """Minimal client of the PandABlocks control protocol (one line per command).""" + + def __init__(self, host: str, port: int = 8888, timeout: float = 5.0): + self.host = host + self.port = port + self.timeout = timeout + self._sock = None + self._buffer = b"" + self._lock = threading.Lock() + + def open(self) -> None: + self._sock = socket.create_connection((self.host, self.port), timeout=self.timeout) + self._buffer = b"" + + def close(self) -> None: + if self._sock is not None: + self._sock.close() + self._sock = None + + def _readline(self) -> str: + while b"\n" not in self._buffer: + data = self._sock.recv(4096) + if not data: + raise PandaLineTriggerError(f"PandA {self.host} closed the connection.") + self._buffer += data + line, self._buffer = self._buffer.split(b"\n", 1) + return line.decode().strip() + + def send(self, command: str) -> str: + """Send one command, return the value of 'OK =' ('' for 'OK').""" + with self._lock: + if self._sock is None: + self.open() + self._sock.sendall(command.encode() + b"\n") + reply = self._readline() + if reply.startswith("OK ="): + return reply[4:] + if reply == "OK": + return "" + raise PandaLineTriggerError(f"PandA {self.host}: '{command}' -> '{reply}'") + + def get(self, field: str) -> str: + return self.send(f"{field}?") + + def set(self, field: str, value) -> None: + self.send(f"{field}={value}") + + +class PandaLineTrigger(Device): + """ + PandABox line trigger: on the first trigger edge after arm_line(), a shutter pulse and + n detector pulses. + + Args: + host: PandA host name. + port: control port. + input_termination: TTLIN3 termination, "50-Ohm" (pull-down for the Npaq opto + output) or "High-Z". + """ + + USER_ACCESS = [ + "setup_layout", + "arm_line", + "disarm", + "software_trigger", + "line_status", + "wait_line_done", + "panda", + ] + + TRIGGER_INPUT = "TTLIN3" + SOFT_TRIGGER_BIT = "BITS.A" + RAW_TRIGGER_LUT = "LUT8" + ARM_GATE = "SRGATE1" + GATED_TRIGGER_LUT = "LUT7" + SHUTTER_PULSE = "PULSE3" + DETECTOR_PULSE = "PULSE4" + PULSE_COUNTER = "COUNTER8" + SHUTTER_OUTPUT = "TTLOUT2" + DETECTOR_OUTPUT = "TTLOUT3" + + def __init__( + self, + prefix="", + *, + name, + host: str, + port: int = 8888, + input_termination: str = "50-Ohm", + device_manager=None, + **kwargs, + ): + self.panda = PandaControl(host, port) + self.input_termination = input_termination + self._pulse_params = None # parameters written to the PULSE blocks + self._dropped_at_arm = 0 # DROPPED is only zeroed by ENABLE, which is never toggled + self.device_manager = device_manager + super().__init__(prefix, name=name, **kwargs) + + def wait_for_connection(self, all_signals=False, timeout: float = 5.0) -> None: + self.panda.open() + idn = self.panda.send("*IDN?") + logger.info(f"{self.name}: connected to {self.panda.host}: {idn}") + self.setup_layout() + + def destroy(self): + self.panda.close() + super().destroy() + + def stop(self, *, success=False): + try: + self.disarm() + except Exception as exc: # stop must not raise + logger.warning(f"{self.name}: disarm on stop failed: {exc}") + super().stop(success=success) + + def _layout(self) -> list[tuple[str, str]]: + raw, gate, gated = self.RAW_TRIGGER_LUT, self.ARM_GATE, self.GATED_TRIGGER_LUT + sp, dp, cnt = self.SHUTTER_PULSE, self.DETECTOR_PULSE, self.PULSE_COUNTER + fields = [ + (f"{self.TRIGGER_INPUT}.TERM", self.input_termination), + (self.SOFT_TRIGGER_BIT, "0"), + (f"{raw}.INPA", f"{self.TRIGGER_INPUT}.VAL"), + (f"{raw}.INPB", "BITS.OUTA"), + (f"{raw}.FUNC", "A|B"), + (f"{gate}.ENABLE", "ONE"), + (f"{gate}.SET", "ZERO"), + (f"{gate}.RST", f"{raw}.OUT"), + (f"{gate}.RST_EDGE", "Rising"), + (f"{gate}.WHEN_DISABLED", "Set output low"), + (f"{gate}.FORCE_RST", ""), + (f"{gated}.INPA", f"{raw}.OUT"), + (f"{gated}.INPB", f"{gate}.OUT"), + (f"{gated}.FUNC", "A&B"), + ] + for pulse in (sp, dp): + fields += [ + (f"{pulse}.ENABLE", "ONE"), + (f"{pulse}.TRIG", f"{gated}.OUT"), + (f"{pulse}.TRIG_EDGE", "Rising"), + (f"{pulse}.DELAY.UNITS", "s"), + (f"{pulse}.WIDTH.UNITS", "s"), + (f"{pulse}.STEP.UNITS", "s"), + ] + fields += [ + (f"{cnt}.ENABLE", "ZERO"), + (f"{cnt}.TRIG", f"{dp}.OUT"), + (f"{cnt}.TRIG_EDGE", "Rising"), + (f"{cnt}.DIR", "ZERO"), + (f"{cnt}.START", "0"), + (f"{cnt}.STEP", "1"), + (f"{self.SHUTTER_OUTPUT}.VAL", f"{sp}.OUT"), + (f"{self.DETECTOR_OUTPUT}.VAL", f"{dp}.OUT"), + ] + return fields + + def setup_layout(self) -> None: + """Write the wiring of the line trigger blocks (other blocks are not touched).""" + for field, value in self._layout(): + self.panda.set(field, value) + self._pulse_params = None + + @staticmethod + def _train_duration(params: dict | None) -> float: + if params is None: + return 0.1 + return params["delay"] + params["n_pulses"] * params["step"] + + def _set_pulse_params(self, params: dict) -> None: + """Write the PULSE parameters (only if changed) and wait until a replayed train is + over: a stable counter and both PULSE outputs low.""" + if params == self._pulse_params: + return + sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE + self.panda.set(f"{dp}.DELAY", f"{params['delay']:.9f}") + self.panda.set(f"{dp}.WIDTH", f"{params['width']:.9f}") + self.panda.set(f"{dp}.STEP", f"{params['step']:.9f}") + self.panda.set(f"{dp}.PULSES", params["n_pulses"]) + self.panda.set(f"{sp}.DELAY", "0") + self.panda.set(f"{sp}.WIDTH", f"{params['shutter_width']:.9f}") + self.panda.set(f"{sp}.STEP", "0") + self.panda.set(f"{sp}.PULSES", 1) + timeout = 2 * max(self._train_duration(self._pulse_params), self._train_duration(params)) + self._pulse_params = None + deadline = time.time() + timeout + 1.0 + last = None + while True: + time.sleep(0.02) + state = ( + self.panda.get(f"{self.PULSE_COUNTER}.OUT"), + self.panda.get(f"{sp}.OUT"), + self.panda.get(f"{dp}.OUT"), + ) + if state == last and state[1:] == ("0", "0"): + break + if time.time() > deadline: + raise PandaLineTriggerError( + f"{self.name}: PULSE blocks still active {timeout + 1:.1f} s after a" + " parameter change." + ) + last = state + self._pulse_params = params + + def arm_line( + self, n_pulses: int, exp_time: float, readout_time: float = 0.0, shutter_delay: float = 0.0 + ) -> None: + """ + Arm one line: on the next trigger edge, n_pulses detector pulses of width + exp_time - readout_time every exp_time, the first one shutter_delay after the edge. + The shutter pulse lasts from the edge to the end of the last exposure. Further + edges are ignored until the next arm_line(). + """ + n_pulses = int(n_pulses) + width = exp_time - readout_time + if n_pulses < 1: + raise PandaLineTriggerError(f"Number of pulses must be at least 1, got {n_pulses}.") + if width <= 0: + raise PandaLineTriggerError( + f"readout_time {readout_time} s must be shorter than exp_time {exp_time} s." + ) + if shutter_delay < 0: + raise PandaLineTriggerError(f"shutter_delay must be >= 0, got {shutter_delay}.") + self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") + if self.panda.get(f"{self.RAW_TRIGGER_LUT}.OUT") != "0": + raise PandaLineTriggerError( + f"{self.name}: line trigger input {self.TRIGGER_INPUT} is high, cannot arm." + ) + self._set_pulse_params( + { + "n_pulses": n_pulses, + "width": width, + "step": exp_time, + "delay": shutter_delay, + "shutter_width": shutter_delay + (n_pulses - 1) * exp_time + width, + } + ) + # rising edge of ENABLE resets the counter to START + cnt = self.PULSE_COUNTER + self.panda.set(f"{cnt}.ENABLE", "ZERO") + self.panda.set(f"{cnt}.ENABLE", "ONE") + self._dropped_at_arm = int(self.panda.get(f"{self.DETECTOR_PULSE}.DROPPED")) + self.panda.set(f"{self.ARM_GATE}.FORCE_SET", "") + + def disarm(self) -> None: + """Ignore line trigger edges until the next arm_line() (a running line continues).""" + self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") + self.panda.set(self.SOFT_TRIGGER_BIT, "0") + + def software_trigger(self) -> None: + """Start the armed line without an input edge.""" + self.panda.set(self.SOFT_TRIGGER_BIT, "1") + self.panda.set(self.SOFT_TRIGGER_BIT, "0") + + def line_status(self) -> dict: + dp = self.DETECTOR_PULSE + return { + "armed": self.panda.get(f"{self.ARM_GATE}.OUT") == "1", + "pulses_sent": int(self.panda.get(f"{self.PULSE_COUNTER}.OUT")), + "queued": int(self.panda.get(f"{dp}.QUEUED")), + "dropped": int(self.panda.get(f"{dp}.DROPPED")) - self._dropped_at_arm, + } + + def wait_line_done(self, n_pulses: int, timeout: float, poll: float = 0.02) -> dict: + """Wait until the line was triggered and n_pulses detector pulses were sent.""" + start = time.time() + while True: + status = self.line_status() + if status["dropped"]: + raise PandaLineTriggerError( + f"{self.name}: {status['dropped']} trigger(s) dropped by {self.DETECTOR_PULSE}." + ) + if not status["armed"] and status["pulses_sent"] >= n_pulses and not status["queued"]: + break + if time.time() - start > timeout: + raise PandaLineTriggerError( + f"{self.name}: line not finished within {timeout} s: {status}. Was the line" + f" trigger received on {self.TRIGGER_INPUT}?" + ) + time.sleep(poll) + if status["pulses_sent"] != n_pulses: + raise PandaLineTriggerError( + f"{self.name}: sent {status['pulses_sent']} pulses instead of {n_pulses}." + ) + return status diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py new file mode 100644 index 00000000..ee592398 --- /dev/null +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -0,0 +1,166 @@ +import pytest + +from csaxs_bec.devices.panda_box.panda_line_trigger import ( + PandaControl, + PandaLineTrigger, + PandaLineTriggerError, +) + + +class FakePanda(PandaControl): + """In-memory PandA: fields, the one-shot arm gate, the counter and a PULSE4 train.""" + + def __init__(self): + super().__init__("fake-panda") + self.fields = {} + self.writes = [] + + def open(self): + pass + + def close(self): + pass + + def send(self, command: str) -> str: + if command == "*IDN?": + return "PandA SW: fake" + if command.endswith("?"): + return self.fields.get(command[:-1], "0") + field, value = command.split("=", 1) + self.writes.append(command) + if field == "SRGATE1.FORCE_SET": + self.fields["SRGATE1.OUT"] = "1" + elif field == "SRGATE1.FORCE_RST": + self.fields["SRGATE1.OUT"] = "0" + elif field == "COUNTER8.ENABLE" and value == "ONE": + self.fields["COUNTER8.OUT"] = "0" + elif field == "BITS.A" and value == "1" and self.fields.get("SRGATE1.OUT") == "1": + # first edge after arming: gate closes, the whole train is sent at once + self.fields["SRGATE1.OUT"] = "0" + self.fields["COUNTER8.OUT"] = self.fields.get("PULSE4.PULSES", "1") + else: + self.fields[field] = value + return "" + + +@pytest.fixture +def trigger(): + device = PandaLineTrigger(name="pandatrig", host="fake-panda") + device.panda = FakePanda() + device.wait_for_connection() + return device + + +def test_setup_layout_wires_the_blocks(trigger): + fields = trigger.panda.fields + assert fields["PULSE3.TRIG"] == "LUT7.OUT" + assert fields["PULSE4.TRIG"] == "LUT7.OUT" + assert fields["LUT7.FUNC"] == "A&B" + assert fields["LUT8.INPA"] == "TTLIN3.VAL" + assert fields["SRGATE1.RST"] == "LUT8.OUT" + assert fields["COUNTER8.TRIG"] == "PULSE4.OUT" + assert fields["TTLOUT2.VAL"] == "PULSE3.OUT" + assert fields["TTLOUT3.VAL"] == "PULSE4.OUT" + assert fields["TTLIN3.TERM"] == "50-Ohm" + assert fields["PULSE3.ENABLE"] == fields["PULSE4.ENABLE"] == "ONE" + + +def test_arm_line_sets_timing(trigger): + trigger.arm_line(10, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) + fields = trigger.panda.fields + assert float(fields["PULSE4.DELAY"]) == pytest.approx(0.005) + assert float(fields["PULSE4.WIDTH"]) == pytest.approx(0.008) + assert float(fields["PULSE4.STEP"]) == pytest.approx(0.01) + assert fields["PULSE4.PULSES"] == "10" + # shutter: from the trigger to the end of the last exposure + assert float(fields["PULSE3.WIDTH"]) == pytest.approx(0.005 + 9 * 0.01 + 0.008) + assert trigger.line_status()["armed"] + + +def test_pulse_parameters_are_written_only_on_change(trigger): + trigger.arm_line(10, exp_time=0.01) + trigger.panda.writes.clear() + trigger.arm_line(10, exp_time=0.01) + assert not [w for w in trigger.panda.writes if w.startswith(("PULSE3.", "PULSE4."))] + trigger.arm_line(11, exp_time=0.01) + assert "PULSE4.PULSES=11" in trigger.panda.writes + + +def test_never_toggles_pulse_enable_after_setup(trigger): + trigger.panda.writes.clear() + trigger.arm_line(5, exp_time=0.01) + trigger.software_trigger() + trigger.wait_line_done(5, timeout=1) + trigger.disarm() + trigger.stop() + assert not [w for w in trigger.panda.writes if ".ENABLE" in w and "PULSE" in w] + + +def test_line_runs_once_per_arm(trigger): + trigger.arm_line(5, exp_time=0.01) + trigger.software_trigger() + assert trigger.wait_line_done(5, timeout=1)["pulses_sent"] == 5 + assert not trigger.line_status()["armed"] + + +def test_wait_line_done_times_out_without_trigger(trigger): + trigger.arm_line(5, exp_time=0.01) + with pytest.raises(PandaLineTriggerError, match="not finished"): + trigger.wait_line_done(5, timeout=0.1, poll=0.01) + + +def test_wait_line_done_reports_wrong_count(trigger): + trigger.arm_line(5, exp_time=0.01) + trigger.software_trigger() + trigger.panda.fields["COUNTER8.OUT"] = "6" + with pytest.raises(PandaLineTriggerError, match="6 pulses instead of 5"): + trigger.wait_line_done(5, timeout=0.1, poll=0.01) + + +def test_wait_line_done_reports_dropped(trigger): + trigger.arm_line(5, exp_time=0.01) + trigger.panda.fields["PULSE4.DROPPED"] = "1" + with pytest.raises(PandaLineTriggerError, match="dropped"): + trigger.wait_line_done(5, timeout=0.1, poll=0.01) + + +def test_dropped_counts_only_since_arm(trigger): + trigger.panda.fields["PULSE4.DROPPED"] = "3" + trigger.arm_line(5, exp_time=0.01) + assert trigger.line_status()["dropped"] == 0 + + +def test_arm_refuses_high_trigger_input(trigger): + trigger.panda.fields["LUT8.OUT"] = "1" + with pytest.raises(PandaLineTriggerError, match="is high"): + trigger.arm_line(5, exp_time=0.01) + + +@pytest.mark.parametrize( + "kwargs, match", + [ + ({"n_pulses": 0, "exp_time": 0.01}, "at least 1"), + ({"n_pulses": 5, "exp_time": 0.01, "readout_time": 0.01}, "shorter than"), + ({"n_pulses": 5, "exp_time": 0.01, "shutter_delay": -0.001}, "shutter_delay"), + ], +) +def test_arm_line_checks_arguments(trigger, kwargs, match): + with pytest.raises(PandaLineTriggerError, match=match): + trigger.arm_line(**kwargs) + + +def test_disarm_closes_the_gate(trigger): + trigger.arm_line(5, exp_time=0.01) + trigger.disarm() + trigger.software_trigger() + assert trigger.line_status() == {"armed": False, "pulses_sent": 0, "queued": 0, "dropped": 0} + + +def test_control_reply_parsing(): + control = PandaControl("fake") + control._sock = type("S", (), {"sendall": lambda self, data: None})() + control._buffer = b"OK =1.5\nOK\nERR No such field\n" + assert control.send("PULSE4.WIDTH?") == "1.5" + assert control.send("PULSE4.WIDTH=1") == "" + with pytest.raises(PandaLineTriggerError, match="No such field"): + control.send("PULSE4.FOO?") -- 2.54.0 From b518e657cf4ac2cd5d32b8b5846255c0ac12e6ed Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 16:22:55 +0100 Subject: [PATCH 08/52] feat(panda): line trigger stays armed like the DDG, every edge starts a line configure_line() once, arm() once, then every line trigger edge (TTLIN3 or software) starts a line; a scan no longer re-arms per line. LUT7 accepts an edge only while armed and while no line is running (PULSE3/PULSE4 low), so edges during a line are ignored like on the DDG and never reach the PULSE blocks (a dropped trigger puts the FPGA 4.1 PULSE block into the replay state). COUNTER6/7 count raw edges and accepted lines, wait_lines_done() checks lines, gates and ignored edges. arm(one_shot=True) keeps the single-line mode. Design SASTT_line_trigger updated on omny-panda. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 36 ++- .../panda_box/layouts/sastt_line_trigger.ini | 44 ++-- .../devices/panda_box/panda_line_trigger.py | 245 +++++++++++------- .../tests_devices/test_panda_line_trigger.py | 135 ++++++---- 4 files changed, 281 insertions(+), 179 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 5ef3ab79..0a5b6f50 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -99,28 +99,36 @@ Commissioning of the scan: Config `device_configs/sastt_aerotech_panda.yaml` = `sastt_aerotech.yaml` + device `pandatrig` (`devices/panda_box/panda_line_trigger.py`, PandA `omny-panda.psi.ch`). The PandA -replaces DDG1/DDG2: on the line trigger edge it sends a shutter pulse and n detector pulses, -8 ns resolution. +replaces DDG1/DDG2 and behaves like them: it stays armed, and every line trigger edge sends a +shutter pulse and n detector gates (8 ns resolution). A scan configures and arms once; the +lines run back to back without re-arming. | PandA | Use | Wiring | |-------|-----|--------| | TTLIN3 (50 Ω) | line trigger | Npaq J7 opto output: J7-25 (+5 V) -> J7-6, J7-7 -> TTLIN3, J7-24 -> TTLIN3 ground | -| TTLOUT2 | "shutter": trigger edge to end of the last exposure | scope | -| TTLOUT3 | "detector": n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope, later Npaq J12 pin 1 (+) / pin 2 (-) for position sampling | +| TTLOUT2 | "shutter": trigger edge to end of the last gate | scope | +| TTLOUT3 | "detector" gates (high during exposure): n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope, later Npaq J12 pin 1 (+) / pin 2 (-) for position sampling (rising edge = exposure start) | -Blocks: LUT8 = TTLIN3 | BITS.A (software trigger), SRGATE1 one-shot arm gate (set by -`arm_line`, reset by the first edge), LUT7 = LUT8 & SRGATE1 -> PULSE3 (shutter), PULSE4 -(detector), COUNTER8 counts the PULSE4 pulses. Design `SASTT_line_trigger` on the PandA web +Blocks: LUT8 = TTLIN3 | BITS.A (software trigger); SRGATE1 arm gate; LUT7 = rising edge of +LUT8 & SRGATE1 & no line running (PULSE3/PULSE4 low) -> PULSE3 (shutter), PULSE4 (gates). +An edge during a running line is ignored, like on the DDG. COUNTER6 counts raw edges, +COUNTER7 accepted lines, COUNTER8 gates. Design `SASTT_line_trigger` on the PandA web interface, copy in `devices/panda_box/layouts/sastt_line_trigger.ini`; the device also writes the wiring at connect. ```python -dev.pandatrig.arm_line(n_pulses=100, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) -dev.pandatrig.software_trigger() # or the PSO edge on TTLIN3 -dev.pandatrig.wait_line_done(100, timeout=5) +dev.pandatrig.configure_line(n_pulses=100, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) +dev.pandatrig.arm() # arm(one_shot=True): only the next edge +dev.pandatrig.software_trigger() # or the PSO edges on TTLIN3, one per line +dev.pandatrig.wait_lines_done(n_lines=1, timeout=5) +dev.pandatrig.line_status() # triggers, lines, pulses, dropped since arm() ``` -PandA FPGA 4.1 PULSE block: an ENABLE edge or a parameter write often replays the previous -pulse train without a trigger. The device keeps the PULSE blocks enabled and writes their -parameters only when they change (once per scan), then waits until such a train is over; -during that wait pulses can appear on TTLOUT2/3. +`wait_lines_done` fails on timeout, on a wrong number of lines or gates and on ignored +edges. + +PandA FPGA 4.1 PULSE block: an ENABLE edge, a parameter write or a dropped trigger often +makes it replay the previous pulse train without a trigger. The device keeps the PULSE +blocks enabled, writes their parameters only when they change (once per scan) and then waits +until such a train is over (pulses can appear on TTLOUT2/3 meanwhile), and the trigger path +filters out edges during a line, so no trigger is ever dropped by a PULSE block. diff --git a/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini b/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini index af2554ca..6e4bb072 100644 --- a/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini +++ b/csaxs_bec/devices/panda_box/layouts/sastt_line_trigger.ini @@ -522,7 +522,7 @@ SRGATE1.ENABLE=ONE SRGATE2.ENABLE=ZERO SRGATE3.ENABLE=ZERO SRGATE4.ENABLE=ZERO -SRGATE1.RST=LUT8.OUT +SRGATE1.RST=ZERO SRGATE2.RST=ZERO SRGATE3.RST=ZERO SRGATE4.RST=ZERO @@ -548,7 +548,7 @@ LUT3.INPC=ZERO LUT4.INPC=ZERO LUT5.INPC=ZERO LUT6.INPC=ZERO -LUT7.INPC=ZERO +LUT7.INPC=PULSE3.OUT LUT8.INPC=ZERO LUT1.INPA=ZERO LUT2.INPA=ZERO @@ -564,7 +564,7 @@ LUT3.INPD=ZERO LUT4.INPD=ZERO LUT5.INPD=ZERO LUT6.INPD=ZERO -LUT7.INPD=ZERO +LUT7.INPD=PULSE4.OUT LUT8.INPD=ZERO LUT1.INPE=ZERO LUT2.INPE=ZERO @@ -580,7 +580,7 @@ LUT3.FUNC=0x00000000 LUT4.FUNC=0x00000000 LUT5.FUNC=0x00000000 LUT6.FUNC=0x00000000 -LUT7.FUNC=A&B +LUT7.FUNC=A&B&~C&~D LUT8.FUNC=A|B LUT1.TYPEA=Input-Level LUT2.TYPEA=Input-Level @@ -588,7 +588,7 @@ LUT3.TYPEA=Input-Level LUT4.TYPEA=Input-Level LUT5.TYPEA=Input-Level LUT6.TYPEA=Input-Level -LUT7.TYPEA=Input-Level +LUT7.TYPEA=Pulse-On-Rising-Edge LUT8.TYPEA=Input-Level LUT1.TYPEC=Input-Level LUT2.TYPEC=Input-Level @@ -682,15 +682,15 @@ COUNTER3.ENABLE=ZERO COUNTER4.ENABLE=ZERO COUNTER5.ENABLE=ZERO COUNTER6.ENABLE=ZERO -COUNTER7.ENABLE=ONE -COUNTER8.ENABLE=ONE +COUNTER7.ENABLE=ZERO +COUNTER8.ENABLE=ZERO COUNTER1.TRIG=ZERO COUNTER2.TRIG=ZERO COUNTER3.TRIG=ZERO COUNTER4.TRIG=ZERO COUNTER5.TRIG=ZERO -COUNTER6.TRIG=ZERO -COUNTER7.TRIG=PULSE4.OUT +COUNTER6.TRIG=LUT8.OUT +COUNTER7.TRIG=LUT7.OUT COUNTER8.TRIG=PULSE4.OUT COUNTER1.TRIG_EDGE=Falling COUNTER2.TRIG_EDGE=Rising @@ -735,7 +735,7 @@ COUNTER8.STEP=1 PULSE1.PULSES=0 PULSE2.PULSES=5 PULSE3.PULSES=1 -PULSE4.PULSES=10 +PULSE4.PULSES=5 PULSE1.ENABLE=ONE PULSE2.ENABLE=ZERO PULSE3.ENABLE=ONE @@ -743,7 +743,7 @@ PULSE4.ENABLE=ONE PULSE1.DELAY=0 PULSE2.DELAY=0 PULSE3.DELAY=0 -PULSE4.DELAY=0.001 +PULSE4.DELAY=0 PULSE1.TRIG=ZERO PULSE2.TRIG=ZERO PULSE3.TRIG=LUT7.OUT @@ -754,12 +754,12 @@ PULSE3.TRIG_EDGE=Rising PULSE4.TRIG_EDGE=Rising PULSE1.WIDTH=0.001 PULSE2.WIDTH=0.002 -PULSE3.WIDTH=0.0108 -PULSE4.WIDTH=0.0008 +PULSE3.WIDTH=0.01 +PULSE4.WIDTH=0.002 PULSE1.STEP=0 PULSE2.STEP=0.0025 PULSE3.STEP=0 -PULSE4.STEP=0.001 +PULSE4.STEP=0.002 FILTER1.INP=ZERO FILTER2.INP=ZERO FILTER1.MODE=average @@ -866,14 +866,16 @@ PGEN2.TABLE TTLOUT2: "shutter", one pulse from the trigger to the end of the last exposure -- PULSE4 -> TTLOUT3: "detector", n pulses of width exp_time - readout_time every exp_time, - the first one shutter_delay after the trigger -COUNTER8 counts the PULSE4 pulses of the line. +- PULSE4 -> TTLOUT3: "detector" gates, n pulses of width exp_time - readout_time every + exp_time, the first one shutter_delay after the trigger +so a scan arms once and the lines run back to back. Trigger path: - LUT8 = TTLIN3 | BITS.A: raw line trigger, BITS.A is the software trigger -- SRGATE1: one-shot arm gate, set by arm_line() (FORCE_SET), reset by the first raw - trigger edge -- LUT7 = LUT8 & SRGATE1: only the first edge after arming reaches PULSE3/PULSE4 +- SRGATE1: arm gate, set by arm(); one_shot=True resets it with the first accepted edge +- LUT7 = rising edge of LUT8 & SRGATE1 & ~PULSE3.OUT & ~PULSE4.OUT: an edge is accepted + only while armed and while no line is running; edges during a line are ignored (the + DDG ignores them too) and never reach the PULSE blocks +Counters: COUNTER6 raw edges, COUNTER7 accepted lines, COUNTER8 detector pulses. -PULSE block problem (PandA FPGA 4.1, seen 2026-10-28): a rising edge of ENABLE or a -parameter write often replays a pulse train without any trigger (typically the previous -train). With unchanged parameters and ENABLE kept high, 200 triggered lines in a row were -correct. So the PULSE blocks stay enabled, their parameters are only written when they -change (once per scan), followed by a wait until a replayed train is over, and per line -only the counter is reset and the one-shot gate armed. Replayed trains during a parameter -change do appear on TTLOUT2/3 (the gate only protects the trigger path). +PULSE block problem (PandA FPGA 4.1, seen 2026-10-28): a rising edge of ENABLE, a parameter +write or a dropped trigger often makes the block replay the previous pulse train without +any trigger. With unchanged parameters and ENABLE kept high, hundreds of triggered lines in +a row were correct. So the PULSE blocks stay enabled, their parameters are only written +when they change (once per scan), followed by a wait until a replayed train is over, and +edges during a line are filtered out before the PULSE blocks. Replayed trains during a +parameter change do appear on TTLOUT2/3. Only the control port (8888) is used: the fields are written by setup_layout() at connect, the PCAP data capture is not touched. Timing resolution 8 ns (125 MHz FPGA clock). +Design SASTT_line_trigger on the PandA web interface, copy in layouts/sastt_line_trigger.ini. """ from __future__ import annotations @@ -92,8 +96,8 @@ class PandaControl: class PandaLineTrigger(Device): """ - PandABox line trigger: on the first trigger edge after arm_line(), a shutter pulse and - n detector pulses. + PandABox line trigger: while armed, every line trigger edge gives a shutter pulse and + n detector gates. Args: host: PandA host name. @@ -104,11 +108,12 @@ class PandaLineTrigger(Device): USER_ACCESS = [ "setup_layout", - "arm_line", + "configure_line", + "arm", "disarm", "software_trigger", "line_status", - "wait_line_done", + "wait_lines_done", "panda", ] @@ -116,9 +121,11 @@ class PandaLineTrigger(Device): SOFT_TRIGGER_BIT = "BITS.A" RAW_TRIGGER_LUT = "LUT8" ARM_GATE = "SRGATE1" - GATED_TRIGGER_LUT = "LUT7" + ACCEPTED_TRIGGER_LUT = "LUT7" SHUTTER_PULSE = "PULSE3" DETECTOR_PULSE = "PULSE4" + TRIGGER_COUNTER = "COUNTER6" + LINE_COUNTER = "COUNTER7" PULSE_COUNTER = "COUNTER8" SHUTTER_OUTPUT = "TTLOUT2" DETECTOR_OUTPUT = "TTLOUT3" @@ -159,8 +166,8 @@ class PandaLineTrigger(Device): super().stop(success=success) def _layout(self) -> list[tuple[str, str]]: - raw, gate, gated = self.RAW_TRIGGER_LUT, self.ARM_GATE, self.GATED_TRIGGER_LUT - sp, dp, cnt = self.SHUTTER_PULSE, self.DETECTOR_PULSE, self.PULSE_COUNTER + raw, gate, accepted = self.RAW_TRIGGER_LUT, self.ARM_GATE, self.ACCEPTED_TRIGGER_LUT + sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE fields = [ (f"{self.TRIGGER_INPUT}.TERM", self.input_termination), (self.SOFT_TRIGGER_BIT, "0"), @@ -169,30 +176,43 @@ class PandaLineTrigger(Device): (f"{raw}.FUNC", "A|B"), (f"{gate}.ENABLE", "ONE"), (f"{gate}.SET", "ZERO"), - (f"{gate}.RST", f"{raw}.OUT"), + (f"{gate}.RST", "ZERO"), (f"{gate}.RST_EDGE", "Rising"), (f"{gate}.WHEN_DISABLED", "Set output low"), (f"{gate}.FORCE_RST", ""), - (f"{gated}.INPA", f"{raw}.OUT"), - (f"{gated}.INPB", f"{gate}.OUT"), - (f"{gated}.FUNC", "A&B"), + (f"{accepted}.INPA", f"{raw}.OUT"), + (f"{accepted}.TYPEA", "Pulse-On-Rising-Edge"), + (f"{accepted}.INPB", f"{gate}.OUT"), + (f"{accepted}.TYPEB", "Input-Level"), + (f"{accepted}.INPC", f"{sp}.OUT"), + (f"{accepted}.TYPEC", "Input-Level"), + (f"{accepted}.INPD", f"{dp}.OUT"), + (f"{accepted}.TYPED", "Input-Level"), + (f"{accepted}.FUNC", "A&B&~C&~D"), ] for pulse in (sp, dp): fields += [ (f"{pulse}.ENABLE", "ONE"), - (f"{pulse}.TRIG", f"{gated}.OUT"), + (f"{pulse}.TRIG", f"{accepted}.OUT"), (f"{pulse}.TRIG_EDGE", "Rising"), (f"{pulse}.DELAY.UNITS", "s"), (f"{pulse}.WIDTH.UNITS", "s"), (f"{pulse}.STEP.UNITS", "s"), ] + for counter, source in ( + (self.TRIGGER_COUNTER, f"{raw}.OUT"), + (self.LINE_COUNTER, f"{accepted}.OUT"), + (self.PULSE_COUNTER, f"{dp}.OUT"), + ): + fields += [ + (f"{counter}.ENABLE", "ZERO"), + (f"{counter}.TRIG", source), + (f"{counter}.TRIG_EDGE", "Rising"), + (f"{counter}.DIR", "ZERO"), + (f"{counter}.START", "0"), + (f"{counter}.STEP", "1"), + ] fields += [ - (f"{cnt}.ENABLE", "ZERO"), - (f"{cnt}.TRIG", f"{dp}.OUT"), - (f"{cnt}.TRIG_EDGE", "Rising"), - (f"{cnt}.DIR", "ZERO"), - (f"{cnt}.START", "0"), - (f"{cnt}.STEP", "1"), (f"{self.SHUTTER_OUTPUT}.VAL", f"{sp}.OUT"), (f"{self.DETECTOR_OUTPUT}.VAL", f"{dp}.OUT"), ] @@ -210,49 +230,14 @@ class PandaLineTrigger(Device): return 0.1 return params["delay"] + params["n_pulses"] * params["step"] - def _set_pulse_params(self, params: dict) -> None: - """Write the PULSE parameters (only if changed) and wait until a replayed train is - over: a stable counter and both PULSE outputs low.""" - if params == self._pulse_params: - return - sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE - self.panda.set(f"{dp}.DELAY", f"{params['delay']:.9f}") - self.panda.set(f"{dp}.WIDTH", f"{params['width']:.9f}") - self.panda.set(f"{dp}.STEP", f"{params['step']:.9f}") - self.panda.set(f"{dp}.PULSES", params["n_pulses"]) - self.panda.set(f"{sp}.DELAY", "0") - self.panda.set(f"{sp}.WIDTH", f"{params['shutter_width']:.9f}") - self.panda.set(f"{sp}.STEP", "0") - self.panda.set(f"{sp}.PULSES", 1) - timeout = 2 * max(self._train_duration(self._pulse_params), self._train_duration(params)) - self._pulse_params = None - deadline = time.time() + timeout + 1.0 - last = None - while True: - time.sleep(0.02) - state = ( - self.panda.get(f"{self.PULSE_COUNTER}.OUT"), - self.panda.get(f"{sp}.OUT"), - self.panda.get(f"{dp}.OUT"), - ) - if state == last and state[1:] == ("0", "0"): - break - if time.time() > deadline: - raise PandaLineTriggerError( - f"{self.name}: PULSE blocks still active {timeout + 1:.1f} s after a" - " parameter change." - ) - last = state - self._pulse_params = params - - def arm_line( + def configure_line( self, n_pulses: int, exp_time: float, readout_time: float = 0.0, shutter_delay: float = 0.0 ) -> None: """ - Arm one line: on the next trigger edge, n_pulses detector pulses of width - exp_time - readout_time every exp_time, the first one shutter_delay after the edge. - The shutter pulse lasts from the edge to the end of the last exposure. Further - edges are ignored until the next arm_line(). + Pulses of one line: n_pulses detector gates of width exp_time - readout_time every + exp_time, the first one shutter_delay after the trigger edge. The shutter pulse + lasts from the edge to the end of the last gate. Written to the PandA only when the + values change; must not be called while lines are running. """ n_pulses = int(n_pulses) width = exp_time - readout_time @@ -264,48 +249,102 @@ class PandaLineTrigger(Device): ) if shutter_delay < 0: raise PandaLineTriggerError(f"shutter_delay must be >= 0, got {shutter_delay}.") + params = { + "n_pulses": n_pulses, + "width": width, + "step": exp_time, + "delay": shutter_delay, + "shutter_width": shutter_delay + (n_pulses - 1) * exp_time + width, + } + if params == self._pulse_params: + return + sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") + self.panda.set(f"{dp}.DELAY", f"{params['delay']:.9f}") + self.panda.set(f"{dp}.WIDTH", f"{params['width']:.9f}") + self.panda.set(f"{dp}.STEP", f"{params['step']:.9f}") + self.panda.set(f"{dp}.PULSES", params["n_pulses"]) + self.panda.set(f"{sp}.DELAY", "0") + self.panda.set(f"{sp}.WIDTH", f"{params['shutter_width']:.9f}") + self.panda.set(f"{sp}.STEP", "0") + self.panda.set(f"{sp}.PULSES", 1) + self._pulse_params = None + self._wait_pulses_idle( + 2 * max(self._train_duration(self._pulse_params), self._train_duration(params)) + 1 + ) + self._pulse_params = params + + def _wait_pulses_idle(self, timeout: float) -> None: + """Wait until a replayed train is over: stable counter and both PULSE outputs low.""" + sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE + deadline = time.time() + timeout + last = None + while True: + time.sleep(0.02) + state = ( + self.panda.get(f"{self.PULSE_COUNTER}.OUT"), + self.panda.get(f"{sp}.OUT"), + self.panda.get(f"{dp}.OUT"), + ) + if state == last and state[1:] == ("0", "0"): + return + if time.time() > deadline: + raise PandaLineTriggerError( + f"{self.name}: PULSE blocks still active {timeout:.1f} s after a parameter" + " change." + ) + last = state + + def arm(self, one_shot: bool = False) -> None: + """ + Reset the counters and accept line trigger edges: every edge while no line is + running starts a line, until disarm(). one_shot=True accepts only the next edge. + """ + if self._pulse_params is None: + raise PandaLineTriggerError(f"{self.name}: configure_line() before arm().") + gate = self.ARM_GATE + self.panda.set(f"{gate}.FORCE_RST", "") if self.panda.get(f"{self.RAW_TRIGGER_LUT}.OUT") != "0": raise PandaLineTriggerError( f"{self.name}: line trigger input {self.TRIGGER_INPUT} is high, cannot arm." ) - self._set_pulse_params( - { - "n_pulses": n_pulses, - "width": width, - "step": exp_time, - "delay": shutter_delay, - "shutter_width": shutter_delay + (n_pulses - 1) * exp_time + width, - } - ) - # rising edge of ENABLE resets the counter to START - cnt = self.PULSE_COUNTER - self.panda.set(f"{cnt}.ENABLE", "ZERO") - self.panda.set(f"{cnt}.ENABLE", "ONE") + self.panda.set(f"{gate}.RST", f"{self.ACCEPTED_TRIGGER_LUT}.OUT" if one_shot else "ZERO") + # rising edge of ENABLE resets a counter to START + for counter in (self.TRIGGER_COUNTER, self.LINE_COUNTER, self.PULSE_COUNTER): + self.panda.set(f"{counter}.ENABLE", "ZERO") + self.panda.set(f"{counter}.ENABLE", "ONE") self._dropped_at_arm = int(self.panda.get(f"{self.DETECTOR_PULSE}.DROPPED")) - self.panda.set(f"{self.ARM_GATE}.FORCE_SET", "") + self.panda.set(f"{gate}.FORCE_SET", "") def disarm(self) -> None: - """Ignore line trigger edges until the next arm_line() (a running line continues).""" + """Ignore line trigger edges until the next arm() (a running line continues).""" self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") self.panda.set(self.SOFT_TRIGGER_BIT, "0") def software_trigger(self) -> None: - """Start the armed line without an input edge.""" + """Line trigger edge from software (accepted like a TTLIN3 edge).""" self.panda.set(self.SOFT_TRIGGER_BIT, "1") self.panda.set(self.SOFT_TRIGGER_BIT, "0") def line_status(self) -> dict: - dp = self.DETECTOR_PULSE + """Counts since arm(): trigger edges, accepted lines, detector pulses.""" + sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE return { "armed": self.panda.get(f"{self.ARM_GATE}.OUT") == "1", - "pulses_sent": int(self.panda.get(f"{self.PULSE_COUNTER}.OUT")), - "queued": int(self.panda.get(f"{dp}.QUEUED")), + "busy": self.panda.get(f"{sp}.OUT") == "1" or self.panda.get(f"{dp}.OUT") == "1", + "triggers": int(self.panda.get(f"{self.TRIGGER_COUNTER}.OUT")), + "lines": int(self.panda.get(f"{self.LINE_COUNTER}.OUT")), + "pulses": int(self.panda.get(f"{self.PULSE_COUNTER}.OUT")), "dropped": int(self.panda.get(f"{dp}.DROPPED")) - self._dropped_at_arm, } - def wait_line_done(self, n_pulses: int, timeout: float, poll: float = 0.02) -> dict: - """Wait until the line was triggered and n_pulses detector pulses were sent.""" + def wait_lines_done(self, n_lines: int, timeout: float, poll: float = 0.02) -> dict: + """ + Wait until n_lines lines were triggered and all their pulses sent. Fails on timeout, + on more lines or pulses than expected and on ignored trigger edges (edge during a + running line). + """ + n_pulses = self._pulse_params["n_pulses"] if self._pulse_params else 0 start = time.time() while True: status = self.line_status() @@ -313,16 +352,26 @@ class PandaLineTrigger(Device): raise PandaLineTriggerError( f"{self.name}: {status['dropped']} trigger(s) dropped by {self.DETECTOR_PULSE}." ) - if not status["armed"] and status["pulses_sent"] >= n_pulses and not status["queued"]: + if ( + status["lines"] >= n_lines + and status["pulses"] >= status["lines"] * n_pulses + and not status["busy"] + ): break if time.time() - start > timeout: raise PandaLineTriggerError( - f"{self.name}: line not finished within {timeout} s: {status}. Was the line" - f" trigger received on {self.TRIGGER_INPUT}?" + f"{self.name}: {n_lines} line(s) not finished within {timeout} s: {status}." + f" Was the line trigger received on {self.TRIGGER_INPUT}?" ) time.sleep(poll) - if status["pulses_sent"] != n_pulses: + if status["lines"] != n_lines or status["pulses"] != n_lines * n_pulses: raise PandaLineTriggerError( - f"{self.name}: sent {status['pulses_sent']} pulses instead of {n_pulses}." + f"{self.name}: expected {n_lines} line(s) with {n_lines * n_pulses} pulses," + f" got {status['lines']} with {status['pulses']}." + ) + if status["triggers"] != status["lines"]: + raise PandaLineTriggerError( + f"{self.name}: {status['triggers'] - status['lines']} trigger edge(s) ignored" + f" (edge while a line was running or while not armed): {status}." ) return status diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index ee592398..f092da9a 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -8,12 +8,13 @@ from csaxs_bec.devices.panda_box.panda_line_trigger import ( class FakePanda(PandaControl): - """In-memory PandA: fields, the one-shot arm gate, the counter and a PULSE4 train.""" + """In-memory PandA: fields, arm gate, counters; a line runs completely at its trigger.""" def __init__(self): super().__init__("fake-panda") self.fields = {} self.writes = [] + self.busy = False # simulate a running line: edges are ignored def open(self): pass @@ -28,18 +29,23 @@ class FakePanda(PandaControl): return self.fields.get(command[:-1], "0") field, value = command.split("=", 1) self.writes.append(command) + f = self.fields if field == "SRGATE1.FORCE_SET": - self.fields["SRGATE1.OUT"] = "1" + f["SRGATE1.OUT"] = "1" elif field == "SRGATE1.FORCE_RST": - self.fields["SRGATE1.OUT"] = "0" - elif field == "COUNTER8.ENABLE" and value == "ONE": - self.fields["COUNTER8.OUT"] = "0" - elif field == "BITS.A" and value == "1" and self.fields.get("SRGATE1.OUT") == "1": - # first edge after arming: gate closes, the whole train is sent at once - self.fields["SRGATE1.OUT"] = "0" - self.fields["COUNTER8.OUT"] = self.fields.get("PULSE4.PULSES", "1") + f["SRGATE1.OUT"] = "0" + elif field.startswith("COUNTER") and field.endswith(".ENABLE") and value == "ONE": + f[field.replace("ENABLE", "OUT")] = "0" + elif field == "BITS.A" and value == "1": + f["COUNTER6.OUT"] = str(int(f.get("COUNTER6.OUT", "0")) + 1) + if f.get("SRGATE1.OUT") == "1" and not self.busy: + f["COUNTER7.OUT"] = str(int(f.get("COUNTER7.OUT", "0")) + 1) + pulses = int(f.get("COUNTER8.OUT", "0")) + int(f.get("PULSE4.PULSES", "1")) + f["COUNTER8.OUT"] = str(pulses) + if f.get("SRGATE1.RST") == "LUT7.OUT": + f["SRGATE1.OUT"] = "0" else: - self.fields[field] = value + f[field] = value return "" @@ -53,11 +59,13 @@ def trigger(): def test_setup_layout_wires_the_blocks(trigger): fields = trigger.panda.fields - assert fields["PULSE3.TRIG"] == "LUT7.OUT" - assert fields["PULSE4.TRIG"] == "LUT7.OUT" - assert fields["LUT7.FUNC"] == "A&B" + assert fields["PULSE3.TRIG"] == fields["PULSE4.TRIG"] == "LUT7.OUT" + assert fields["LUT7.FUNC"] == "A&B&~C&~D" + assert fields["LUT7.TYPEA"] == "Pulse-On-Rising-Edge" + assert (fields["LUT7.INPC"], fields["LUT7.INPD"]) == ("PULSE3.OUT", "PULSE4.OUT") assert fields["LUT8.INPA"] == "TTLIN3.VAL" - assert fields["SRGATE1.RST"] == "LUT8.OUT" + assert fields["COUNTER6.TRIG"] == "LUT8.OUT" + assert fields["COUNTER7.TRIG"] == "LUT7.OUT" assert fields["COUNTER8.TRIG"] == "PULSE4.OUT" assert fields["TTLOUT2.VAL"] == "PULSE3.OUT" assert fields["TTLOUT3.VAL"] == "PULSE4.OUT" @@ -65,8 +73,8 @@ def test_setup_layout_wires_the_blocks(trigger): assert fields["PULSE3.ENABLE"] == fields["PULSE4.ENABLE"] == "ONE" -def test_arm_line_sets_timing(trigger): - trigger.arm_line(10, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) +def test_configure_line_sets_timing(trigger): + trigger.configure_line(10, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) fields = trigger.panda.fields assert float(fields["PULSE4.DELAY"]) == pytest.approx(0.005) assert float(fields["PULSE4.WIDTH"]) == pytest.approx(0.008) @@ -74,66 +82,99 @@ def test_arm_line_sets_timing(trigger): assert fields["PULSE4.PULSES"] == "10" # shutter: from the trigger to the end of the last exposure assert float(fields["PULSE3.WIDTH"]) == pytest.approx(0.005 + 9 * 0.01 + 0.008) - assert trigger.line_status()["armed"] def test_pulse_parameters_are_written_only_on_change(trigger): - trigger.arm_line(10, exp_time=0.01) + trigger.configure_line(10, exp_time=0.01) trigger.panda.writes.clear() - trigger.arm_line(10, exp_time=0.01) - assert not [w for w in trigger.panda.writes if w.startswith(("PULSE3.", "PULSE4."))] - trigger.arm_line(11, exp_time=0.01) + trigger.configure_line(10, exp_time=0.01) + assert not trigger.panda.writes + trigger.configure_line(11, exp_time=0.01) assert "PULSE4.PULSES=11" in trigger.panda.writes def test_never_toggles_pulse_enable_after_setup(trigger): + trigger.configure_line(5, exp_time=0.01) trigger.panda.writes.clear() - trigger.arm_line(5, exp_time=0.01) + trigger.arm() trigger.software_trigger() - trigger.wait_line_done(5, timeout=1) + trigger.wait_lines_done(1, timeout=1) trigger.disarm() trigger.stop() - assert not [w for w in trigger.panda.writes if ".ENABLE" in w and "PULSE" in w] + assert not [w for w in trigger.panda.writes if w.startswith("PULSE")] -def test_line_runs_once_per_arm(trigger): - trigger.arm_line(5, exp_time=0.01) +def test_stays_armed_for_many_lines(trigger): + trigger.configure_line(5, exp_time=0.01) + trigger.arm() + for _ in range(4): + trigger.software_trigger() + status = trigger.wait_lines_done(4, timeout=1) + assert (status["lines"], status["pulses"], status["armed"]) == (4, 20, True) + + +def test_one_shot_accepts_one_edge(trigger): + trigger.configure_line(5, exp_time=0.01) + trigger.arm(one_shot=True) trigger.software_trigger() - assert trigger.wait_line_done(5, timeout=1)["pulses_sent"] == 5 - assert not trigger.line_status()["armed"] + assert trigger.wait_lines_done(1, timeout=1)["armed"] is False + trigger.software_trigger() + with pytest.raises(PandaLineTriggerError, match="1 trigger edge"): + trigger.wait_lines_done(1, timeout=1) -def test_wait_line_done_times_out_without_trigger(trigger): - trigger.arm_line(5, exp_time=0.01) +def test_edge_during_line_is_reported(trigger): + trigger.configure_line(5, exp_time=0.01) + trigger.arm() + trigger.software_trigger() + trigger.panda.busy = True + trigger.software_trigger() + trigger.panda.busy = False + with pytest.raises(PandaLineTriggerError, match="ignored"): + trigger.wait_lines_done(1, timeout=1) + + +def test_arm_needs_configure(trigger): + with pytest.raises(PandaLineTriggerError, match="configure_line"): + trigger.arm() + + +def test_wait_lines_done_times_out_without_trigger(trigger): + trigger.configure_line(5, exp_time=0.01) + trigger.arm() with pytest.raises(PandaLineTriggerError, match="not finished"): - trigger.wait_line_done(5, timeout=0.1, poll=0.01) + trigger.wait_lines_done(1, timeout=0.1, poll=0.01) -def test_wait_line_done_reports_wrong_count(trigger): - trigger.arm_line(5, exp_time=0.01) - trigger.software_trigger() - trigger.panda.fields["COUNTER8.OUT"] = "6" - with pytest.raises(PandaLineTriggerError, match="6 pulses instead of 5"): - trigger.wait_line_done(5, timeout=0.1, poll=0.01) +def test_wait_lines_done_reports_too_many_lines(trigger): + trigger.configure_line(5, exp_time=0.01) + trigger.arm() + for _ in range(3): + trigger.software_trigger() + with pytest.raises(PandaLineTriggerError, match="expected 2 line"): + trigger.wait_lines_done(2, timeout=0.1, poll=0.01) -def test_wait_line_done_reports_dropped(trigger): - trigger.arm_line(5, exp_time=0.01) +def test_wait_lines_done_reports_dropped(trigger): + trigger.configure_line(5, exp_time=0.01) + trigger.arm() trigger.panda.fields["PULSE4.DROPPED"] = "1" with pytest.raises(PandaLineTriggerError, match="dropped"): - trigger.wait_line_done(5, timeout=0.1, poll=0.01) + trigger.wait_lines_done(1, timeout=0.1, poll=0.01) def test_dropped_counts_only_since_arm(trigger): + trigger.configure_line(5, exp_time=0.01) trigger.panda.fields["PULSE4.DROPPED"] = "3" - trigger.arm_line(5, exp_time=0.01) + trigger.arm() assert trigger.line_status()["dropped"] == 0 def test_arm_refuses_high_trigger_input(trigger): + trigger.configure_line(5, exp_time=0.01) trigger.panda.fields["LUT8.OUT"] = "1" with pytest.raises(PandaLineTriggerError, match="is high"): - trigger.arm_line(5, exp_time=0.01) + trigger.arm() @pytest.mark.parametrize( @@ -144,16 +185,18 @@ def test_arm_refuses_high_trigger_input(trigger): ({"n_pulses": 5, "exp_time": 0.01, "shutter_delay": -0.001}, "shutter_delay"), ], ) -def test_arm_line_checks_arguments(trigger, kwargs, match): +def test_configure_line_checks_arguments(trigger, kwargs, match): with pytest.raises(PandaLineTriggerError, match=match): - trigger.arm_line(**kwargs) + trigger.configure_line(**kwargs) def test_disarm_closes_the_gate(trigger): - trigger.arm_line(5, exp_time=0.01) + trigger.configure_line(5, exp_time=0.01) + trigger.arm() trigger.disarm() trigger.software_trigger() - assert trigger.line_status() == {"armed": False, "pulses_sent": 0, "queued": 0, "dropped": 0} + status = trigger.line_status() + assert (status["armed"], status["lines"], status["pulses"]) == (False, 0, 0) def test_control_reply_parsing(): -- 2.54.0 From 1222198a1b4d3fc94b732d6894888ab362d4d414 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 16:35:21 +0100 Subject: [PATCH 09/52] fix(aerotech): move completes on the controller's In Position status First moves on the SAXS-TT hardware (X, Z, Y, +-0.5 mm, 2026-10-28): the move was evaluated when the motion profile ended (MoveDone), X was still settling (1.2 um off) and the move failed. After MoveDone, move() now waits for DriveStatus In Position (InPositionDistance / InPositionTime, timeout in_position_timeout = 2 s) before comparing with the target. axis_status() also reports in_position and encoder_clipping. Tolerances per axis in the configs: X 1 um, Z/Y 5 um (InPositionDistance 5 um). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/device_configs/sastt_aerotech.yaml | 4 ++++ .../device_configs/sastt_aerotech_panda.yaml | 3 +++ .../simulated_sastt_aerotech.yaml | 3 +++ csaxs_bec/devices/aerotech/a3200.py | 20 +++++++++++++++++++ csaxs_bec/devices/sim/sim_a3200.py | 7 ++++++- tests/tests_devices/test_a3200.py | 20 +++++++++++++++++++ 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index b64ebb0a..40adb73f 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -10,6 +10,7 @@ # Z has no emulated quadrature output, so no PSO line trigger on Z. # PSO interlock defeated (IOSetup 0x80) on all axes. # controller soft limits: X -50..150, Z -100..100, Y -10..10 mm (active after home) +# tolerance: X 1 um, Z/Y 5 um (InPositionDistance X 10 nm, Z/Y 5 um) # velocity / acceleration below = DefaultSpeed / DefaultRampRate (MaxSpeedClamp: # X 200, Z 10, Y 10 mm/s, U 720 deg/s) # Wiring for aerotech_cont_grid: PSO output (J7 "PSO INTERFACE") -> DDG1 EXT, @@ -30,6 +31,7 @@ aerox: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.001 limits: - -50 - 150 @@ -52,6 +54,7 @@ aeroy: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.005 limits: - -10 - 10 @@ -73,6 +76,7 @@ aeroz: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.005 limits: - -100 - 100 diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index 5bfcfe7a..09a40597 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -21,6 +21,7 @@ aerox: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.001 limits: - -50 - 150 @@ -43,6 +44,7 @@ aeroy: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.005 limits: - -10 - 10 @@ -64,6 +66,7 @@ aeroz: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.005 limits: - -100 - 100 diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index eb66414d..e95bd902 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -17,6 +17,7 @@ aerox: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.001 limits: - -50 - 150 @@ -40,6 +41,7 @@ aeroy: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.005 limits: - -10 - 10 @@ -62,6 +64,7 @@ aeroz: port: 8000 velocity: 5.0 egu: mm + tolerance: 0.005 limits: - -100 - 100 diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index 97190b24..8a1a063b 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -61,6 +61,8 @@ DRIVESTATUS_ENABLED = 1 << 0 DRIVESTATUS_CW_EOT_LIMIT = 1 << 1 DRIVESTATUS_CCW_EOT_LIMIT = 1 << 2 DRIVESTATUS_HOME_LIMIT = 1 << 3 +DRIVESTATUS_IN_POSITION = 1 << 25 # InPositionDistance / InPositionTime +DRIVESTATUS_ENCODER_CLIPPING = 1 << 29 # encoder multiplier input signals out of range # Axis Fault bits AXIS_FAULT_NAMES = { @@ -246,8 +248,10 @@ class A3200Controller(Controller): "homed": bool(axis_status & AXISSTATUS_HOMED), "moving": self._is_moving(axis_status), "faults": decode_axis_fault(axis_fault), + "in_position": bool(drive_status & DRIVESTATUS_IN_POSITION), "cw_limit": bool(drive_status & DRIVESTATUS_CW_EOT_LIMIT), "ccw_limit": bool(drive_status & DRIVESTATUS_CCW_EOT_LIMIT), + "encoder_clipping": bool(drive_status & DRIVESTATUS_ENCODER_CLIPPING), } @staticmethod @@ -489,6 +493,10 @@ class A3200Motor(Device, PositionerBase): velocity: move speed in user units per second (A3200 user units: mm or deg). egu: engineering units shown in BEC. require_homed: refuse moves while the axis is not homed. + tolerance: allowed deviation from the target after the move, in user units. + in_position_timeout: after the motion profile ends, wait this long (s) for the + controller's In Position status (InPositionDistance / InPositionTime) before the + position is compared with the target. acceleration: ramp rate for moves in user units/s^2; None keeps the controller's ramp settings (DefaultRampRate). pso_input: PSO input of this axis, needed for the PSO line trigger @@ -539,6 +547,7 @@ class A3200Motor(Device, PositionerBase): velocity=1.0, egu="mm", tolerance=0.001, + in_position_timeout=2.0, require_homed=True, acceleration=None, pso_input=None, @@ -555,6 +564,7 @@ class A3200Motor(Device, PositionerBase): self.controller.set_axis(axis=self, axis_nr=axis_nr) self.sign = sign self.tolerance = tolerance + self.in_position_timeout = in_position_timeout self.require_homed = require_homed self.pso_input = pso_input self.pso_counts_divider = pso_counts_divider @@ -641,6 +651,16 @@ class A3200Motor(Device, PositionerBase): val = self.readback.read() self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) time.sleep(0.1) + # the profile is done, the axis may still be settling + deadline = time.time() + self.in_position_timeout + while not self.controller.axis_status(self.axis_Id)["in_position"]: + if time.time() > deadline: + logger.warning( + f"{self.name}: not in position {self.in_position_timeout} s" + " after the end of the move." + ) + break + time.sleep(0.02) val = self.readback.read() self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) success = np.isclose(val[self.name]["value"], position, atol=self.tolerance) diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index 20c6b9e6..7d8f637e 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -33,6 +33,7 @@ from csaxs_bec.devices.aerotech.a3200 import ( AXISSTATUS_JOGGING, AXISSTATUS_MOVE_DONE, DRIVESTATUS_ENABLED, + DRIVESTATUS_IN_POSITION, A3200Motor, A3200PositionCapture, ) @@ -58,6 +59,7 @@ class _SimA3200Axis: self.counts_per_unit = 10000.0 self.counter_offset = 0 self.ramp_rate = None + self.settle_time = 0.0 # In Position this long after the end of a move self.pos = 0.0 self._move_start = 0.0 self._move_target = 0.0 @@ -105,7 +107,10 @@ class _SimA3200Axis: return status def drive_status(self) -> int: - return DRIVESTATUS_ENABLED if self.enabled else 0 + status = DRIVESTATUS_ENABLED if self.enabled else 0 + if not self.is_moving() and time.time() >= self._move_t1 + self.settle_time: + status |= DRIVESTATUS_IN_POSITION + return status class SimA3200State: diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index 7fcd7ceb..f2d84f33 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -1,3 +1,5 @@ +import time + import pytest from bec_server.device_server.tests.utils import DMMock from ophyd.utils import LimitError @@ -335,3 +337,21 @@ def test_capture_rejects_analog_sources(): channels={6: "X", 12: "Y"}, device_manager=DMMock(), ) + + +def test_move_waits_for_in_position(): + motor = _sim_motor(velocity=100.0) + SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X").settle_time = 0.3 + start = time.time() + motor.move(1.0, wait=True) + assert time.time() - start >= 0.3 + assert motor.status()["in_position"] + + +def test_move_not_in_position_still_finishes(): + motor = _sim_motor(velocity=100.0, in_position_timeout=0.1) + SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X").settle_time = 5.0 + start = time.time() + motor.move(1.0, wait=True) + assert time.time() - start < 1.0 + assert motor.readback.get() == pytest.approx(1.0) -- 2.54.0 From f137517e62b3015195b858a955aac7bd3d33a0a5 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 16:59:29 +0100 Subject: [PATCH 10/52] feat(aerotech): snake scan with one PSO window per scan, arm once aerotech_cont_grid: - snake=True (default): alternating line direction. One PSO window per scan, [fast_start - v*delay, exposure_end + v*delay], armed once in pre_scan; its output rises on entering the window, i.e. at the low edge forward and the high edge backward. snake=False switches the PSO off during the flyback. - trigger_source "ddg" (beamline), "panda" (PandA configured and armed once, every line checked) or None (no trigger hardware); trigger_edge for DDG1, default falling (active low open collector / differential PSO output). - settle_time (default 0.15 s) for the run-up at constant speed, measured on the hardware 2026-10-28; capture_positions defaults to False until the sampling is commissioned. - the fast axis does not wait for In Position at the turnarounds (wait_in_position signal of A3200Motor, off during the scan); restore steps are all tried. PandaLineTrigger: trigger_edge config, "falling" inverts TTLIN3. Hardware test of the scan sequence (trigger_source=None, 1x1 mm, 11 lines, X 1 mm/s): 15.3 s instead of 23.1 s; PSO window switched on at X=0.25 forward and X=0.76 backward. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- .../device_configs/sastt_aerotech_panda.yaml | 1 + csaxs_bec/devices/aerotech/README.md | 57 +++- csaxs_bec/devices/aerotech/a3200.py | 19 +- .../devices/panda_box/panda_line_trigger.py | 9 +- .../scans/scans_v4/aerotech_cont_grid.py | 308 ++++++++++++------ tests/tests_devices/test_a3200.py | 10 + .../tests_devices/test_panda_line_trigger.py | 12 + .../test_aerotech_cont_grid_scan.py | 255 +++++++++++++-- 8 files changed, 523 insertions(+), 148 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index 09a40597..039071f8 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -128,6 +128,7 @@ pandatrig: host: omny-panda.psi.ch port: 8888 input_termination: 50-Ohm + trigger_edge: rising enabled: true onFailure: buffer readOnly: false diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 0a5b6f50..8664e6e3 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -55,23 +55,54 @@ Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface" ## Scan: aerotech_cont_grid -`scans_v4/aerotech_cont_grid.py`, same arguments as `cont_grid`: +`scans_v4/aerotech_cont_grid.py`, arguments as `cont_grid` plus options: ```python -scans.aerotech_cont_grid(dev.aerox, 0, 2, 0.01, dev.aeroy, 0, 1, 0.05, exp_time=0.01) +scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0.01, + snake=True, trigger_source="ddg", settle_time=0.15) ``` -- fast axis moves continuously at `fast_step_size / exp_time`, the step axis steps between lines -- line start is triggered by position: PSO window on the fast axis, the output goes high - at `fast_start - velocity * shutter_open_delay` and triggers DDG1 (external, rising edge), - so the first exposure starts at `fast_start` -- run-up / run-out = ramp distance `v^2 / (2 * acceleration)` + 20 % + 50 ms at speed -- `capture_positions=True` (default): device `aerocapture` latches the encoder counters of - all configured channels on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz - with MXR sources, drive array of 2M elements). After each line they are read back, converted to user units and - published as async data `aerocapture_positions_enc`. -- the counter-to-position offset is calibrated in pre_scan with one software PSO fire, - before DDG1 is switched to the external trigger +- fast axis X moves continuously at `fast_step_size / exp_time`, the step axis Y steps between + lines; `snake=True` (default) alternates the line direction +- line start triggered by position: one PSO window per scan, + `[fast_start - v * shutter_delay, exposure_end + v * shutter_delay]`, armed once in + pre_scan. Its output rises on entering the window: at the low edge on forward lines, at + the high edge on backward lines, so in both directions the first exposure starts one + shutter delay after the trigger at the first point of the line +- `trigger_source`: `"ddg"` (beamline: PSO -> DDG1 EXT; DDG1/DDG2 take exp_time and + frames_per_trigger = frames per line from the scan info at stage; MCS prepared per line), + `"panda"` (test setup: PandA configured and armed once, every line checked with + `wait_lines_done`; `readout_time`, `shutter_delay` arguments) or `None` (no trigger + hardware, motion tests) +- `trigger_edge` (DDG1): `"falling"` (default) for the active low PSO open collector (J7-8) + or differential (J7-1/2) output, `"rising"` for the opto output (J7-6/7). The PandA has + the same choice as device config `trigger_edge` (default `"rising"`, opto wiring). +- run-up / run-out = ramp distance `v^2 / (2 * acceleration)` + 20 % + `v * settle_time` + (default 0.15 s, measured 2026-10-28: the following error needs ~100-200 ms after the + ramp at 1 mm/s / 10 mm/s^2). In a snake the run-out of a line is the run-up of the next. +- at the turnarounds X does not wait for In Position (`wait_in_position` signal off during + the scan); X reaches 1 um within ~60 ms but 10 nm only after 0.5-2 s +- `snake=False`: the flyback passes the PSO window, so the PSO is switched off during it +- `capture_positions=True` (default False until commissioned): device `aerocapture` latches + the encoder counters on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz with + MXR sources); after each line they are read back (~2600 values/s over the ASCII interface) + and published as async data `aerocapture_positions_enc`. The counter offset is + calibrated in pre_scan with one software PSO fire, before any trigger receiver is armed. + +Soft test 2026-10-28 (scan sequence on the hardware, trigger_source=None, 1x1 mm, X 1 mm/s, +11 lines): 15.3 s, line 1.11 s, Y step 0.28 s; PSO "inside window" switched on at X = 0.25 +going forward and at X = 0.76 going backward (window 0.25..0.76). + +PSO output polarity: the PSO status bit "Output 1" is high outside the window. Open collector +(J7-8) and differential outputs are active low; the opto output (J7-6/7, JP16 normally open) +conducts inside the window. Check on the wiring day that the trigger receiver input goes +high when X enters the window (e.g. park X inside / outside and read `TTLIN3.VAL`). + +Second client on the ASCII interface: only `~STATUS` (server command) is safe in parallel. +Controller commands of all clients run on the library task by default and are refused +while another client's command is executing ("An immediate command cannot be executed while +another immediate command is executing"); a second client that sends controller commands +must switch to its own task first (`~TASK 3`, tasks 1-4 are idle). Wiring: diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index 8a1a063b..7abf069b 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -494,6 +494,9 @@ class A3200Motor(Device, PositionerBase): egu: engineering units shown in BEC. require_homed: refuse moves while the axis is not homed. tolerance: allowed deviation from the target after the move, in user units. + wait_in_position (signal): False makes a move finish when the motion profile ends, + without waiting for In Position and without the tolerance check (used by + scans for the turnarounds of the fast axis). Default True. in_position_timeout: after the motion profile ends, wait this long (s) for the controller's In Position status (InPositionDistance / InPositionTime) before the position is compared with the target. @@ -522,6 +525,7 @@ class A3200Motor(Device, PositionerBase): homed = Cpt(A3200AxisHomed, signal_name="homed", kind="config") motor_velocity = Cpt(Signal, value=1.0, kind="config") motor_acceleration = Cpt(Signal, value=0.0, kind="config") + wait_in_position = Cpt(Signal, value=True, kind="config") high_limit_travel = Cpt(Signal, value=0, kind="omitted") low_limit_travel = Cpt(Signal, value=0, kind="omitted") @@ -646,11 +650,22 @@ class A3200Motor(Device, PositionerBase): def move_and_finish(): # MOVEABS is asynchronous; give the controller one cycle to set the busy bits - time.sleep(0.05) + time.sleep(0.02) + last_readback = 0.0 while self.motor_is_moving.get(): + if time.time() - last_readback > 0.1: + val = self.readback.read() + self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) + last_readback = time.time() + time.sleep(0.02) + if not self.wait_in_position.get(): val = self.readback.read() self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time()) - time.sleep(0.1) + faults = self.controller.axis_status(self.axis_Id)["faults"] + if faults: + logger.warning(f"{self.name} stopped with faults {faults}.") + self._done_moving(success=not faults) + return # the profile is done, the axis may still be settling deadline = time.time() + self.in_position_timeout while not self.controller.axis_status(self.axis_Id)["in_position"]: diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 45a2a1d8..3b887789 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -104,6 +104,8 @@ class PandaLineTrigger(Device): port: control port. input_termination: TTLIN3 termination, "50-Ohm" (pull-down for the Npaq opto output) or "High-Z". + trigger_edge: edge of TTLIN3 that starts a line, "rising" (Npaq opto output) or + "falling" (active low open collector / differential output). """ USER_ACCESS = [ @@ -138,11 +140,15 @@ class PandaLineTrigger(Device): host: str, port: int = 8888, input_termination: str = "50-Ohm", + trigger_edge: str = "rising", device_manager=None, **kwargs, ): self.panda = PandaControl(host, port) self.input_termination = input_termination + if trigger_edge not in ("rising", "falling"): + raise ValueError(f"trigger_edge must be 'rising' or 'falling', got {trigger_edge!r}.") + self.trigger_edge = trigger_edge self._pulse_params = None # parameters written to the PULSE blocks self._dropped_at_arm = 0 # DROPPED is only zeroed by ENABLE, which is never toggled self.device_manager = device_manager @@ -173,7 +179,8 @@ class PandaLineTrigger(Device): (self.SOFT_TRIGGER_BIT, "0"), (f"{raw}.INPA", f"{self.TRIGGER_INPUT}.VAL"), (f"{raw}.INPB", "BITS.OUTA"), - (f"{raw}.FUNC", "A|B"), + # raw trigger active high: a falling TTLIN3 edge is inverted to a rising one + (f"{raw}.FUNC", "A|B" if self.trigger_edge == "rising" else "~A|B"), (f"{gate}.ENABLE", "ONE"), (f"{gate}.SET", "ZERO"), (f"{gate}.RST", "ZERO"), diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 02353f02..4a91de85 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -1,21 +1,34 @@ """ Continuous grid scan for Aerotech A3200 stages with a position triggered line start. -Same geometry as cont_grid (fast axis moving continuously, step axis stepping between -lines), but the start of each line is triggered in hardware: the A3200 PSO output goes -high when the fast axis enters the line window and triggers DDG1 (EXT input, rising edge), -which runs the shutter / detector / MCS sequence. With a position capture device, the -encoder positions of the axes are latched on each detector trigger (Npaq UINT1 input) -and published per line. +The fast axis moves continuously, the step axis steps between lines. By default the lines +run as a snake (alternating direction). The start of each line is triggered in hardware by +the A3200 PSO output of the fast axis: one PSO window per scan, -Wiring: - Npaq PSO output (J7) -> DDG1 EXT/EN - detector trigger (DDG2 AB) -> Npaq UINT1 (J12 pins 1/2, 5 V opto input) + [fast_start - v * shutter_delay, exposure_end + v * shutter_delay] -Timing per line: - trigger position = fast_start - velocity * shutter_open_delay, so the first exposure - starts at fast_start; the fast axis starts one ramp distance (plus margin) before the - trigger position and stops one ramp distance after the last exposure. +goes high when the axis enters it, i.e. at the low edge on forward lines and at the high +edge on backward lines, so in both directions the first exposure starts one shutter delay +after the trigger at the first point of the line. The window is armed once per scan and +the trigger receiver stays armed; between lines the scan only moves the axes. + +Trigger sources (trigger_source): + "ddg": PSO output -> DDG1 EXT (beamline). DDG1 runs shutter / detector / MCS, the MCS + card is prepared per line. trigger_edge: the Npaq open collector (J7-8) and + differential (J7-1/2) PSO outputs are active low, so the window entry is a + falling edge (default); the opto output (J7-6/7) gives a rising edge. + "panda": PSO output -> PandABox TTLIN3 (device pandatrig, test setup). The PandA is + configured and armed once and sends the shutter pulse and the detector gates + of each line. + None: no trigger hardware (motion tests); the PSO window is still armed. + +With a position capture device (capture_positions=True), the encoder positions are latched +at each detector trigger (Npaq UINT1) and published per line. + +Run-up / run-out of each line: ramp distance v^2 / (2 * acceleration) + 20 % + v * +settle_time beyond the trigger positions. In a snake the run-out of a line is the run-up of +the next one, so both sides are equal. At the turnarounds the fast axis does not wait for +In Position. Scan procedure: - prepare_scan @@ -32,7 +45,6 @@ Scan procedure: from __future__ import annotations -from copy import deepcopy from typing import Annotated, TypedDict import numpy as np @@ -40,7 +52,6 @@ from bec_lib.device import DeviceBase from bec_lib.logger import bec_logger from bec_lib.scan_args import DefaultArgType, ScanArgument from bec_server.scan_server.errors import ScanAbortion -from bec_server.scan_server.scans import position_generators from bec_server.scan_server.scans.scan_base import ScanBase, ScanType from bec_server.scan_server.scans.scan_modifier import scan_hook @@ -50,11 +61,14 @@ logger = bec_logger.logger # DDG trigger sources (csaxs_bec.devices.epics.delay_generator_csaxs.TRIGGERSOURCE) DDG_EXT_RISING_EDGE = 1 +DDG_EXT_FALLING_EDGE = 2 DDG_SINGLE_SHOT = 5 +DDG_TRIGGER_EDGES = {"rising": DDG_EXT_RISING_EDGE, "falling": DDG_EXT_FALLING_EDGE} -# Extra run-up / run-out beyond the ramp distance, as a fraction of it, and as settling time +# Extra run-up / run-out beyond the ramp distance, as a fraction of it RAMP_MARGIN_FRACTION = 0.2 -SETTLE_TIME = 0.05 + +TRIGGER_SOURCES = ("ddg", "panda", None) class AerotechLineParameter(TypedDict): @@ -64,7 +78,8 @@ class AerotechLineParameter(TypedDict): target_velocity: float | None acceleration: float | None shutter_open_delay: float | None - trigger_position: float | None + trigger_low: float | None + trigger_high: float | None exposure_end: float | None line_start: float | None line_end: float | None @@ -72,8 +87,7 @@ class AerotechLineParameter(TypedDict): class AerotechContGrid(ScanBase): - # One DDG1 sequence per line, like cont_grid: the MCS card and DDG1 count each line as - # one point with frames_per_trigger frames. + # One line = one point with frames_per_trigger frames, like cont_grid. scan_type = ScanType.SOFTWARE_TRIGGERED scan_name = "aerotech_cont_grid" @@ -88,7 +102,13 @@ class AerotechContGrid(ScanBase): "stepper_stop", "stepper_step_size", ], - "Acquisition Parameters": ["exp_time", "relative", "capture_positions"], + "Acquisition Parameters": [ + "exp_time", + "relative", + "snake", + "trigger_source", + "capture_positions", + ], } def __init__( @@ -104,15 +124,22 @@ class AerotechContGrid(ScanBase): stepper_step_size: Annotated[float, ScanArgument(display_name="Step Step Size", description="Step size of the step axis in units of the motor.")], exp_time: DefaultArgType.ExposureTime, relative: DefaultArgType.Relative = False, - capture_positions: bool = True, + snake: bool = True, + trigger_source: str | None = "ddg", + trigger_edge: str = "falling", + trigger_device: str = "pandatrig", + readout_time: float = 0.0, + shutter_delay: float | None = None, + settle_time: float = 0.15, + capture_positions: bool = False, capture_device: str = "aerocapture", **kwargs, # fmt: on ): """ - Continuous grid scan for A3200 stages: the PSO output of the fast axis triggers - DDG1 at the start of each line, optionally the axis positions are latched at each - detector trigger. + Continuous grid scan for A3200 stages: the PSO output of the fast axis triggers the + start of each line, optionally the axis positions are latched at each detector + trigger. Args: fast_axis (DeviceBase): A3200 axis with continuous motion (needs pso_input and acceleration configured). @@ -123,9 +150,16 @@ class AerotechContGrid(ScanBase): stepper_start (float): Start position of the step axis. stepper_stop (float): End position of the step axis. stepper_step_size (float): Step size of the step axis in units of the motor. - exp_time (float): Exposure time in seconds + exp_time (float): Exposure time (period of the detector gates) in seconds. relative (bool): Whether the positions are relative to the current position. Default is False. - capture_positions (bool): Latch the axis positions at each detector trigger. Default is True. + snake (bool): Alternate the direction of the lines. Default is True. + trigger_source (str | None): "ddg" (beamline, DDG1/MCS), "panda" (PandABox line trigger) or None (no trigger hardware). Default is "ddg". + trigger_edge (str): DDG1 external trigger edge, "falling" (PSO open collector / differential output) or "rising" (opto output). Default is "falling". + trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". + readout_time (float): Detector readout time, gate width = exp_time - readout_time (trigger_source="panda"). Default is 0. + shutter_delay (float | None): Delay from the line trigger to the first exposure in s. Default: DDG1 shutter delay for "ddg", 0 otherwise. + settle_time (float): Run-up at constant velocity before the trigger position, in s. Default is 0.15. + capture_positions (bool): Latch the axis positions at each detector trigger. Default is False. capture_device (str): Name of the A3200PositionCapture device. Default is "aerocapture". Returns: @@ -134,8 +168,17 @@ class AerotechContGrid(ScanBase): super().__init__(**kwargs) self.components = CsaxsBecScanComponents(self) self._baseline_readout_status = None + if trigger_source not in TRIGGER_SOURCES: + raise ScanAbortion( + f"trigger_source must be one of {TRIGGER_SOURCES}, got {trigger_source!r}." + ) + if trigger_edge not in DDG_TRIGGER_EDGES: + raise ScanAbortion( + f"trigger_edge must be one of {list(DDG_TRIGGER_EDGES)}, got {trigger_edge!r}." + ) + self.trigger_edge = trigger_edge self.fast_axis = fast_axis - if fast_end < fast_start: # the line always runs in positive direction + if fast_end < fast_start: # the points are defined in positive direction fast_start, fast_end = fast_end, fast_start self.fast_start = fast_start self.fast_end = fast_end @@ -146,21 +189,35 @@ class AerotechContGrid(ScanBase): self.stepper_step_size = stepper_step_size self.relative = relative self.exp_time = exp_time + self.snake = snake + self.trigger_source = trigger_source + self.readout_time = readout_time + self.shutter_delay = shutter_delay + self.settle_time = settle_time self.capture_positions = capture_positions - self.ddg1 = self.device_manager.devices["ddg1"] - self.mcs = self.device_manager.devices["mcs"] + + devices = self.device_manager.devices + self.ddg1 = self.mcs = self.panda = None + if trigger_source == "ddg": + self.ddg1 = devices["ddg1"] + self.mcs = devices["mcs"] + elif trigger_source == "panda": + if trigger_device not in devices: + raise ScanAbortion(f"PandA line trigger device '{trigger_device}' not found.") + self.panda = devices[trigger_device] self.capture = None if capture_positions: - if capture_device not in self.device_manager.devices: + if capture_device not in devices: raise ScanAbortion( f"Position capture device '{capture_device}' not found." " Use capture_positions=False to scan without it." ) - self.capture = self.device_manager.devices[capture_device] + self.capture = devices[capture_device] self.motors = [fast_axis, stepper_axis] self.frames_per_line = 0 self._line: AerotechLineParameter = AerotechLineParameter() + self._fast_axis_configured = False self.update_scan_info(relative=relative, exp_time=exp_time) @scan_hook @@ -170,30 +227,33 @@ class AerotechContGrid(ScanBase): self.frames_per_line = ( int(np.ceil(np.abs(self.fast_end - self.fast_start) / self.fast_step_size)) + 1 ) # include last point - self._line["num_lines"] = ( + num_lines = ( int(np.ceil(np.abs(self.stepper_stop - self.stepper_start) / self.stepper_step_size)) + 1 ) # include last point - positions = position_generators.nd_grid_positions( - [ - (self.fast_start, self.fast_end, self.frames_per_line), - (self.stepper_start, self.stepper_stop, self._line["num_lines"]), - ], - snaked=False, - ) + self._line["num_lines"] = num_lines if self.relative: self.start_positions = self.components.get_start_positions(self.motors) - positions += self.start_positions self.fast_start += self.start_positions[0] self.fast_end += self.start_positions[0] self.stepper_start += self.start_positions[1] self.stepper_stop += self.start_positions[1] + # nominal points: the start of each exposure on forward lines; a backward line + # exposes the same intervals in reverse order + fast_points = self.fast_start + np.arange(self.frames_per_line) * self.fast_step_size + stepper_points = np.linspace(self.stepper_start, self.stepper_stop, num_lines) + positions = [] + for line_index, stepper_pos in enumerate(stepper_points): + points = fast_points if self._is_forward(line_index) else fast_points[::-1] + positions.extend([point, stepper_pos] for point in points) + positions = np.array(positions) + self._compute_line_geometry() - # One readout per line: the end point of each line, as in cont_grid - self.positions = deepcopy(positions[(self.frames_per_line - 1) :: self.frames_per_line, :]) + # One readout per line: the last point of each line, as in cont_grid + self.positions = positions[(self.frames_per_line - 1) :: self.frames_per_line, :].copy() self.actions.set_device_readout_priority(self.motors, priority="monitored") self.update_scan_info( @@ -201,10 +261,11 @@ class AerotechContGrid(ScanBase): num_points=len(self.positions), frames_per_trigger=self.frames_per_line, computed_positions=positions, - num_lines=self._line["num_lines"], + num_lines=num_lines, ) - self.actions.add_scan_report_instruction_device_progress(self.mcs) + if self.mcs is not None: + self.actions.add_scan_report_instruction_device_progress(self.mcs) self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) self._premove_motor_status = self.actions.set( self.motors, [self._line["line_start"], self.stepper_start], wait=False @@ -217,38 +278,61 @@ class AerotechContGrid(ScanBase): @scan_hook def stage(self): - """Stage all devices. DDG1 is set to its default (single shot) trigger here.""" + """Stage all devices.""" self.actions.stage_all_devices() @scan_hook def pre_scan(self): """ - Calibrate the position capture while DDG1 still ignores external triggers (the - calibration fires the PSO output once), then switch DDG1 to the external trigger. + Calibrate the position capture while no trigger receiver is armed (the calibration + fires the PSO output once), then arm the PSO window once for the whole scan, set the + scan velocity and arm the trigger receiver. """ self._premove_motor_status.wait() if self.capture is not None: self.capture.calibrate() - self.ddg1.set_trigger(DDG_EXT_RISING_EDGE) + if "target_velocity" not in self._line: # pre_scan without prepare_scan + self._compute_line_geometry() + line = self._line + self._fast_axis_configured = True + self.fast_axis.motor_velocity.set(line["target_velocity"]).wait(timeout=5) + self.fast_axis.wait_in_position.set(False).wait(timeout=5) + self.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]) + if self.trigger_source == "ddg": + self.ddg1.set_trigger(DDG_TRIGGER_EDGES[self.trigger_edge]) + elif self.trigger_source == "panda": + self.panda.configure_line( + self.frames_per_line, self.exp_time, self.readout_time, line["shutter_open_delay"] + ) + self.panda.arm() self.actions.pre_scan_all_devices() @scan_hook def scan_core(self): """One continuous line per step axis position.""" - for line_index in range(self._line["num_lines"]): - status_motor = self.actions.set( - self.motors, [self._line["line_start"], self.positions[line_index][1]] - ) - status_motor.wait() - status_mcs = self.ddg1.prepare_mcs_on_trigger() - status_mcs.wait() - mcs_acquiring_status = self.mcs.get_transition_status( - signal_name="acquiring", transition=[1, 0] - ) + line = self._line + for line_index in range(line["num_lines"]): + stepper_pos = self.positions[line_index][1] + if line_index > 0: + if self.snake: + self.actions.set([self.stepper_axis], [stepper_pos]).wait() + else: + # the flyback passes the PSO window: no line trigger while returning + self.fast_axis.disarm_line_trigger() + self.actions.set(self.motors, [line["line_start"], stepper_pos]).wait() + self.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]) + status_mcs = None + if self.trigger_source == "ddg": + self.ddg1.prepare_mcs_on_trigger().wait() + status_mcs = self.mcs.get_transition_status( + signal_name="acquiring", transition=[1, 0] + ) + target = line["line_end"] if self._is_forward(line_index) else line["line_start"] self.at_each_point( motors=[self.fast_axis], - positions=np.array([self._line["line_end"]]), - status_mcs=mcs_acquiring_status, + positions=np.array([target]), + line_index=line_index, + status_mcs=status_mcs, ) @scan_hook @@ -256,43 +340,41 @@ class AerotechContGrid(ScanBase): self, motors: list[str | DeviceBase], positions: np.ndarray, - status_mcs, + line_index: int, + status_mcs=None, last_positions: np.ndarray | None = None, ): """ - Arm the line trigger (and the position capture), run the line at scan velocity, - wait for the MCS card, then read the captured positions. + Run one line: arm the position capture, move the fast axis through the PSO window, + then check that the line was triggered and read the captured positions. """ if self.capture is not None: self.capture.arm(self.frames_per_line) - self.fast_axis.arm_line_trigger(self._line["trigger_position"], self._line["exposure_end"]) - self.fast_axis.motor_velocity.set(self._line["target_velocity"]).wait(timeout=5) - try: - move_status = self.actions.set(motors, positions, wait=False) - while not move_status.done: - self.actions.read_monitored_devices(wait=True) - try: - move_status.wait(timeout=0.5) - except TimeoutError: - continue - finally: - self.fast_axis.motor_velocity.set(self._line["original_velocity"]).wait(timeout=5) - self.fast_axis.disarm_line_trigger() + move_status = self.actions.set(motors, positions, wait=False) + while not move_status.done: + self.actions.read_monitored_devices(wait=True) + try: + move_status.wait(timeout=0.5) + except TimeoutError: + continue - try: - status_mcs.wait(timeout=3) - except TimeoutError as exc: - raise ScanAbortion( - "MCS card did not go back to DONE after the line. Was the line trigger" - " received? Check the PSO output -> DDG1 EXT wiring and the PSO interlock." - ) from exc + if self.trigger_source == "ddg": + try: + status_mcs.wait(timeout=3) + except TimeoutError as exc: + raise ScanAbortion( + "MCS card did not go back to DONE after the line. Was the line trigger" + " received? Check the PSO output -> DDG1 EXT wiring and the PSO interlock." + ) from exc + elif self.trigger_source == "panda": + self.panda.wait_lines_done(line_index + 1, timeout=self._line_duration() + 2) if self.capture is not None: self.capture.read_line() @scan_hook def post_scan(self): - """Restore DDG1 trigger and fast axis velocity, complete devices.""" + """Restore trigger receiver, PSO and fast axis settings, complete devices.""" self._restore() status = self.actions.complete_all_devices(wait=False) if self.relative: @@ -314,7 +396,7 @@ class AerotechContGrid(ScanBase): @scan_hook def on_exception(self, exception: Exception): - """Return to a safe state: no external DDG1 trigger, PSO off, velocity restored.""" + """Return to a safe state: no external trigger, PSO off, fast axis settings restored.""" self._restore() if self.relative: self.components.move_and_wait(self.motors, self.start_positions) @@ -323,6 +405,13 @@ class AerotechContGrid(ScanBase): ######### Helper methods for the scan logic ########### ####################################################### + def _is_forward(self, line_index: int) -> bool: + return not self.snake or line_index % 2 == 0 + + def _line_duration(self) -> float: + line = self._line + return abs(line["line_end"] - line["line_start"]) / line["target_velocity"] + def _check_motor_inputs(self): if np.isclose(self.fast_start, self.fast_end, atol=self.fast_step_size): raise ScanAbortion( @@ -332,11 +421,18 @@ class AerotechContGrid(ScanBase): raise ScanAbortion( f"Stepper stop {self.stepper_stop} and stepper start {self.stepper_start} positions are too close for the given step size {self.stepper_step_size}." ) + if self.settle_time < 0: + raise ScanAbortion(f"settle_time must be >= 0, got {self.settle_time}.") def _compute_line_geometry(self): - """Velocity, trigger position and run-up / run-out of the fast axis.""" + """Velocity, PSO window and run-up / run-out of the fast axis.""" line = self._line - line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() + if self.shutter_delay is not None: + line["shutter_open_delay"] = self.shutter_delay + elif self.trigger_source == "ddg": + line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() + else: + line["shutter_open_delay"] = 0.0 line["original_velocity"] = self.fast_axis.motor_velocity.get() line["acceleration"] = self.fast_axis.motor_acceleration.get() @@ -352,18 +448,36 @@ class AerotechContGrid(ScanBase): " run-up distance of the line." ) ramp_distance = velocity**2 / (2 * line["acceleration"]) - margin = RAMP_MARGIN_FRACTION * ramp_distance + velocity * SETTLE_TIME + margin = RAMP_MARGIN_FRACTION * ramp_distance + velocity * self.settle_time + lead = velocity * line["shutter_open_delay"] line["target_velocity"] = velocity - line["trigger_position"] = self.fast_start - velocity * line["shutter_open_delay"] line["exposure_end"] = self.fast_start + self.frames_per_line * self.fast_step_size - line["line_start"] = line["trigger_position"] - ramp_distance - margin - line["line_end"] = line["exposure_end"] + ramp_distance + margin + line["trigger_low"] = self.fast_start - lead + line["trigger_high"] = line["exposure_end"] + lead + line["line_start"] = line["trigger_low"] - ramp_distance - margin + line["line_end"] = line["trigger_high"] + ramp_distance + margin def _restore(self): - try: - self.ddg1.set_trigger(DDG_SINGLE_SHOT) - finally: + """Undo the scan settings; every step is tried even if an earlier one fails.""" + steps = [] + if self.trigger_source == "ddg": + steps.append(lambda: self.ddg1.set_trigger(DDG_SINGLE_SHOT)) + elif self.trigger_source == "panda": + steps.append(self.panda.disarm) + if self._fast_axis_configured: if self._line.get("original_velocity") is not None: - self.fast_axis.motor_velocity.put(self._line["original_velocity"]) - self.fast_axis.disarm_line_trigger() + steps.append( + lambda: self.fast_axis.motor_velocity.put(self._line["original_velocity"]) + ) + steps.append(lambda: self.fast_axis.wait_in_position.put(True)) + steps.append(self.fast_axis.disarm_line_trigger) + errors = [] + for step in steps: + try: + step() + except Exception as exc: # pylint: disable=broad-except + logger.error(f"aerotech_cont_grid: restore step failed: {exc}") + errors.append(exc) + if errors: + raise errors[0] diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index f2d84f33..77b0f102 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -355,3 +355,13 @@ def test_move_not_in_position_still_finishes(): motor.move(1.0, wait=True) assert time.time() - start < 1.0 assert motor.readback.get() == pytest.approx(1.0) + + +def test_move_without_in_position_wait(): + motor = _sim_motor(velocity=100.0, tolerance=1e-9) + SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X").settle_time = 5.0 + motor.wait_in_position.put(False) + start = time.time() + status = motor.move(1.0, wait=True) + assert time.time() - start < 0.5 + assert status.success diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index f092da9a..287da0e7 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -207,3 +207,15 @@ def test_control_reply_parsing(): assert control.send("PULSE4.WIDTH=1") == "" with pytest.raises(PandaLineTriggerError, match="No such field"): control.send("PULSE4.FOO?") + + +def test_falling_edge_inverts_the_input(): + device = PandaLineTrigger(name="pandatrig", host="fake-panda", trigger_edge="falling") + device.panda = FakePanda() + device.wait_for_connection() + assert device.panda.fields["LUT8.FUNC"] == "~A|B" + + +def test_invalid_trigger_edge(): + with pytest.raises(ValueError, match="trigger_edge"): + PandaLineTrigger(name="pandatrig", host="fake-panda", trigger_edge="both") diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 2962a448..ede79aeb 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -17,7 +17,7 @@ def _signal(obj_name, kind="config"): return {"obj_name": obj_name, "kind_str": kind, "describe": {"precision": 3}} -def _add_devices(device_manager, capture=True, acceleration=100.0): +def _add_devices(device_manager, capture=True, acceleration=100.0, panda=True): aerox = MockCustomDevice( "aerox", device_info={ @@ -25,16 +25,21 @@ def _add_devices(device_manager, capture=True, acceleration=100.0): "readback": _signal("aerox", "hinted"), "motor_velocity": _signal("aerox_motor_velocity"), "motor_acceleration": _signal("aerox_motor_acceleration"), + "wait_in_position": _signal("aerox_wait_in_position"), } }, signal_read_values={ "aerox": 0.0, "aerox_motor_velocity": 10.0, "aerox_motor_acceleration": acceleration, + "aerox_wait_in_position": True, }, ) aerox.arm_line_trigger = mock.MagicMock() aerox.disarm_line_trigger = mock.MagicMock() + # signal writes go through RPC; mock them, keep the configured values readable + for signal_name, value in (("motor_velocity", 10.0), ("wait_in_position", True)): + setattr(aerox, signal_name, mock.MagicMock(**{"get.return_value": value})) aeroy = MockCustomDevice( "aeroy", device_info={"signals": {"readback": _signal("aeroy", "hinted")}}, @@ -53,6 +58,15 @@ def _add_devices(device_manager, capture=True, acceleration=100.0): signal_read_values={"mcs": 0.0}, ) devices = [aerox, aeroy, ddg1, mcs] + if panda: + pandatrig = MockCustomDevice( + "pandatrig", + device_info={"signals": {"readback": _signal("pandatrig", "hinted")}}, + signal_read_values={"pandatrig": 0.0}, + ) + for method in ("configure_line", "arm", "disarm", "wait_lines_done"): + setattr(pandatrig, method, mock.MagicMock()) + devices.append(pandatrig) if capture: aerocapture = MockCustomDevice( "aerocapture", @@ -68,13 +82,25 @@ def _add_devices(device_manager, capture=True, acceleration=100.0): def _assemble(v4_scan_assembler, device_manager, **kwargs): _add_devices( - device_manager, **{k: kwargs.pop(k) for k in ("capture", "acceleration") if k in kwargs} + device_manager, + **{k: kwargs.pop(k) for k in ("capture", "acceleration", "panda") if k in kwargs}, ) return v4_scan_assembler( "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.5, "aeroy", -2.0, 2.0, 2, exp_time=0.1, **kwargs ) +def _ready_for_core(scan, nth_done_status_mock): + """Prepared scan with mocked motion and trigger receivers.""" + scan.prepare_scan() + scan.fast_axis = mock.MagicMock() + scan.actions.set = mock.MagicMock( + side_effect=lambda *args, **kwargs: nth_done_status_mock(resolve_after=1) + ) + scan.actions.read_monitored_devices = mock.MagicMock() + return scan + + @pytest.mark.parametrize(("hook_name", "hook_tests"), AEROTECH_CONT_GRID_HOOK_TESTS) def test_aerotech_cont_grid_default_hooks( v4_scan_assembler, device_manager, nth_done_status_mock, hook_name, hook_tests @@ -84,25 +110,67 @@ def test_aerotech_cont_grid_default_hooks( def test_line_geometry(v4_scan_assembler, device_manager): - scan = _assemble(v4_scan_assembler, device_manager) + scan = _assemble(v4_scan_assembler, device_manager, settle_time=0.1) scan.prepare_scan() line = scan._line velocity = 0.5 / 0.1 ramp = velocity**2 / (2 * 100.0) - margin = 0.2 * ramp + velocity * 0.05 + margin = 0.2 * ramp + velocity * 0.1 + lead = velocity * 2e-3 # DDG1 shutter delay assert scan.frames_per_line == 5 assert line["num_lines"] == 3 assert line["target_velocity"] == pytest.approx(velocity) - # first exposure starts at fast_start: trigger one shutter delay earlier - assert line["trigger_position"] == pytest.approx(-1.0 - velocity * 2e-3) assert line["exposure_end"] == pytest.approx(-1.0 + 5 * 0.5) - assert line["line_start"] == pytest.approx(line["trigger_position"] - ramp - margin) - assert line["line_end"] == pytest.approx(line["exposure_end"] + ramp + margin) - assert np.array_equal(scan.positions, np.array([[1.0, -2.0], [1.0, 0.0], [1.0, 2.0]])) + # PSO window: entered at the low edge going forward, at the high edge going backward + assert line["trigger_low"] == pytest.approx(-1.0 - lead) + assert line["trigger_high"] == pytest.approx(line["exposure_end"] + lead) + assert line["line_start"] == pytest.approx(line["trigger_low"] - ramp - margin) + assert line["line_end"] == pytest.approx(line["trigger_high"] + ramp + margin) assert scan.scan_info.frames_per_trigger == 5 +def test_snake_positions(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + # last point of each line: forward, backward, forward + assert np.allclose(scan.positions, [[1.0, -2.0], [-1.0, 0.0], [1.0, 2.0]]) + computed = np.asarray(scan.scan_info.additional_scan_parameters["computed_positions"]) + assert np.allclose(computed[:5, 0], [-1.0, -0.5, 0.0, 0.5, 1.0]) + assert np.allclose(computed[5:10, 0], [1.0, 0.5, 0.0, -0.5, -1.0]) + + +def test_raster_positions(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, snake=False) + scan.prepare_scan() + assert np.allclose(scan.positions, [[1.0, -2.0], [1.0, 0.0], [1.0, 2.0]]) + + +def test_shutter_delay_without_ddg(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, trigger_source=None) + scan.prepare_scan() + assert scan._line["shutter_open_delay"] == 0.0 + assert scan._line["trigger_low"] == pytest.approx(-1.0) + assert scan.ddg1 is None and scan.mcs is None + + +def test_shutter_delay_argument(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, trigger_source="panda", shutter_delay=4e-3) + scan.prepare_scan() + assert scan._line["trigger_low"] == pytest.approx(-1.0 - 5.0 * 4e-3) + + +def test_invalid_trigger_source_aborts(v4_scan_assembler, device_manager): + with pytest.raises(ScanAbortion, match="trigger_source"): + _assemble(v4_scan_assembler, device_manager, trigger_source="rio") + + +def test_missing_panda_aborts(v4_scan_assembler, device_manager): + device_manager.devices.pop("pandatrig", None) + with pytest.raises(ScanAbortion, match="pandatrig"): + _assemble(v4_scan_assembler, device_manager, panda=False, trigger_source="panda") + + def test_velocity_above_configured_velocity_aborts(v4_scan_assembler, device_manager): _add_devices(device_manager) scan = v4_scan_assembler( @@ -121,65 +189,182 @@ def test_missing_acceleration_aborts(v4_scan_assembler, device_manager): def test_missing_capture_device_aborts(v4_scan_assembler, device_manager): device_manager.devices.pop("aerocapture", None) with pytest.raises(ScanAbortion, match="aerocapture"): - _assemble(v4_scan_assembler, device_manager, capture=False) + _assemble(v4_scan_assembler, device_manager, capture=False, capture_positions=True) -def test_scan_without_capture(v4_scan_assembler, device_manager): - device_manager.devices.pop("aerocapture", None) - scan = _assemble(v4_scan_assembler, device_manager, capture=False, capture_positions=False) +def test_capture_is_off_by_default(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) assert scan.capture is None -def test_pre_scan_calibrates_before_ddg_goes_external( +def test_pre_scan_arms_once_after_calibration( v4_scan_assembler, device_manager, nth_done_status_mock ): - scan = _assemble(v4_scan_assembler, device_manager) + scan = _assemble(v4_scan_assembler, device_manager, capture_positions=True) scan.prepare_scan() order = mock.MagicMock() scan.capture = order.capture + scan.fast_axis = order.fast_axis scan.ddg1 = order.ddg1 scan._premove_motor_status = nth_done_status_mock(resolve_after=1) scan.actions.pre_scan_all_devices = mock.MagicMock() scan.pre_scan() - assert order.mock_calls[:2] == [mock.call.capture.calibrate(), mock.call.ddg1.set_trigger(1)] + line = scan._line + calls = [c for c in order.mock_calls if not c[0].endswith(".wait")] + assert calls == [ + mock.call.capture.calibrate(), + mock.call.fast_axis.motor_velocity.set(line["target_velocity"]), + mock.call.fast_axis.wait_in_position.set(False), + mock.call.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]), + mock.call.ddg1.set_trigger(2), # falling edge: PSO open collector / differential + ] -def test_at_each_point_arms_trigger_and_reads_capture( +def test_ddg_rising_edge_for_opto_output(v4_scan_assembler, device_manager, nth_done_status_mock): + scan = _assemble(v4_scan_assembler, device_manager, trigger_edge="rising") + scan.prepare_scan() + scan.fast_axis = mock.MagicMock() + scan.ddg1 = mock.MagicMock() + scan._premove_motor_status = nth_done_status_mock(resolve_after=1) + scan.actions.pre_scan_all_devices = mock.MagicMock() + scan.pre_scan() + scan.ddg1.set_trigger.assert_called_once_with(1) + + +def test_invalid_trigger_edge_aborts(v4_scan_assembler, device_manager): + with pytest.raises(ScanAbortion, match="trigger_edge"): + _assemble(v4_scan_assembler, device_manager, trigger_edge="both") + + +def test_pre_scan_configures_and_arms_panda( v4_scan_assembler, device_manager, nth_done_status_mock ): - scan = _assemble(v4_scan_assembler, device_manager) - scan.prepare_scan() - scan.capture = mock.MagicMock() - scan.fast_axis = mock.MagicMock() - scan.actions.set = mock.MagicMock(return_value=nth_done_status_mock(resolve_after=1)) - scan.actions.read_monitored_devices = mock.MagicMock() - - scan.at_each_point( - [scan.fast_axis], - np.array([scan._line["line_end"]]), - status_mcs=nth_done_status_mock(resolve_after=1), + scan = _assemble( + v4_scan_assembler, + device_manager, + trigger_source="panda", + readout_time=0.02, + shutter_delay=0.003, ) + scan.prepare_scan() + scan.fast_axis = mock.MagicMock() + scan.panda = mock.MagicMock() + scan._premove_motor_status = nth_done_status_mock(resolve_after=1) + scan.actions.pre_scan_all_devices = mock.MagicMock() + + scan.pre_scan() + + scan.panda.configure_line.assert_called_once_with(5, 0.1, 0.02, 0.003) + scan.panda.arm.assert_called_once_with() + + +def test_snake_scan_core(v4_scan_assembler, device_manager, nth_done_status_mock): + scan = _ready_for_core( + _assemble(v4_scan_assembler, device_manager, trigger_source=None), nth_done_status_mock + ) + scan.scan_core() + + line = scan._line + moves = [(c.args[0], list(c.args[1])) for c in scan.actions.set.call_args_list] + fast, stepper = scan.fast_axis, scan.stepper_axis + assert moves == [ + ([fast], [line["line_end"]]), + ([stepper], [0.0]), + ([fast], [line["line_start"]]), + ([stepper], [2.0]), + ([fast], [line["line_end"]]), + ] + # the PSO window is armed once in pre_scan, not per line + scan.fast_axis.arm_line_trigger.assert_not_called() + scan.fast_axis.disarm_line_trigger.assert_not_called() + + +def test_raster_scan_core_disarms_pso_for_flyback( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _ready_for_core( + _assemble(v4_scan_assembler, device_manager, trigger_source=None, snake=False), + nth_done_status_mock, + ) + scan.scan_core() + + line = scan._line + targets = [list(c.args[1]) for c in scan.actions.set.call_args_list] + assert targets == [ + [line["line_end"]], + [line["line_start"], 0.0], + [line["line_end"]], + [line["line_start"], 2.0], + [line["line_end"]], + ] + assert scan.fast_axis.disarm_line_trigger.call_count == 2 + assert scan.fast_axis.arm_line_trigger.call_count == 2 + + +def test_panda_lines_are_checked(v4_scan_assembler, device_manager, nth_done_status_mock): + scan = _ready_for_core( + _assemble(v4_scan_assembler, device_manager, trigger_source="panda"), nth_done_status_mock + ) + scan.panda = mock.MagicMock() + scan.scan_core() + + lines = [c.args[0] for c in scan.panda.wait_lines_done.call_args_list] + assert lines == [1, 2, 3] + scan.panda.arm.assert_not_called() # armed once in pre_scan + + +def test_ddg_scan_core_prepares_mcs_per_line( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _ready_for_core(_assemble(v4_scan_assembler, device_manager), nth_done_status_mock) + scan.ddg1 = mock.MagicMock() + scan.mcs = mock.MagicMock() + scan.mcs.get_transition_status.side_effect = lambda **kwargs: nth_done_status_mock( + resolve_after=1 + ) + scan.scan_core() + assert scan.ddg1.prepare_mcs_on_trigger.call_count == 3 + + +def test_at_each_point_reads_capture(v4_scan_assembler, device_manager, nth_done_status_mock): + scan = _ready_for_core( + _assemble(v4_scan_assembler, device_manager, trigger_source=None, capture_positions=True), + nth_done_status_mock, + ) + scan.capture = mock.MagicMock() + + scan.at_each_point([scan.fast_axis], np.array([scan._line["line_end"]]), line_index=0) scan.capture.arm.assert_called_once_with(5) - scan.fast_axis.arm_line_trigger.assert_called_once_with( - scan._line["trigger_position"], scan._line["exposure_end"] - ) - velocity_sets = [c.args[0] for c in scan.fast_axis.motor_velocity.set.call_args_list] - assert velocity_sets == [scan._line["target_velocity"], scan._line["original_velocity"]] - scan.fast_axis.disarm_line_trigger.assert_called_once_with() scan.capture.read_line.assert_called_once_with() -def test_on_exception_restores_ddg_and_pso(v4_scan_assembler, device_manager): +def test_on_exception_restores_ddg_pso_and_axis(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager) scan.prepare_scan() scan.ddg1 = mock.MagicMock() scan.fast_axis = mock.MagicMock() + scan._fast_axis_configured = True scan.on_exception(RuntimeError("test")) scan.ddg1.set_trigger.assert_called_once_with(5) scan.fast_axis.disarm_line_trigger.assert_called_once_with() scan.fast_axis.motor_velocity.put.assert_called_once_with(10.0) + scan.fast_axis.wait_in_position.put.assert_called_once_with(True) + + +def test_restore_continues_after_failing_step(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, trigger_source="panda") + scan.prepare_scan() + scan.panda = mock.MagicMock() + scan.panda.disarm.side_effect = RuntimeError("panda offline") + scan.fast_axis = mock.MagicMock() + scan._fast_axis_configured = True + + with pytest.raises(RuntimeError, match="panda offline"): + scan.on_exception(RuntimeError("test")) + scan.fast_axis.disarm_line_trigger.assert_called_once_with() + scan.fast_axis.wait_in_position.put.assert_called_once_with(True) -- 2.54.0 From 46751e4fccd119855bccb6fd842b936766a13ebf Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 17:07:25 +0100 Subject: [PATCH 11/52] feat(aerotech): aerotech_cont_grid is hardware triggered, MCS armed once scan_type HARDWARE_TRIGGERED: the MCS card and the detectors are armed once at stage for all lines x frames (existing hardware_triggered mode of mcs_card_csaxs / ddg_1) instead of per line; per line the scan only moves the axes. With trigger_source="ddg" each line is still checked: the MCS must have reached the line's last channel, so a missing line trigger aborts after the first line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 8 ++- .../scans/scans_v4/aerotech_cont_grid.py | 56 +++++++++++-------- .../test_aerotech_cont_grid_scan.py | 26 +++++++-- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 8664e6e3..498e19a7 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -69,8 +69,12 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 pre_scan. Its output rises on entering the window: at the low edge on forward lines, at the high edge on backward lines, so in both directions the first exposure starts one shutter delay after the trigger at the first point of the line -- `trigger_source`: `"ddg"` (beamline: PSO -> DDG1 EXT; DDG1/DDG2 take exp_time and - frames_per_trigger = frames per line from the scan info at stage; MCS prepared per line), +- hardware triggered scan: MCS card and detectors are armed once at stage for all + lines x frames (num_points = lines, frames_per_trigger = frames per line); per line the + scan only moves X and Y +- `trigger_source`: `"ddg"` (beamline: PSO -> DDG1 EXT, one DDG1 burst of frames_per_line + gates per trigger; DDG1/DDG2 take exp_time and frames_per_trigger from the scan info at + stage; after each line the scan checks that the MCS reached the line's last channel), `"panda"` (test setup: PandA configured and armed once, every line checked with `wait_lines_done`; `readout_time`, `shutter_delay` arguments) or `None` (no trigger hardware, motion tests) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 4a91de85..94487b70 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -12,15 +12,22 @@ edge on backward lines, so in both directions the first exposure starts one shut after the trigger at the first point of the line. The window is armed once per scan and the trigger receiver stays armed; between lines the scan only moves the axes. +The scan is hardware triggered: the MCS card and the detectors are armed once at stage for +all num_lines x frames_per_line frames (num_points = lines, frames_per_trigger = frames per +line) and checked for completeness in complete. + Trigger sources (trigger_source): - "ddg": PSO output -> DDG1 EXT (beamline). DDG1 runs shutter / detector / MCS, the MCS - card is prepared per line. trigger_edge: the Npaq open collector (J7-8) and + "ddg": PSO output -> DDG1 EXT (beamline). Every trigger starts one DDG1 burst of + frames_per_line gates (shutter / detectors / MCS). After each line the scan + checks that the MCS card counted the line's frames. trigger_edge: the Npaq open collector (J7-8) and differential (J7-1/2) PSO outputs are active low, so the window entry is a falling edge (default); the opto output (J7-6/7) gives a rising edge. "panda": PSO output -> PandABox TTLIN3 (device pandatrig, test setup). The PandA is configured and armed once and sends the shutter pulse and the detector gates of each line. - None: no trigger hardware (motion tests); the PSO window is still armed. + None: no trigger hardware (motion tests); the PSO window is still armed. Only for + sessions without hardware triggered detectors / MCS, they would wait for + frames that never come. With a position capture device (capture_positions=True), the encoder positions are latched at each detector trigger (Npaq UINT1) and published per line. @@ -45,6 +52,7 @@ Scan procedure: from __future__ import annotations +import time from typing import Annotated, TypedDict import numpy as np @@ -87,8 +95,9 @@ class AerotechLineParameter(TypedDict): class AerotechContGrid(ScanBase): - # One line = one point with frames_per_trigger frames, like cont_grid. - scan_type = ScanType.SOFTWARE_TRIGGERED + # One line = one point with frames_per_trigger frames; MCS and detectors are armed once + # for all frames of the scan. + scan_type = ScanType.HARDWARE_TRIGGERED scan_name = "aerotech_cont_grid" gui_config = { @@ -321,18 +330,9 @@ class AerotechContGrid(ScanBase): self.fast_axis.disarm_line_trigger() self.actions.set(self.motors, [line["line_start"], stepper_pos]).wait() self.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]) - status_mcs = None - if self.trigger_source == "ddg": - self.ddg1.prepare_mcs_on_trigger().wait() - status_mcs = self.mcs.get_transition_status( - signal_name="acquiring", transition=[1, 0] - ) target = line["line_end"] if self._is_forward(line_index) else line["line_start"] self.at_each_point( - motors=[self.fast_axis], - positions=np.array([target]), - line_index=line_index, - status_mcs=status_mcs, + motors=[self.fast_axis], positions=np.array([target]), line_index=line_index ) @scan_hook @@ -341,7 +341,6 @@ class AerotechContGrid(ScanBase): motors: list[str | DeviceBase], positions: np.ndarray, line_index: int, - status_mcs=None, last_positions: np.ndarray | None = None, ): """ @@ -359,13 +358,7 @@ class AerotechContGrid(ScanBase): continue if self.trigger_source == "ddg": - try: - status_mcs.wait(timeout=3) - except TimeoutError as exc: - raise ScanAbortion( - "MCS card did not go back to DONE after the line. Was the line trigger" - " received? Check the PSO output -> DDG1 EXT wiring and the PSO interlock." - ) from exc + self._wait_mcs_frames(line_index) elif self.trigger_source == "panda": self.panda.wait_lines_done(line_index + 1, timeout=self._line_duration() + 2) @@ -408,6 +401,23 @@ class AerotechContGrid(ScanBase): def _is_forward(self, line_index: int) -> bool: return not self.snake or line_index % 2 == 0 + def _wait_mcs_frames(self, line_index: int, timeout: float = 2.0, poll: float = 0.05): + """ + After line line_index the MCS card must have advanced to the last channel of that + line (the last channel of the scan is closed in its complete). Catches a missing + line trigger right after the line instead of at the end of the scan. + """ + expected = (line_index + 1) * self.frames_per_line - 1 + deadline = time.time() + timeout + while (channel := self.mcs.current_channel.get()) < expected: + if time.time() > deadline: + raise ScanAbortion( + f"MCS card at channel {channel} after line {line_index}, expected" + f" {expected}. Was the line trigger received? Check the PSO output ->" + " DDG1 EXT wiring and trigger_edge." + ) + time.sleep(poll) + def _line_duration(self) -> float: line = self._line return abs(line["line_end"] - line["line_start"]) / line["target_velocity"] diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index ede79aeb..bfdf8e8c 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -315,17 +315,33 @@ def test_panda_lines_are_checked(v4_scan_assembler, device_manager, nth_done_sta scan.panda.arm.assert_not_called() # armed once in pre_scan -def test_ddg_scan_core_prepares_mcs_per_line( +def test_scan_is_hardware_triggered(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + assert scan.scan_info.scan_type == "hardware_triggered" + # MCS / detectors: num_points lines x frames_per_trigger frames, armed once + assert scan.scan_info.num_points == 3 + assert scan.scan_info.frames_per_trigger == 5 + + +def test_ddg_scan_core_checks_mcs_frames_per_line( v4_scan_assembler, device_manager, nth_done_status_mock ): scan = _ready_for_core(_assemble(v4_scan_assembler, device_manager), nth_done_status_mock) scan.ddg1 = mock.MagicMock() scan.mcs = mock.MagicMock() - scan.mcs.get_transition_status.side_effect = lambda **kwargs: nth_done_status_mock( - resolve_after=1 - ) + scan.mcs.current_channel.get.side_effect = [4, 9, 14] scan.scan_core() - assert scan.ddg1.prepare_mcs_on_trigger.call_count == 3 + scan.ddg1.prepare_mcs_on_trigger.assert_not_called() # no arming per line + assert scan.mcs.current_channel.get.call_count == 3 + + +def test_ddg_missing_line_trigger_aborts(v4_scan_assembler, device_manager, nth_done_status_mock): + scan = _ready_for_core(_assemble(v4_scan_assembler, device_manager), nth_done_status_mock) + scan.mcs = mock.MagicMock() + scan.mcs.current_channel.get.return_value = 0 + with pytest.raises(ScanAbortion, match="expected 4"): + scan._wait_mcs_frames(0, timeout=0.1, poll=0.02) def test_at_each_point_reads_capture(v4_scan_assembler, device_manager, nth_done_status_mock): -- 2.54.0 From 40c8d90072269e863f7ac1b2ac5e56fb14d2a98a Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Wed, 28 Oct 2026 17:30:11 +0100 Subject: [PATCH 12/52] refactor(aerotech): aerotech_cont_grid always runs as a snake Remove the snake argument and the raster path (flyback with PSO disarm/re-arm); snake is the only mode used. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 5 ++-- .../scans/scans_v4/aerotech_cont_grid.py | 18 +++--------- .../test_aerotech_cont_grid_scan.py | 28 ------------------- 3 files changed, 6 insertions(+), 45 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 498e19a7..5e6f3a47 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -59,11 +59,11 @@ Documentation: A3200 6.04.004 help (`A3200.chm`, topic "ASCII Command Interface" ```python scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0.01, - snake=True, trigger_source="ddg", settle_time=0.15) + trigger_source="ddg", settle_time=0.15) ``` - fast axis X moves continuously at `fast_step_size / exp_time`, the step axis Y steps between - lines; `snake=True` (default) alternates the line direction + lines; the lines always run as a snake (alternating direction) - line start triggered by position: one PSO window per scan, `[fast_start - v * shutter_delay, exposure_end + v * shutter_delay]`, armed once in pre_scan. Its output rises on entering the window: at the low edge on forward lines, at @@ -86,7 +86,6 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 ramp at 1 mm/s / 10 mm/s^2). In a snake the run-out of a line is the run-up of the next. - at the turnarounds X does not wait for In Position (`wait_in_position` signal off during the scan); X reaches 1 um within ~60 ms but 10 nm only after 0.5-2 s -- `snake=False`: the flyback passes the PSO window, so the PSO is switched off during it - `capture_positions=True` (default False until commissioned): device `aerocapture` latches the encoder counters on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz with MXR sources); after each line they are read back (~2600 values/s over the ASCII interface) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 94487b70..95fe5976 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -1,8 +1,8 @@ """ Continuous grid scan for Aerotech A3200 stages with a position triggered line start. -The fast axis moves continuously, the step axis steps between lines. By default the lines -run as a snake (alternating direction). The start of each line is triggered in hardware by +The fast axis moves continuously, the step axis steps between lines. The lines always run +as a snake (alternating direction). The start of each line is triggered in hardware by the A3200 PSO output of the fast axis: one PSO window per scan, [fast_start - v * shutter_delay, exposure_end + v * shutter_delay] @@ -114,7 +114,6 @@ class AerotechContGrid(ScanBase): "Acquisition Parameters": [ "exp_time", "relative", - "snake", "trigger_source", "capture_positions", ], @@ -133,7 +132,6 @@ class AerotechContGrid(ScanBase): stepper_step_size: Annotated[float, ScanArgument(display_name="Step Step Size", description="Step size of the step axis in units of the motor.")], exp_time: DefaultArgType.ExposureTime, relative: DefaultArgType.Relative = False, - snake: bool = True, trigger_source: str | None = "ddg", trigger_edge: str = "falling", trigger_device: str = "pandatrig", @@ -161,7 +159,6 @@ class AerotechContGrid(ScanBase): stepper_step_size (float): Step size of the step axis in units of the motor. exp_time (float): Exposure time (period of the detector gates) in seconds. relative (bool): Whether the positions are relative to the current position. Default is False. - snake (bool): Alternate the direction of the lines. Default is True. trigger_source (str | None): "ddg" (beamline, DDG1/MCS), "panda" (PandABox line trigger) or None (no trigger hardware). Default is "ddg". trigger_edge (str): DDG1 external trigger edge, "falling" (PSO open collector / differential output) or "rising" (opto output). Default is "falling". trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". @@ -198,7 +195,6 @@ class AerotechContGrid(ScanBase): self.stepper_step_size = stepper_step_size self.relative = relative self.exp_time = exp_time - self.snake = snake self.trigger_source = trigger_source self.readout_time = readout_time self.shutter_delay = shutter_delay @@ -323,13 +319,7 @@ class AerotechContGrid(ScanBase): for line_index in range(line["num_lines"]): stepper_pos = self.positions[line_index][1] if line_index > 0: - if self.snake: - self.actions.set([self.stepper_axis], [stepper_pos]).wait() - else: - # the flyback passes the PSO window: no line trigger while returning - self.fast_axis.disarm_line_trigger() - self.actions.set(self.motors, [line["line_start"], stepper_pos]).wait() - self.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]) + self.actions.set([self.stepper_axis], [stepper_pos]).wait() target = line["line_end"] if self._is_forward(line_index) else line["line_start"] self.at_each_point( motors=[self.fast_axis], positions=np.array([target]), line_index=line_index @@ -399,7 +389,7 @@ class AerotechContGrid(ScanBase): ####################################################### def _is_forward(self, line_index: int) -> bool: - return not self.snake or line_index % 2 == 0 + return line_index % 2 == 0 def _wait_mcs_frames(self, line_index: int, timeout: float = 2.0, poll: float = 0.05): """ diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index bfdf8e8c..9b516008 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -140,12 +140,6 @@ def test_snake_positions(v4_scan_assembler, device_manager): assert np.allclose(computed[5:10, 0], [1.0, 0.5, 0.0, -0.5, -1.0]) -def test_raster_positions(v4_scan_assembler, device_manager): - scan = _assemble(v4_scan_assembler, device_manager, snake=False) - scan.prepare_scan() - assert np.allclose(scan.positions, [[1.0, -2.0], [1.0, 0.0], [1.0, 2.0]]) - - def test_shutter_delay_without_ddg(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager, trigger_source=None) scan.prepare_scan() @@ -281,28 +275,6 @@ def test_snake_scan_core(v4_scan_assembler, device_manager, nth_done_status_mock scan.fast_axis.disarm_line_trigger.assert_not_called() -def test_raster_scan_core_disarms_pso_for_flyback( - v4_scan_assembler, device_manager, nth_done_status_mock -): - scan = _ready_for_core( - _assemble(v4_scan_assembler, device_manager, trigger_source=None, snake=False), - nth_done_status_mock, - ) - scan.scan_core() - - line = scan._line - targets = [list(c.args[1]) for c in scan.actions.set.call_args_list] - assert targets == [ - [line["line_end"]], - [line["line_start"], 0.0], - [line["line_end"]], - [line["line_start"], 2.0], - [line["line_end"]], - ] - assert scan.fast_axis.disarm_line_trigger.call_count == 2 - assert scan.fast_axis.arm_line_trigger.call_count == 2 - - def test_panda_lines_are_checked(v4_scan_assembler, device_manager, nth_done_status_mock): scan = _ready_for_core( _assemble(v4_scan_assembler, device_manager, trigger_source="panda"), nth_done_status_mock -- 2.54.0 From 834e6ad4dfcdc488b77ee43e08bb6822f956879e Mon Sep 17 00:00:00 2001 From: x01dc Date: Wed, 28 Oct 2026 19:57:05 +0100 Subject: [PATCH 13/52] fix(aerotech): quote aerocapture channel keys so the sastt config loads Integer YAML map keys made the DeviceConfigMessage undecodable on SciHub (msgpack strict_map_key), so update_session_with_file hung until timeout. A3200PositionCapture already converts the keys with int(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/device_configs/sastt_aerotech.yaml | 9 +++++---- csaxs_bec/device_configs/sastt_aerotech_panda.yaml | 9 +++++---- .../simulated_omny/simulated_sastt_aerotech.yaml | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index 40adb73f..b7981b79 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -120,11 +120,12 @@ aerocapture: port: 8000 drive_axis: X # DATAACQ source -> axis: 0-5 local encoder counter, 6-11 MXR channel 0-5 (max. 4 kHz) + # keys are quoted: integer map keys break the config message (msgpack strict_map_key) channels: - 6: X - 11: Z - 2: Y - 9: U + "6": X + "11": Z + "2": Y + "9": U enabled: false onFailure: buffer readOnly: false diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index 039071f8..2bcb4f93 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -110,11 +110,12 @@ aerocapture: port: 8000 drive_axis: X # DATAACQ source -> axis: 0-5 local encoder counter, 6-11 MXR channel 0-5 (max. 4 kHz) + # keys are quoted: integer map keys break the config message (msgpack strict_map_key) channels: - 6: X - 11: Z - 2: Y - 9: U + "6": X + "11": Z + "2": Y + "9": U enabled: false onFailure: buffer readOnly: false diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index e95bd902..7e5d4a2e 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -109,11 +109,12 @@ aerocapture: port: 8000 drive_axis: X # DATAACQ source -> axis: 0-5 local encoder counter, 6-11 MXR channel 0-5 (max. 4 kHz) + # keys are quoted: integer map keys break the config message (msgpack strict_map_key) channels: - 6: X - 11: Z - 2: Y - 9: U + "6": X + "11": Z + "2": Y + "9": U enabled: true onFailure: buffer readOnly: false -- 2.54.0 From d1d63fb947b62b853b56244fca11e77c6b710464 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 09:38:12 +0100 Subject: [PATCH 14/52] fix(aerotech): move settles on the position error, not the In Position bit aerox settles to 10-30 nm within 0.5 s, but the controller's InPositionDistance for X is 10 nm, so In Position took several seconds or flickered and every move logged "not in position 2.0 s after the end of the move". The move now waits until |PositionError| is within the axis tolerance. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/aerotech/a3200.py | 16 +++++++++++----- csaxs_bec/devices/sim/sim_a3200.py | 8 ++++++++ tests/tests_devices/test_a3200.py | 29 +++++++++++++++++++++++------ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index 7abf069b..cb429e25 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -254,6 +254,11 @@ class A3200Controller(Controller): "encoder_clipping": bool(drive_status & DRIVESTATUS_ENCODER_CLIPPING), } + def position_error(self, axis: str) -> float: + """Position command minus position feedback of one axis, in user units.""" + (error,) = self.get_status_items([(axis, "PositionError")]) + return error + @staticmethod def _is_moving(axis_status: int) -> bool: return bool(axis_status & AXISSTATUS_BUSY_MASK) or not axis_status & AXISSTATUS_MOVE_DONE @@ -498,8 +503,9 @@ class A3200Motor(Device, PositionerBase): without waiting for In Position and without the tolerance check (used by scans for the turnarounds of the fast axis). Default True. in_position_timeout: after the motion profile ends, wait this long (s) for the - controller's In Position status (InPositionDistance / InPositionTime) before the - position is compared with the target. + position error to settle within tolerance before the position is compared with + the target. The controller's In Position status is not used: its + InPositionDistance can be tighter than the axis settles (X: 10 nm). acceleration: ramp rate for moves in user units/s^2; None keeps the controller's ramp settings (DefaultRampRate). pso_input: PSO input of this axis, needed for the PSO line trigger @@ -668,11 +674,11 @@ class A3200Motor(Device, PositionerBase): return # the profile is done, the axis may still be settling deadline = time.time() + self.in_position_timeout - while not self.controller.axis_status(self.axis_Id)["in_position"]: + while abs(self.controller.position_error(self.axis_Id)) > self.tolerance: if time.time() > deadline: logger.warning( - f"{self.name}: not in position {self.in_position_timeout} s" - " after the end of the move." + f"{self.name}: position error not within {self.tolerance}" + f" {self.egu} {self.in_position_timeout} s after the end of the move." ) break time.sleep(0.02) diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index 7d8f637e..a245ba7f 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -60,6 +60,7 @@ class _SimA3200Axis: self.counter_offset = 0 self.ramp_rate = None self.settle_time = 0.0 # In Position this long after the end of a move + self.settle_error = 0.0 # position error during settle_time self.pos = 0.0 self._move_start = 0.0 self._move_target = 0.0 @@ -106,6 +107,11 @@ class _SimA3200Axis: status |= AXISSTATUS_MOVE_DONE return status + def position_error(self) -> float: + if self.is_moving() or time.time() < self._move_t1 + self.settle_time: + return self.settle_error + return 0.0 + def drive_status(self) -> int: status = DRIVESTATUS_ENABLED if self.enabled else 0 if not self.is_moving() and time.time() >= self._move_t1 + self.settle_time: @@ -383,6 +389,8 @@ class SimA3200Socket(SimSocketBase): "ProgramPositionCommand", ): return f"{ax.position():.6f}" + if data_item == "PositionError": + return f"{ax.position_error():.9f}" if data_item == "AxisStatus": return str(ax.axis_status()) if data_item == "DriveStatus": diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index 77b0f102..dc0a5ab4 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -339,18 +339,35 @@ def test_capture_rejects_analog_sources(): ) -def test_move_waits_for_in_position(): - motor = _sim_motor(velocity=100.0) - SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X").settle_time = 0.3 +def test_move_waits_for_position_error_within_tolerance(): + motor = _sim_motor(velocity=100.0, tolerance=0.001) + axis = SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X") + axis.settle_time = 0.3 + axis.settle_error = 0.01 start = time.time() motor.move(1.0, wait=True) assert time.time() - start >= 0.3 - assert motor.status()["in_position"] + assert motor.controller.position_error("X") == 0.0 -def test_move_not_in_position_still_finishes(): +def test_move_ignores_in_position_tighter_than_tolerance(): + # settled within tolerance, but outside the controller's InPositionDistance + motor = _sim_motor(velocity=100.0, tolerance=0.001) + axis = SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X") + axis.settle_time = 5.0 + axis.settle_error = 2e-5 + start = time.time() + status = motor.move(1.0, wait=True) + assert time.time() - start < 0.5 + assert status.success + assert not motor.status()["in_position"] + + +def test_move_not_settled_still_finishes(): motor = _sim_motor(velocity=100.0, in_position_timeout=0.1) - SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X").settle_time = 5.0 + axis = SimStateRegistry.get(SimA3200State, _HOST, _PORT).axis("X") + axis.settle_time = 5.0 + axis.settle_error = 0.01 start = time.time() motor.move(1.0, wait=True) assert time.time() - start < 1.0 -- 2.54.0 From 2944da560952a9456c3790b645d6474f6234f718 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 11:11:59 +0100 Subject: [PATCH 15/52] docs(aerotech): beamline examples in the aerotech_cont_grid docstring Largest (3.5 x 3.0 mm) and smallest (0.8 x 1.2 mm) field of view of the last beamtime, each as overview, quick overview and high resolution scan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../scans/scans_v4/aerotech_cont_grid.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 95fe5976..14180af7 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -170,6 +170,30 @@ class AerotechContGrid(ScanBase): Returns: ScanReport + + Examples: + Beamline (trigger_source="ddg"), X fast, Y step, field of view centered on the + current position (relative=True). Scan velocity = fast_step_size / exp_time, it + must not exceed the velocity of aerox (sastt_aerotech.yaml: 5 mm/s), so the + 20 mm/s quick overviews need a higher aerox velocity. + + Largest field of view, 3.5 mm x 3.0 mm (horizontal x vertical): + + >>> # overview, good statistics: 40 um, 20 ms (2 mm/s), 89 x 76 points + >>> scans.aerotech_cont_grid(dev.aerox, -1.75, 1.75, 0.04, dev.aeroy, -1.5, 1.5, 0.04, exp_time=0.02, relative=True) + >>> # quick overview: 100 um, 5 ms (20 mm/s), 36 x 31 points + >>> scans.aerotech_cont_grid(dev.aerox, -1.75, 1.75, 0.1, dev.aeroy, -1.5, 1.5, 0.1, exp_time=0.005, relative=True) + >>> # high resolution, good statistics: 10 um, 20 ms (0.5 mm/s), 351 x 301 points + >>> scans.aerotech_cont_grid(dev.aerox, -1.75, 1.75, 0.01, dev.aeroy, -1.5, 1.5, 0.01, exp_time=0.02, relative=True) + + Smallest field of view, 0.8 mm x 1.2 mm (horizontal x vertical): + + >>> # overview, good statistics: 40 um, 20 ms (2 mm/s), 21 x 31 points + >>> scans.aerotech_cont_grid(dev.aerox, -0.4, 0.4, 0.04, dev.aeroy, -0.6, 0.6, 0.04, exp_time=0.02, relative=True) + >>> # quick overview: 100 um, 5 ms (20 mm/s), 9 x 13 points + >>> scans.aerotech_cont_grid(dev.aerox, -0.4, 0.4, 0.1, dev.aeroy, -0.6, 0.6, 0.1, exp_time=0.005, relative=True) + >>> # high resolution, good statistics: 10 um, 20 ms (0.5 mm/s), 81 x 121 points + >>> scans.aerotech_cont_grid(dev.aerox, -0.4, 0.4, 0.01, dev.aeroy, -0.6, 0.6, 0.01, exp_time=0.02, relative=True) """ super().__init__(**kwargs) self.components = CsaxsBecScanComponents(self) -- 2.54.0 From 1da6330d3c2ae99923e8c764f23ee62cb886de17 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 11:19:05 +0100 Subject: [PATCH 16/52] feat(aerotech): scan velocity up to max_scan_velocity, run-up checked against limits - A3200Motor.max_scan_velocity (default: velocity) limits the scan velocity of aerotech_cont_grid; the move velocity is restored after the scan, also after an aborted scan - run-up / run-out: 2x ramp distance + v * settle_time (was 1.2x ramp) - the full line range including run-up / run-out and the step axis range are checked against the soft limits in prepare_scan - aerox: acceleration 500 mm/s^2, max_scan_velocity 50 mm/s, moves stay 5 mm/s Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/device_configs/sastt_aerotech.yaml | 6 +- .../device_configs/sastt_aerotech_panda.yaml | 3 +- .../simulated_sastt_aerotech.yaml | 3 +- csaxs_bec/devices/aerotech/README.md | 3 +- csaxs_bec/devices/aerotech/a3200.py | 6 ++ csaxs_bec/devices/sim/sim_a3200.py | 2 + .../scans/scans_v4/aerotech_cont_grid.py | 45 +++++++++++---- tests/tests_devices/test_a3200.py | 5 ++ .../test_aerotech_cont_grid_scan.py | 55 +++++++++++++++++-- 9 files changed, 108 insertions(+), 20 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index b7981b79..0342b3bc 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -12,7 +12,8 @@ # controller soft limits: X -50..150, Z -100..100, Y -10..10 mm (active after home) # tolerance: X 1 um, Z/Y 5 um (InPositionDistance X 10 nm, Z/Y 5 um) # velocity / acceleration below = DefaultSpeed / DefaultRampRate (MaxSpeedClamp: -# X 200, Z 10, Y 10 mm/s, U 720 deg/s) +# X 200, Z 10, Y 10 mm/s, U 720 deg/s), except X: acceleration 500 mm/s^2 (short +# run-up in aerotech_cont_grid), scans up to max_scan_velocity 50 mm/s # Wiring for aerotech_cont_grid: PSO output (J7 "PSO INTERFACE") -> DDG1 EXT, # detector trigger -> UINT1 (J12 "MISC I/O"). # UU (ADRS-150 slipring follower) follows U as gantry slave and is not a BEC device. @@ -26,10 +27,11 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 10.0 + acceleration: 500.0 host: pc14643.psi.ch port: 8000 velocity: 5.0 + max_scan_velocity: 50.0 egu: mm tolerance: 0.001 limits: diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index 2bcb4f93..22ce45c3 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -16,10 +16,11 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 10.0 + acceleration: 500.0 host: pc14643.psi.ch port: 8000 velocity: 5.0 + max_scan_velocity: 50.0 egu: mm tolerance: 0.001 limits: diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index 7e5d4a2e..f1015d98 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -12,10 +12,11 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 10.0 + acceleration: 500.0 host: sim-a3200-saxstt port: 8000 velocity: 5.0 + max_scan_velocity: 50.0 egu: mm tolerance: 0.001 limits: diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 5e6f3a47..96294d9a 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -123,7 +123,8 @@ Commissioning of the scan: only; open collector and differential are active low). The PSO interlock is defeated (`IOSetup` 0x80). 3. `acceleration` of the fast axis = the ramp rate BEC sets before each move. -4. `velocity` of the fast axis is the maximum scan velocity. +4. `max_scan_velocity` of the fast axis is the maximum scan velocity (default: `velocity`, + the move velocity, which the scan restores afterwards). 5. `GlobalDoubles` >= 255 (SAXS-TT: 255): BEC uses `$global[0..199]` for array reads and `$global[254]` as scratch variable. Another client must not use them during a scan. 6. Check one line with the scope: PSO edge at the trigger position, number of UINT1 edges diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index cb429e25..bae945fd 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -508,6 +508,9 @@ class A3200Motor(Device, PositionerBase): InPositionDistance can be tighter than the axis settles (X: 10 nm). acceleration: ramp rate for moves in user units/s^2; None keeps the controller's ramp settings (DefaultRampRate). + max_scan_velocity: highest velocity a continuous scan may set on this axis, in user + units per second; None: velocity. The scan restores velocity afterwards, also + after an aborted scan. pso_input: PSO input of this axis, needed for the PSO line trigger (arm_line_trigger). Npaq: 0-5 local encoder counter (square wave encoders), 6-8 MXR emulated quadrature (sine encoders, see EmulatedQuadratureChannel). @@ -531,6 +534,7 @@ class A3200Motor(Device, PositionerBase): homed = Cpt(A3200AxisHomed, signal_name="homed", kind="config") motor_velocity = Cpt(Signal, value=1.0, kind="config") motor_acceleration = Cpt(Signal, value=0.0, kind="config") + max_scan_velocity = Cpt(Signal, value=1.0, kind="config") wait_in_position = Cpt(Signal, value=True, kind="config") high_limit_travel = Cpt(Signal, value=0, kind="omitted") low_limit_travel = Cpt(Signal, value=0, kind="omitted") @@ -560,6 +564,7 @@ class A3200Motor(Device, PositionerBase): in_position_timeout=2.0, require_homed=True, acceleration=None, + max_scan_velocity=None, pso_input=None, pso_counts_divider=1, socket_cls=SocketIO, @@ -593,6 +598,7 @@ class A3200Motor(Device, PositionerBase): self.readback.name = self.name self.motor_velocity.put(velocity) self.motor_acceleration.put(acceleration or 0.0) + self.max_scan_velocity.put(max_scan_velocity or velocity) self.controller.subscribe( self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE ) diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index a245ba7f..acd03e82 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -424,6 +424,7 @@ class SimA3200Motor(A3200Motor): tolerance=0.001, require_homed=True, acceleration=None, + max_scan_velocity=None, pso_input=None, pso_counts_divider=1, device_manager=None, @@ -449,6 +450,7 @@ class SimA3200Motor(A3200Motor): tolerance=tolerance, require_homed=require_homed, acceleration=acceleration, + max_scan_velocity=max_scan_velocity, pso_input=pso_input, pso_counts_divider=pso_counts_divider, socket_cls=SimA3200Socket, diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 14180af7..e59361ef 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -32,8 +32,11 @@ Trigger sources (trigger_source): With a position capture device (capture_positions=True), the encoder positions are latched at each detector trigger (Npaq UINT1) and published per line. -Run-up / run-out of each line: ramp distance v^2 / (2 * acceleration) + 20 % + v * -settle_time beyond the trigger positions. In a snake the run-out of a line is the run-up of +Run-up / run-out of each line: twice the ramp distance v^2 / (2 * acceleration) + v * +settle_time beyond the trigger positions. The whole range including run-up / run-out is +checked against the soft limits before the scan starts. The scan velocity may be higher +than the normal move velocity, up to max_scan_velocity of the fast axis; the move velocity +is restored after the scan, also after an aborted scan. In a snake the run-out of a line is the run-up of the next one, so both sides are equal. At the turnarounds the fast axis does not wait for In Position. @@ -73,8 +76,9 @@ DDG_EXT_FALLING_EDGE = 2 DDG_SINGLE_SHOT = 5 DDG_TRIGGER_EDGES = {"rising": DDG_EXT_RISING_EDGE, "falling": DDG_EXT_FALLING_EDGE} -# Extra run-up / run-out beyond the ramp distance, as a fraction of it -RAMP_MARGIN_FRACTION = 0.2 +# Extra run-up / run-out beyond the ramp distance, as a fraction of it (headroom for the +# servo following error at the end of the ramp) +RAMP_MARGIN_FRACTION = 1.0 TRIGGER_SOURCES = ("ddg", "panda", None) @@ -173,9 +177,8 @@ class AerotechContGrid(ScanBase): Examples: Beamline (trigger_source="ddg"), X fast, Y step, field of view centered on the - current position (relative=True). Scan velocity = fast_step_size / exp_time, it - must not exceed the velocity of aerox (sastt_aerotech.yaml: 5 mm/s), so the - 20 mm/s quick overviews need a higher aerox velocity. + current position (relative=True). Scan velocity = fast_step_size / exp_time, at + most max_scan_velocity of aerox (sastt_aerotech.yaml: 50 mm/s; moves: 5 mm/s). Largest field of view, 3.5 mm x 3.0 mm (horizontal x vertical): @@ -280,6 +283,7 @@ class AerotechContGrid(ScanBase): positions = np.array(positions) self._compute_line_geometry() + self._check_line_limits() # One readout per line: the last point of each line, as in cont_grid self.positions = positions[(self.frames_per_line - 1) :: self.frames_per_line, :].copy() @@ -461,10 +465,11 @@ class AerotechContGrid(ScanBase): line["acceleration"] = self.fast_axis.motor_acceleration.get() velocity = self.fast_step_size / self.exp_time - if velocity > line["original_velocity"]: + max_velocity = self.fast_axis.max_scan_velocity.get() + if velocity > max_velocity: raise ScanAbortion( - f"Requested velocity of {velocity} exceeds the velocity {line['original_velocity']}" - f" configured for {self.fast_axis.name}." + f"Requested velocity of {velocity} (fast_step_size / exp_time) exceeds the" + f" maximum scan velocity {max_velocity} configured for {self.fast_axis.name}." ) if not line["acceleration"] or line["acceleration"] <= 0: raise ScanAbortion( @@ -482,6 +487,26 @@ class AerotechContGrid(ScanBase): line["line_start"] = line["trigger_low"] - ramp_distance - margin line["line_end"] = line["trigger_high"] + ramp_distance + margin + def _check_line_limits(self): + """The lines including run-up / run-out and the step axis range must fit the limits.""" + line = self._line + low, high = self.fast_axis.limits + if low < high and (line["line_start"] < low or line["line_end"] > high): + run_up = line["trigger_low"] - line["line_start"] + raise ScanAbortion( + f"{self.fast_axis.name} range {line['line_start']:.4f} .. {line['line_end']:.4f}" + f" (including {run_up:.4f} run-up / run-out on each side at" + f" {line['target_velocity']} / s, acceleration {line['acceleration']} / s^2)" + f" is outside the limits ({low}, {high})." + ) + low, high = self.stepper_axis.limits + stepper_range = sorted((self.stepper_start, self.stepper_stop)) + if low < high and (stepper_range[0] < low or stepper_range[1] > high): + raise ScanAbortion( + f"{self.stepper_axis.name} range {stepper_range[0]:.4f} .. {stepper_range[1]:.4f}" + f" is outside the limits ({low}, {high})." + ) + def _restore(self): """Undo the scan settings; every step is tried even if an earlier one fails.""" steps = [] diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index dc0a5ab4..4ea4e2e9 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -382,3 +382,8 @@ def test_move_without_in_position_wait(): status = motor.move(1.0, wait=True) assert time.time() - start < 0.5 assert status.success + + +def test_max_scan_velocity_defaults_to_velocity(): + assert _sim_motor(velocity=5.0).max_scan_velocity.get() == 5.0 + assert _sim_motor(velocity=5.0, max_scan_velocity=50.0).max_scan_velocity.get() == 50.0 diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 9b516008..a7e4dcbe 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -17,7 +17,9 @@ def _signal(obj_name, kind="config"): return {"obj_name": obj_name, "kind_str": kind, "describe": {"precision": 3}} -def _add_devices(device_manager, capture=True, acceleration=100.0, panda=True): +def _add_devices( + device_manager, capture=True, acceleration=100.0, panda=True, max_scan_velocity=10.0 +): aerox = MockCustomDevice( "aerox", device_info={ @@ -38,7 +40,11 @@ def _add_devices(device_manager, capture=True, acceleration=100.0, panda=True): aerox.arm_line_trigger = mock.MagicMock() aerox.disarm_line_trigger = mock.MagicMock() # signal writes go through RPC; mock them, keep the configured values readable - for signal_name, value in (("motor_velocity", 10.0), ("wait_in_position", True)): + for signal_name, value in ( + ("motor_velocity", 10.0), + ("max_scan_velocity", max_scan_velocity), + ("wait_in_position", True), + ): setattr(aerox, signal_name, mock.MagicMock(**{"get.return_value": value})) aeroy = MockCustomDevice( "aeroy", @@ -83,7 +89,11 @@ def _add_devices(device_manager, capture=True, acceleration=100.0, panda=True): def _assemble(v4_scan_assembler, device_manager, **kwargs): _add_devices( device_manager, - **{k: kwargs.pop(k) for k in ("capture", "acceleration", "panda") if k in kwargs}, + **{ + k: kwargs.pop(k) + for k in ("capture", "acceleration", "panda", "max_scan_velocity") + if k in kwargs + }, ) return v4_scan_assembler( "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.5, "aeroy", -2.0, 2.0, 2, exp_time=0.1, **kwargs @@ -116,7 +126,7 @@ def test_line_geometry(v4_scan_assembler, device_manager): velocity = 0.5 / 0.1 ramp = velocity**2 / (2 * 100.0) - margin = 0.2 * ramp + velocity * 0.1 + margin = 1.0 * ramp + velocity * 0.1 lead = velocity * 2e-3 # DDG1 shutter delay assert scan.frames_per_line == 5 assert line["num_lines"] == 3 @@ -170,7 +180,42 @@ def test_velocity_above_configured_velocity_aborts(v4_scan_assembler, device_man scan = v4_scan_assembler( "aerotech_cont_grid", "aerox", -10.0, 10.0, 2.0, "aeroy", -2.0, 2.0, 2, exp_time=0.1 ) - with pytest.raises(ScanAbortion, match="exceeds the velocity"): + with pytest.raises(ScanAbortion, match="exceeds the maximum scan velocity"): + scan.prepare_scan() + + +def test_scan_faster_than_move_velocity_restores_move_velocity(v4_scan_assembler, device_manager): + # 20 mm/s scan, move velocity 10 mm/s, max_scan_velocity 50 mm/s + _add_devices(device_manager, max_scan_velocity=50.0) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.1, "aeroy", -2.0, 2.0, 2, exp_time=0.005 + ) + scan.prepare_scan() + assert scan._line["target_velocity"] == pytest.approx(20.0) + scan.fast_axis = mock.MagicMock() + scan._fast_axis_configured = True + + scan.on_exception(RuntimeError("aborted")) + + scan.fast_axis.motor_velocity.put.assert_called_once_with(10.0) + + +def test_run_up_outside_limits_aborts(v4_scan_assembler, device_manager): + # 20 mm/s at 100 mm/s^2: 2 * 2 mm ramp + 3 mm settle run-up, beyond the +-10 limits + _add_devices(device_manager, max_scan_velocity=50.0) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -4.0, 4.0, 0.1, "aeroy", -2.0, 2.0, 2, exp_time=0.005 + ) + with pytest.raises(ScanAbortion, match="run-up"): + scan.prepare_scan() + + +def test_stepper_outside_limits_aborts(v4_scan_assembler, device_manager): + _add_devices(device_manager) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.5, "aeroy", -2.0, 12.0, 2, exp_time=0.1 + ) + with pytest.raises(ScanAbortion, match="aeroy range"): scan.prepare_scan() -- 2.54.0 From b85f8f9c38fe3efb482c54bb440fc6bf534c1052 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 11:17:09 +0100 Subject: [PATCH 17/52] fix(panda): wait for the late replayed PULSE train after a parameter change The PULSE blocks replay a full train ~0.3-0.6 s after a parameter write, without a trigger. configure_line returned after 50 ms, so the train ran into the first line and blocked its trigger (line counter 0). Now both PULSE outputs must stay low for PARAM_QUIET_TIME (1 s); only on a parameter change, i.e. once per scan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 3 ++- .../devices/panda_box/panda_line_trigger.py | 27 ++++++++++--------- .../tests_devices/test_panda_line_trigger.py | 22 +++++++++++++++ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 96294d9a..7e7b9f5a 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -165,5 +165,6 @@ edges. PandA FPGA 4.1 PULSE block: an ENABLE edge, a parameter write or a dropped trigger often makes it replay the previous pulse train without a trigger. The device keeps the PULSE blocks enabled, writes their parameters only when they change (once per scan) and then waits -until such a train is over (pulses can appear on TTLOUT2/3 meanwhile), and the trigger path +until such a train is over (pulses can appear on TTLOUT2/3 meanwhile; the train starts only +~0.3-0.6 s after the write, so both outputs must stay low for 1 s), and the trigger path filters out edges during a line, so no trigger is ever dropped by a PULSE block. diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 3b887789..e4336836 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -22,7 +22,9 @@ any trigger. With unchanged parameters and ENABLE kept high, hundreds of trigger a row were correct. So the PULSE blocks stay enabled, their parameters are only written when they change (once per scan), followed by a wait until a replayed train is over, and edges during a line are filtered out before the PULSE blocks. Replayed trains during a -parameter change do appear on TTLOUT2/3. +parameter change do appear on TTLOUT2/3. The replayed train starts only ~0.3-0.6 s after +the write (seen 2026-10-29), so configure_line waits until both PULSE outputs stayed low +for PARAM_QUIET_TIME. Only the control port (8888) is used: the fields are written by setup_layout() at connect, the PCAP data capture is not touched. Timing resolution 8 ns (125 MHz FPGA clock). @@ -129,6 +131,8 @@ class PandaLineTrigger(Device): TRIGGER_COUNTER = "COUNTER6" LINE_COUNTER = "COUNTER7" PULSE_COUNTER = "COUNTER8" + # a replayed train starts up to ~0.6 s after a parameter write + PARAM_QUIET_TIME = 1.0 SHUTTER_OUTPUT = "TTLOUT2" DETECTOR_OUTPUT = "TTLOUT3" @@ -282,25 +286,22 @@ class PandaLineTrigger(Device): self._pulse_params = params def _wait_pulses_idle(self, timeout: float) -> None: - """Wait until a replayed train is over: stable counter and both PULSE outputs low.""" + """Wait until a replayed train is over: both PULSE outputs low for PARAM_QUIET_TIME.""" sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE - deadline = time.time() + timeout - last = None + deadline = time.time() + timeout + self.PARAM_QUIET_TIME + last_active = time.time() while True: - time.sleep(0.02) - state = ( - self.panda.get(f"{self.PULSE_COUNTER}.OUT"), - self.panda.get(f"{sp}.OUT"), - self.panda.get(f"{dp}.OUT"), - ) - if state == last and state[1:] == ("0", "0"): + time.sleep(0.005) + now = time.time() + if self.panda.get(f"{sp}.OUT") != "0" or self.panda.get(f"{dp}.OUT") != "0": + last_active = now + elif now - last_active >= self.PARAM_QUIET_TIME: return - if time.time() > deadline: + if now > deadline: raise PandaLineTriggerError( f"{self.name}: PULSE blocks still active {timeout:.1f} s after a parameter" " change." ) - last = state def arm(self, one_shot: bool = False) -> None: """ diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 287da0e7..4a9dfb53 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -52,6 +52,7 @@ class FakePanda(PandaControl): @pytest.fixture def trigger(): device = PandaLineTrigger(name="pandatrig", host="fake-panda") + device.PARAM_QUIET_TIME = 0.01 device.panda = FakePanda() device.wait_for_connection() return device @@ -219,3 +220,24 @@ def test_falling_edge_inverts_the_input(): def test_invalid_trigger_edge(): with pytest.raises(ValueError, match="trigger_edge"): PandaLineTrigger(name="pandatrig", host="fake-panda", trigger_edge="both") + + +def test_configure_line_waits_for_a_late_replayed_train(trigger): + """The replayed train starts only after the write; configure_line must outlast it.""" + import threading + import time + + trigger.PARAM_QUIET_TIME = 0.2 + fields = trigger.panda.fields + + def replay(): + time.sleep(0.05) + fields["PULSE4.OUT"] = "1" + time.sleep(0.05) + fields["PULSE4.OUT"] = "0" + + threading.Thread(target=replay, daemon=True).start() + t0 = time.time() + trigger.configure_line(5, 0.01) + # the train ended at ~0.1 s, then 0.2 s quiet + assert time.time() - t0 >= 0.28 -- 2.54.0 From 37d8605aaf6246b6bcde6dcc2d815bac451d9f3c Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 11:17:09 +0100 Subject: [PATCH 18/52] fix(aerotech): PSO window open beyond the line end, range switched per line With one closed window per scan the window exit coincided with the end of the exposures; encoder noise at the exit edge gave extra trigger edges after the train (2 in 11 lines on the PandA, one started an extra line). Now the window is open on the far side of the line and only its range is switched before each line (PSOWINDOW RANGE, no reload, so the position reference of the arm stays). Hardware test: 2 x 11 lines, 22 edges, 22 lines, 2222 gates, 0 dropped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 15 ++++++---- csaxs_bec/devices/aerotech/a3200.py | 29 +++++++++++++++--- .../scans/scans_v4/aerotech_cont_grid.py | 30 ++++++++++++++----- tests/tests_devices/test_a3200.py | 22 ++++++++++++++ .../test_aerotech_cont_grid_scan.py | 11 +++++-- 5 files changed, 89 insertions(+), 18 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 7e7b9f5a..a513a78f 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -64,11 +64,16 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 - fast axis X moves continuously at `fast_step_size / exp_time`, the step axis Y steps between lines; the lines always run as a snake (alternating direction) -- line start triggered by position: one PSO window per scan, - `[fast_start - v * shutter_delay, exposure_end + v * shutter_delay]`, armed once in - pre_scan. Its output rises on entering the window: at the low edge on forward lines, at - the high edge on backward lines, so in both directions the first exposure starts one - shutter delay after the trigger at the first point of the line +- line start triggered by position: the PSO output rises on entering the window, at + `trigger_low = fast_start - v * shutter_delay` on forward lines and at + `trigger_high = exposure_end + v * shutter_delay` on backward lines, so in both directions + the first exposure starts one shutter delay after the trigger at the first point of the + line. The window is open on the far side (`[trigger_low, beyond line_end]` forward, + `[beyond line_start, trigger_high]` backward): the axis never leaves it during a line. + With a closed window the exit coincides with the end of the exposures and encoder noise + at the exit edge started extra lines (2026-10-29, PandA: 2 extra edges in 11 lines). + Armed once in pre_scan (one position reference); before each line only the range is + switched (`PSOWINDOW RANGE`, no reload), with the axis outside both windows - hardware triggered scan: MCS card and detectors are armed once at stage for all lines x frames (num_points = lines, frames_per_trigger = frames per line); per line the scan only moves X and Y diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index bae945fd..a4f35d3b 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -146,6 +146,7 @@ class A3200Controller(Controller): if not hasattr(self, "_rx_buffer"): self._rx_buffer = "" self._counts_per_unit = {} + self._pso_reference = {} # axis -> (position at PSOWINDOW LOAD, PSO counts per unit) @threadlocked def socket_put(self, val: str) -> None: @@ -357,10 +358,8 @@ class A3200Controller(Controller): the EmulatedQuadratureDivider for the MXR emulated quadrature inputs 6-8. """ cpu = self.counts_per_unit(axis) / counts_divider - current = self.get_position(axis) - low_counts, high_counts = sorted( - (int(round((low - current) * cpu)), int(round((high - current) * cpu))) - ) + self._pso_reference[axis] = (self.get_position(axis), cpu) + low_counts, high_counts = self._pso_window_counts(axis, low, high) self.command(f"PSOCONTROL {axis} RESET") self.command(f"PSOWINDOW {axis} 1 INPUT {pso_input}") self.command(f"PSOWINDOW {axis} 1 LOAD 0") @@ -368,9 +367,27 @@ class A3200Controller(Controller): self.command(f"PSOOUTPUT {axis} WINDOW") self.command(f"PSOCONTROL {axis} ARM") + def pso_window_range(self, axis: str, low: float, high: float) -> None: + """ + Move the armed window to [low, high] (user units, absolute). The window counter is + not reloaded: the position reference of pso_arm_window stays valid, the axis may be + in motion. + """ + if axis not in self._pso_reference: + raise A3200Error(f"PSO window of axis {axis} is not armed.") + low_counts, high_counts = self._pso_window_counts(axis, low, high) + self.command(f"PSOWINDOW {axis} 1 RANGE {low_counts}, {high_counts}") + + def _pso_window_counts(self, axis: str, low: float, high: float) -> tuple[int, int]: + reference, cpu = self._pso_reference[axis] + return tuple( + sorted((int(round((low - reference) * cpu)), int(round((high - reference) * cpu)))) + ) + def pso_off(self, axis: str) -> None: self.command(f"PSOWINDOW {axis} 1 OFF") self.command(f"PSOCONTROL {axis} OFF") + self._pso_reference.pop(axis, None) # --- DATAACQ (position capture into the drive array) ----------------------- @@ -743,6 +760,10 @@ class A3200Motor(Device, PositionerBase): counts_divider=self.pso_counts_divider, ) + def set_line_trigger_window(self, start: float, end: float) -> None: + """Move the window of the armed line trigger to [start, end], keeping its reference.""" + self.controller.pso_window_range(self.axis_Id, start * self.sign, end * self.sign) + def disarm_line_trigger(self) -> None: self.controller.pso_off(self.axis_Id) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index e59361ef..db384575 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -3,14 +3,20 @@ Continuous grid scan for Aerotech A3200 stages with a position triggered line st The fast axis moves continuously, the step axis steps between lines. The lines always run as a snake (alternating direction). The start of each line is triggered in hardware by -the A3200 PSO output of the fast axis: one PSO window per scan, +the A3200 PSO output of the fast axis, which goes high when the axis enters the PSO window. +The trigger positions are - [fast_start - v * shutter_delay, exposure_end + v * shutter_delay] + trigger_low = fast_start - v * shutter_delay, trigger_high = exposure_end + v * shutter_delay -goes high when the axis enters it, i.e. at the low edge on forward lines and at the high -edge on backward lines, so in both directions the first exposure starts one shutter delay -after the trigger at the first point of the line. The window is armed once per scan and -the trigger receiver stays armed; between lines the scan only moves the axes. +and the window is open on the far side of the line: [trigger_low, beyond line_end] on +forward lines, [beyond line_start, trigger_high] on backward lines, so in both directions +the first exposure starts one shutter delay after the trigger at the first point of the +line. The axis never leaves the window during a line: with a closed window the exit +coincides with the end of the exposures, and encoder noise at the exit edge (seen +2026-10-29: 2 extra edges in 11 lines) could start an extra line in the trigger receiver. +Noise at the entry edge falls into the running line, which DDG and PandA ignore. The window +is armed once per scan (one position reference) and only its range is switched before each +line, while the axis is outside both windows; the trigger receiver stays armed. The scan is hardware triggered: the MCS card and the detectors are armed once at stage for all num_lines x frames_per_line frames (num_points = lines, frames_per_trigger = frames per @@ -330,7 +336,7 @@ class AerotechContGrid(ScanBase): self._fast_axis_configured = True self.fast_axis.motor_velocity.set(line["target_velocity"]).wait(timeout=5) self.fast_axis.wait_in_position.set(False).wait(timeout=5) - self.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]) + self.fast_axis.arm_line_trigger(*self._trigger_window(0)) if self.trigger_source == "ddg": self.ddg1.set_trigger(DDG_TRIGGER_EDGES[self.trigger_edge]) elif self.trigger_source == "panda": @@ -347,6 +353,8 @@ class AerotechContGrid(ScanBase): for line_index in range(line["num_lines"]): stepper_pos = self.positions[line_index][1] if line_index > 0: + # fast axis at the turnaround, outside the window of the next line + self.fast_axis.set_line_trigger_window(*self._trigger_window(line_index)) self.actions.set([self.stepper_axis], [stepper_pos]).wait() target = line["line_end"] if self._is_forward(line_index) else line["line_start"] self.at_each_point( @@ -419,6 +427,14 @@ class AerotechContGrid(ScanBase): def _is_forward(self, line_index: int) -> bool: return line_index % 2 == 0 + def _trigger_window(self, line_index: int) -> tuple[float, float]: + """PSO window of a line: entered at the trigger position, open beyond the line end.""" + line = self._line + span = line["line_end"] - line["line_start"] + if self._is_forward(line_index): + return line["trigger_low"], line["line_end"] + span + return line["line_start"] - span, line["trigger_high"] + def _wait_mcs_frames(self, line_index: int, timeout: float = 2.0, poll: float = 0.05): """ After line line_index the MCS card must have advanced to the last channel of that diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index 4ea4e2e9..cba35c3a 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -262,6 +262,28 @@ def test_line_trigger_window_is_relative_to_current_position(): assert not _sim_state().pso["armed"] +def test_line_trigger_window_range_keeps_the_reference(): + x, _, _ = _capture_setup() + x.move(-1.0, wait=True) + x.arm_line_trigger(0.0, 1.0) + x.move(2.0, wait=True) + x.set_line_trigger_window(-3.0, 1.5) + # relative to the position at arm (-1.0), not to the current one (2.0) + assert _sim_state().pso["range"] == (-40000.0, 50000.0) + assert "[PUT]: PSOWINDOW X 1 LOAD 0" in x.controller.command_history + assert sum("LOAD" in c for c in x.controller.command_history) == 1 + + +def test_line_trigger_window_range_needs_armed_window(): + x, _, _ = _capture_setup() + with pytest.raises(A3200Error, match="not armed"): + x.set_line_trigger_window(0.0, 1.0) + x.arm_line_trigger(0.5, 1.0) + x.disarm_line_trigger() + with pytest.raises(A3200Error, match="not armed"): + x.set_line_trigger_window(0.0, 1.0) + + def test_line_trigger_uses_emulated_quadrature_divider(): u = _sim_motor( "U", axis_nr=3, egu="deg", pso_input=7, pso_counts_divider=40, sim_counts_per_unit=560000 diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index a7e4dcbe..6dcdf027 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -39,6 +39,7 @@ def _add_devices( ) aerox.arm_line_trigger = mock.MagicMock() aerox.disarm_line_trigger = mock.MagicMock() + aerox.set_line_trigger_window = mock.MagicMock() # signal writes go through RPC; mock them, keep the configured values readable for signal_name, value in ( ("motor_velocity", 10.0), @@ -256,7 +257,9 @@ def test_pre_scan_arms_once_after_calibration( mock.call.capture.calibrate(), mock.call.fast_axis.motor_velocity.set(line["target_velocity"]), mock.call.fast_axis.wait_in_position.set(False), - mock.call.fast_axis.arm_line_trigger(line["trigger_low"], line["trigger_high"]), + mock.call.fast_axis.arm_line_trigger( + line["trigger_low"], 2 * line["line_end"] - line["line_start"] + ), mock.call.ddg1.set_trigger(2), # falling edge: PSO open collector / differential ] @@ -315,9 +318,13 @@ def test_snake_scan_core(v4_scan_assembler, device_manager, nth_done_status_mock ([stepper], [2.0]), ([fast], [line["line_end"]]), ] - # the PSO window is armed once in pre_scan, not per line + # the PSO window is armed once in pre_scan, not per line; only its range is switched scan.fast_axis.arm_line_trigger.assert_not_called() scan.fast_axis.disarm_line_trigger.assert_not_called() + span = line["line_end"] - line["line_start"] + backward = mock.call(line["line_start"] - span, line["trigger_high"]) + forward = mock.call(line["trigger_low"], line["line_end"] + span) + assert scan.fast_axis.set_line_trigger_window.call_args_list == [backward, forward] def test_panda_lines_are_checked(v4_scan_assembler, device_manager, nth_done_status_mock): -- 2.54.0 From 5819d6b1fd3ef811fc2f1c20d23b99c1007e9726 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 11:24:05 +0100 Subject: [PATCH 19/52] fix(aerotech): limit abort explains the run-up separately from the scan range The message now says whether the scan range itself is outside the limits or only the extra run-up travel for acceleration and settling, by how much, and what to do about it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../scans/scans_v4/aerotech_cont_grid.py | 25 ++++++++++++++----- .../test_aerotech_cont_grid_scan.py | 11 +++++++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index db384575..ba9a5523 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -506,14 +506,27 @@ class AerotechContGrid(ScanBase): def _check_line_limits(self): """The lines including run-up / run-out and the step axis range must fit the limits.""" line = self._line + name = self.fast_axis.name low, high = self.fast_axis.limits - if low < high and (line["line_start"] < low or line["line_end"] > high): - run_up = line["trigger_low"] - line["line_start"] + if low < high and (self.fast_start < low or self.fast_end > high): raise ScanAbortion( - f"{self.fast_axis.name} range {line['line_start']:.4f} .. {line['line_end']:.4f}" - f" (including {run_up:.4f} run-up / run-out on each side at" - f" {line['target_velocity']} / s, acceleration {line['acceleration']} / s^2)" - f" is outside the limits ({low}, {high})." + f"Scan range of {name} {self.fast_start:.4f} .. {self.fast_end:.4f} is outside" + f" the limits ({low}, {high})." + ) + if low < high and (line["line_start"] < low or line["line_end"] > high): + extra = self.fast_start - line["line_start"] + if line["line_end"] > high: + beyond = f"beyond the high limit {high} by {line['line_end'] - high:.4f}" + else: + beyond = f"beyond the low limit {low} by {low - line['line_start']:.4f}" + raise ScanAbortion( + f"The scan range of {name} {self.fast_start:.4f} .. {self.fast_end:.4f} is" + f" within the limits, but {name} needs {extra:.4f} extra travel on each side" + f" to accelerate to the scan velocity {line['target_velocity']:g} / s and" + f" settle (acceleration {line['acceleration']:g} / s^2, settle_time" + f" {self.settle_time:g} s). It would travel {line['line_start']:.4f} .." + f" {line['line_end']:.4f}, {beyond}. Move the scan away from the limit or" + " scan slower (larger exp_time or smaller fast_step_size)." ) low, high = self.stepper_axis.limits stepper_range = sorted((self.stepper_start, self.stepper_stop)) diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 6dcdf027..f775af0d 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -207,7 +207,16 @@ def test_run_up_outside_limits_aborts(v4_scan_assembler, device_manager): scan = v4_scan_assembler( "aerotech_cont_grid", "aerox", -4.0, 4.0, 0.1, "aeroy", -2.0, 2.0, 2, exp_time=0.005 ) - with pytest.raises(ScanAbortion, match="run-up"): + with pytest.raises(ScanAbortion, match="within the limits, but aerox needs .* extra travel"): + scan.prepare_scan() + + +def test_scan_range_outside_limits_aborts(v4_scan_assembler, device_manager): + _add_devices(device_manager) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -1.0, 11.0, 0.5, "aeroy", -2.0, 2.0, 2, exp_time=0.1 + ) + with pytest.raises(ScanAbortion, match="Scan range of aerox"): scan.prepare_scan() -- 2.54.0 From e4f281249bdbde4ab8625092a21b38ad25fb6784 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 11:27:31 +0100 Subject: [PATCH 20/52] docs(aerotech): README run-up formula matches the 2x ramp margin Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/aerotech/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index a513a78f..49442d2c 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -86,7 +86,7 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 - `trigger_edge` (DDG1): `"falling"` (default) for the active low PSO open collector (J7-8) or differential (J7-1/2) output, `"rising"` for the opto output (J7-6/7). The PandA has the same choice as device config `trigger_edge` (default `"rising"`, opto wiring). -- run-up / run-out = ramp distance `v^2 / (2 * acceleration)` + 20 % + `v * settle_time` +- run-up / run-out = twice the ramp distance `v^2 / (2 * acceleration)` + `v * settle_time` (default 0.15 s, measured 2026-10-28: the following error needs ~100-200 ms after the ramp at 1 mm/s / 10 mm/s^2). In a snake the run-out of a line is the run-up of the next. - at the turnarounds X does not wait for In Position (`wait_in_position` signal off during -- 2.54.0 From db77f0df8133ded81f4689c2331341680b96c6ad Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 11:28:15 +0100 Subject: [PATCH 21/52] config(aerotech): aerox acceleration 1000 mm/s^2 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/device_configs/sastt_aerotech.yaml | 4 ++-- csaxs_bec/device_configs/sastt_aerotech_panda.yaml | 2 +- .../simulated_omny/simulated_sastt_aerotech.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index 0342b3bc..c8d444cb 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -12,7 +12,7 @@ # controller soft limits: X -50..150, Z -100..100, Y -10..10 mm (active after home) # tolerance: X 1 um, Z/Y 5 um (InPositionDistance X 10 nm, Z/Y 5 um) # velocity / acceleration below = DefaultSpeed / DefaultRampRate (MaxSpeedClamp: -# X 200, Z 10, Y 10 mm/s, U 720 deg/s), except X: acceleration 500 mm/s^2 (short +# X 200, Z 10, Y 10 mm/s, U 720 deg/s), except X: acceleration 1000 mm/s^2 (short # run-up in aerotech_cont_grid), scans up to max_scan_velocity 50 mm/s # Wiring for aerotech_cont_grid: PSO output (J7 "PSO INTERFACE") -> DDG1 EXT, # detector trigger -> UINT1 (J12 "MISC I/O"). @@ -27,7 +27,7 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 500.0 + acceleration: 1000.0 host: pc14643.psi.ch port: 8000 velocity: 5.0 diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index 22ce45c3..7fd01781 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -16,7 +16,7 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 500.0 + acceleration: 1000.0 host: pc14643.psi.ch port: 8000 velocity: 5.0 diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index f1015d98..03b62f73 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -12,7 +12,7 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 500.0 + acceleration: 1000.0 host: sim-a3200-saxstt port: 8000 velocity: 5.0 -- 2.54.0 From 77d08df54c22814e7dcf1ab46f9ce93b2c5a4f74 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 11:34:21 +0100 Subject: [PATCH 22/52] fix(panda): edges during a running line are logged, not a scan error At 2 mm/s an extra edge at the PSO window entry arrived during the running line (small field of view, line 4); it was ignored as designed, like on the DDG, but wait_lines_done aborted the scan. Wrong line or pulse counts still fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 4 ++-- csaxs_bec/devices/panda_box/panda_line_trigger.py | 9 +++++---- tests/tests_devices/test_panda_line_trigger.py | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 49442d2c..a82c9e0e 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -164,8 +164,8 @@ dev.pandatrig.wait_lines_done(n_lines=1, timeout=5) dev.pandatrig.line_status() # triggers, lines, pulses, dropped since arm() ``` -`wait_lines_done` fails on timeout, on a wrong number of lines or gates and on ignored -edges. +`wait_lines_done` fails on timeout and on a wrong number of lines or gates; ignored edges +(during a running line, e.g. encoder noise at the PSO window entry) are only logged. PandA FPGA 4.1 PULSE block: an ENABLE edge, a parameter write or a dropped trigger often makes it replay the previous pulse train without a trigger. The device keeps the PULSE diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index e4336836..456e1d55 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -348,9 +348,10 @@ class PandaLineTrigger(Device): def wait_lines_done(self, n_lines: int, timeout: float, poll: float = 0.02) -> dict: """ - Wait until n_lines lines were triggered and all their pulses sent. Fails on timeout, - on more lines or pulses than expected and on ignored trigger edges (edge during a - running line). + Wait until n_lines lines were triggered and all their pulses sent. Fails on timeout + and on more lines or pulses than expected. Ignored trigger edges (edge during a + running line, e.g. encoder noise at the PSO window entry; the DDG ignores them too) + are only logged. """ n_pulses = self._pulse_params["n_pulses"] if self._pulse_params else 0 start = time.time() @@ -378,7 +379,7 @@ class PandaLineTrigger(Device): f" got {status['lines']} with {status['pulses']}." ) if status["triggers"] != status["lines"]: - raise PandaLineTriggerError( + logger.warning( f"{self.name}: {status['triggers'] - status['lines']} trigger edge(s) ignored" f" (edge while a line was running or while not armed): {status}." ) diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 4a9dfb53..8f10d717 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -120,19 +120,19 @@ def test_one_shot_accepts_one_edge(trigger): trigger.software_trigger() assert trigger.wait_lines_done(1, timeout=1)["armed"] is False trigger.software_trigger() - with pytest.raises(PandaLineTriggerError, match="1 trigger edge"): - trigger.wait_lines_done(1, timeout=1) + status = trigger.wait_lines_done(1, timeout=1) + assert status["triggers"] == 2 and status["lines"] == 1 -def test_edge_during_line_is_reported(trigger): +def test_edge_during_line_is_ignored(trigger): trigger.configure_line(5, exp_time=0.01) trigger.arm() trigger.software_trigger() trigger.panda.busy = True trigger.software_trigger() trigger.panda.busy = False - with pytest.raises(PandaLineTriggerError, match="ignored"): - trigger.wait_lines_done(1, timeout=1) + status = trigger.wait_lines_done(1, timeout=1) + assert status["triggers"] == 2 and status["lines"] == 1 and status["pulses"] == 5 def test_arm_needs_configure(trigger): -- 2.54.0 From 972137b871e3de6c564f6231589d19f05ca99c4e Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 11:39:00 +0100 Subject: [PATCH 23/52] fix(aerotech): DDG1 trigger edge rising by default (opto output, measured) The PSO opto output (J7-6/7 sourcing from J7-25 +5 V) into a 50 Ohm input is high inside the window in both directions (measured on the PandA TTLIN3, 2026-10-29). DDG1 EXT is 50 Ohm as well, so the tested wiring needs a rising edge. "falling" stays available for the open collector / differential outputs (not tested). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 17 ++++++++++------- csaxs_bec/scans/scans_v4/aerotech_cont_grid.py | 12 +++++++----- .../tests_scans/test_aerotech_cont_grid_scan.py | 10 ++++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index a82c9e0e..d04ec221 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -83,9 +83,10 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 `"panda"` (test setup: PandA configured and armed once, every line checked with `wait_lines_done`; `readout_time`, `shutter_delay` arguments) or `None` (no trigger hardware, motion tests) -- `trigger_edge` (DDG1): `"falling"` (default) for the active low PSO open collector (J7-8) - or differential (J7-1/2) output, `"rising"` for the opto output (J7-6/7). The PandA has - the same choice as device config `trigger_edge` (default `"rising"`, opto wiring). +- `trigger_edge` (DDG1): `"rising"` (default) for the opto output (J7-6/7, the tested + wiring), `"falling"` for the active low open collector (J7-8) or differential (J7-1/2) + output (not tested). The PandA has the same choice as device config `trigger_edge` + (default `"rising"`, opto wiring). - run-up / run-out = twice the ramp distance `v^2 / (2 * acceleration)` + `v * settle_time` (default 0.15 s, measured 2026-10-28: the following error needs ~100-200 ms after the ramp at 1 mm/s / 10 mm/s^2). In a snake the run-out of a line is the run-up of the next. @@ -102,9 +103,11 @@ Soft test 2026-10-28 (scan sequence on the hardware, trigger_source=None, 1x1 mm going forward and at X = 0.76 going backward (window 0.25..0.76). PSO output polarity: the PSO status bit "Output 1" is high outside the window. Open collector -(J7-8) and differential outputs are active low; the opto output (J7-6/7, JP16 normally open) -conducts inside the window. Check on the wiring day that the trigger receiver input goes -high when X enters the window (e.g. park X inside / outside and read `TTLIN3.VAL`). +(J7-8) and differential outputs are active low (manual, not tested); the opto output +(J7-6/7, JP16 normally open) conducts inside the window. Measured 2026-10-29 with the opto +output sourcing from J7-25 (+5 V -> J7-6, J7-7 -> signal, J7-24 -> ground) into the 50 Ohm +TTLIN3: high inside the window in both directions, i.e. a rising edge at the line start. +The DDG1 EXT input is 50 Ohm as well, so the same wiring with `trigger_edge="rising"`. Second client on the ASCII interface: only `~STATUS` (server command) is safe in parallel. Controller commands of all clients run on the library task by default and are refused @@ -116,7 +119,7 @@ Wiring: | Signal | From | To | |--------|------|----| -| line trigger | Npaq J7 "PSO INTERFACE" (rear, User I/O, 25-pin D-sub): pin 8 open collector (TTL, active low) + pin 24 common, or pins 6/7 opto output, or pins 1/2 RS-422 | DDG1 EXT | +| line trigger | Npaq J7 "PSO INTERFACE" (rear, User I/O, 25-pin D-sub), opto output (tested): J7-25 (+5 V) -> J7-6, J7-7 -> signal, J7-24 -> ground; rising edge. Alternatives (not tested, falling edge): pin 8 open collector + pin 24 common, pins 1/2 RS-422 | DDG1 EXT (50 Ohm) | | exposure trigger | detector trigger (DDG2 AB) | Npaq UINT1 (J12 pins 1+/2-, 5 V opto, 5-15 mA) | Commissioning of the scan: diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index ba9a5523..5d3fb258 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -25,9 +25,11 @@ line) and checked for completeness in complete. Trigger sources (trigger_source): "ddg": PSO output -> DDG1 EXT (beamline). Every trigger starts one DDG1 burst of frames_per_line gates (shutter / detectors / MCS). After each line the scan - checks that the MCS card counted the line's frames. trigger_edge: the Npaq open collector (J7-8) and - differential (J7-1/2) PSO outputs are active low, so the window entry is a - falling edge (default); the opto output (J7-6/7) gives a rising edge. + checks that the MCS card counted the line's frames. trigger_edge: the Npaq opto + output (J7-6/7, sourcing from J7-25 +5 V, into the 50 Ohm DDG input) is high + inside the window, so the window entry is a rising edge (default, measured + 2026-10-29 on the PandA); the open collector (J7-8) and differential (J7-1/2) + outputs are active low according to the manual: "falling". "panda": PSO output -> PandABox TTLIN3 (device pandatrig, test setup). The PandA is configured and armed once and sends the shutter pulse and the detector gates of each line. @@ -143,7 +145,7 @@ class AerotechContGrid(ScanBase): exp_time: DefaultArgType.ExposureTime, relative: DefaultArgType.Relative = False, trigger_source: str | None = "ddg", - trigger_edge: str = "falling", + trigger_edge: str = "rising", trigger_device: str = "pandatrig", readout_time: float = 0.0, shutter_delay: float | None = None, @@ -170,7 +172,7 @@ class AerotechContGrid(ScanBase): exp_time (float): Exposure time (period of the detector gates) in seconds. relative (bool): Whether the positions are relative to the current position. Default is False. trigger_source (str | None): "ddg" (beamline, DDG1/MCS), "panda" (PandABox line trigger) or None (no trigger hardware). Default is "ddg". - trigger_edge (str): DDG1 external trigger edge, "falling" (PSO open collector / differential output) or "rising" (opto output). Default is "falling". + trigger_edge (str): DDG1 external trigger edge, "rising" (opto output, J7-6/7) or "falling" (open collector / differential output). Default is "rising". trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". readout_time (float): Detector readout time, gate width = exp_time - readout_time (trigger_source="panda"). Default is 0. shutter_delay (float | None): Delay from the line trigger to the first exposure in s. Default: DDG1 shutter delay for "ddg", 0 otherwise. diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index f775af0d..de65cc24 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -269,19 +269,21 @@ def test_pre_scan_arms_once_after_calibration( mock.call.fast_axis.arm_line_trigger( line["trigger_low"], 2 * line["line_end"] - line["line_start"] ), - mock.call.ddg1.set_trigger(2), # falling edge: PSO open collector / differential + mock.call.ddg1.set_trigger(1), # rising edge: PSO opto output ] -def test_ddg_rising_edge_for_opto_output(v4_scan_assembler, device_manager, nth_done_status_mock): - scan = _assemble(v4_scan_assembler, device_manager, trigger_edge="rising") +def test_ddg_falling_edge_for_open_collector_output( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _assemble(v4_scan_assembler, device_manager, trigger_edge="falling") scan.prepare_scan() scan.fast_axis = mock.MagicMock() scan.ddg1 = mock.MagicMock() scan._premove_motor_status = nth_done_status_mock(resolve_after=1) scan.actions.pre_scan_all_devices = mock.MagicMock() scan.pre_scan() - scan.ddg1.set_trigger.assert_called_once_with(1) + scan.ddg1.set_trigger.assert_called_once_with(2) def test_invalid_trigger_edge_aborts(v4_scan_assembler, device_manager): -- 2.54.0 From 28ba071d68557fed1c14a6fcae548f21a3feedd5 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 12:08:34 +0100 Subject: [PATCH 24/52] fix(panda): write layout and pulse fields only when they differ A replayed PULSE train also came 1.3 s after an unchanged parameter write by a new device instance (connect / configure_line), outside the 1 s quiet wait, and blocked the first line trigger. Fields are now read first and written only if the PandA holds a different value, so a reconnect on a configured PandA writes nothing; the quiet wait after a real change is 2 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- .../devices/panda_box/panda_line_trigger.py | 64 +++++++++++++------ .../tests_devices/test_panda_line_trigger.py | 14 ++++ 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 456e1d55..4924730f 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -22,9 +22,11 @@ any trigger. With unchanged parameters and ENABLE kept high, hundreds of trigger a row were correct. So the PULSE blocks stay enabled, their parameters are only written when they change (once per scan), followed by a wait until a replayed train is over, and edges during a line are filtered out before the PULSE blocks. Replayed trains during a -parameter change do appear on TTLOUT2/3. The replayed train starts only ~0.3-0.6 s after +parameter change do appear on TTLOUT2/3. The replayed train starts only ~0.3-1.3 s after the write (seen 2026-10-29), so configure_line waits until both PULSE outputs stayed low -for PARAM_QUIET_TIME. +for PARAM_QUIET_TIME. All fields are read first and written only if the PandA holds a +different value: a new device instance (BEC restart, reconnect) on an already configured +PandA writes nothing, so it cannot cause a replay. Only the control port (8888) is used: the fields are written by setup_layout() at connect, the PCAP data capture is not touched. Timing resolution 8 ns (125 MHz FPGA clock). @@ -37,6 +39,7 @@ import socket import threading import time +import numpy as np from bec_lib.logger import bec_logger from ophyd import Device @@ -131,8 +134,8 @@ class PandaLineTrigger(Device): TRIGGER_COUNTER = "COUNTER6" LINE_COUNTER = "COUNTER7" PULSE_COUNTER = "COUNTER8" - # a replayed train starts up to ~0.6 s after a parameter write - PARAM_QUIET_TIME = 1.0 + # a replayed train starts up to ~1.3 s after a parameter write + PARAM_QUIET_TIME = 2.0 SHUTTER_OUTPUT = "TTLOUT2" DETECTOR_OUTPUT = "TTLOUT3" @@ -231,9 +234,29 @@ class PandaLineTrigger(Device): def setup_layout(self) -> None: """Write the wiring of the line trigger blocks (other blocks are not touched).""" - for field, value in self._layout(): - self.panda.set(field, value) + written = [field for field, value in self._layout() if self._set_if_changed(field, value)] self._pulse_params = None + pulses = (self.SHUTTER_PULSE, self.DETECTOR_PULSE) + if any(field.split(".")[0] in pulses for field in written): + self._wait_pulses_idle(2 * self._train_duration(None) + 1) + + def _set_if_changed(self, field: str, value) -> bool: + """ + Write a field only if the PandA holds a different value (fewer writes, fewer replayed + PULSE trains). Actions (empty value, e.g. FORCE_RST) are always sent. Returns True + if the field was written. + """ + value = str(value) + if value != "": + current = self.panda.get(field) + try: + if np.isclose(float(current), float(value), rtol=1e-9, atol=1e-12): + return False + except ValueError: + if current == value: + return False + self.panda.set(field, value) + return True @staticmethod def _train_duration(params: dict | None) -> float: @@ -270,19 +293,24 @@ class PandaLineTrigger(Device): if params == self._pulse_params: return sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE - self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") - self.panda.set(f"{dp}.DELAY", f"{params['delay']:.9f}") - self.panda.set(f"{dp}.WIDTH", f"{params['width']:.9f}") - self.panda.set(f"{dp}.STEP", f"{params['step']:.9f}") - self.panda.set(f"{dp}.PULSES", params["n_pulses"]) - self.panda.set(f"{sp}.DELAY", "0") - self.panda.set(f"{sp}.WIDTH", f"{params['shutter_width']:.9f}") - self.panda.set(f"{sp}.STEP", "0") - self.panda.set(f"{sp}.PULSES", 1) + fields = [ + (f"{dp}.DELAY", f"{params['delay']:.9f}"), + (f"{dp}.WIDTH", f"{params['width']:.9f}"), + (f"{dp}.STEP", f"{params['step']:.9f}"), + (f"{dp}.PULSES", params["n_pulses"]), + (f"{sp}.DELAY", "0"), + (f"{sp}.WIDTH", f"{params['shutter_width']:.9f}"), + (f"{sp}.STEP", "0"), + (f"{sp}.PULSES", 1), + ] + previous = self._pulse_params self._pulse_params = None - self._wait_pulses_idle( - 2 * max(self._train_duration(self._pulse_params), self._train_duration(params)) + 1 - ) + self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") + written = [field for field, value in fields if self._set_if_changed(field, value)] + if written: + self._wait_pulses_idle( + 2 * max(self._train_duration(previous), self._train_duration(params)) + 1 + ) self._pulse_params = params def _wait_pulses_idle(self, timeout: float) -> None: diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 8f10d717..9febf180 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -241,3 +241,17 @@ def test_configure_line_waits_for_a_late_replayed_train(trigger): trigger.configure_line(5, 0.01) # the train ended at ~0.1 s, then 0.2 s quiet assert time.time() - t0 >= 0.28 + + +def test_unchanged_fields_are_not_written(trigger): + """A new device instance on a configured PandA writes nothing (no replayed trains).""" + trigger.configure_line(5, 0.01, 0.002) + fields = dict(trigger.panda.fields) + again = PandaLineTrigger(name="pandatrig", host="fake-panda") + again.PARAM_QUIET_TIME = 0.01 + again.panda = FakePanda() + again.panda.fields = fields + again.wait_for_connection() + again.configure_line(5, 0.01, 0.002) + values = [w for w in again.panda.writes if not w.split("=", 1)[1] == ""] + assert values == [] -- 2.54.0 From 3aca5030a2f36aa70bca1b7632a4b92a63db202d Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 12:08:34 +0100 Subject: [PATCH 25/52] fix(aerotech): position capture latches the gate start (DATAACQ trigger 3) Measured with TTL gates on J12 at 2 mm/s: trigger 0 ("UINT1 rising" in the A3200 help) latched at the gate end (+17.2 um for a 10 ms gate, expected +20), trigger 3 at the gate start (-3.0 um). The Npaq UINT input is inverted compared with the help. Default trigger_input is now 3. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 6 ++++-- csaxs_bec/devices/aerotech/a3200.py | 19 +++++++++++++------ csaxs_bec/devices/sim/sim_a3200.py | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index d04ec221..9d8282c2 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -94,7 +94,9 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 the scan); X reaches 1 um within ~60 ms but 10 nm only after 0.5-2 s - `capture_positions=True` (default False until commissioned): device `aerocapture` latches the encoder counters on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz with - MXR sources); after each line they are read back (~2600 values/s over the ASCII interface) + MXR sources). The Npaq input is inverted compared with the A3200 help (measured + 2026-10-29): DATAACQ trigger 3 latches at the rising TTL edge at J12 (gate start, the + default), trigger 0 at the falling one (gate end); after each line they are read back (~2600 values/s over the ASCII interface) and published as async data `aerocapture_positions_enc`. The counter offset is calibrated in pre_scan with one software PSO fire, before any trigger receiver is armed. @@ -150,7 +152,7 @@ lines run back to back without re-arming. |-------|-----|--------| | TTLIN3 (50 Ω) | line trigger | Npaq J7 opto output: J7-25 (+5 V) -> J7-6, J7-7 -> TTLIN3, J7-24 -> TTLIN3 ground | | TTLOUT2 | "shutter": trigger edge to end of the last gate | scope | -| TTLOUT3 | "detector" gates (high during exposure): n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope, later Npaq J12 pin 1 (+) / pin 2 (-) for position sampling (rising edge = exposure start) | +| TTLOUT3 | "detector" gates (high during exposure): n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope and Npaq J12 pin 1 (+) / pin 2 (-) for position sampling (rising edge = exposure start, DATAACQ trigger 3) | Blocks: LUT8 = TTLIN3 | BITS.A (software trigger); SRGATE1 arm gate; LUT7 = rising edge of LUT8 & SRGATE1 & no line running (PULSE3/PULSE4 low) -> PULSE3 (shutter), PULSE4 (gates). diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index a4f35d3b..b4838345 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -394,7 +394,8 @@ class A3200Controller(Controller): def dataacq_arm(self, axis: str, trigger: int, channels: list[int], n_triggers: int) -> None: """ Capture the source signals `channels` on each trigger into the drive array, - starting at index 0. Npaq trigger 0 = Input 12 (UINT1) rising edge, 6 = PSO firing. + starting at index 0. Npaq trigger 0 / 3 = Input 12 (UINT1) "rising" / "falling" edge + (measured: 3 latches when the TTL level at J12 rises), 6 = PSO firing. Npaq sources: 0-5 local encoder counter, 6-11 encoder multiplier (MXR) channel 0-5. """ self.command(f"DATAACQ {axis} 1 OFF") @@ -774,7 +775,12 @@ class A3200Motor(Device, PositionerBase): # Npaq DATAACQ position sources: 0-5 local encoder counter, 6-11 encoder multiplier (MXR) # channel 0-5 (max. 4 kHz trigger rate); 12-15 are analog inputs, not used here. DATAACQ_POSITION_SOURCES = 12 -DATAACQ_TRIGGER_UINT1_RISING = 0 +# Npaq DATAACQ triggers on UINT1 (J12 pin 1+/2-). Measured 2026-10-29 with a TTL gate on +# J12 (PandA TTLOUT3, X at 2 mm/s): trigger 3 ("falling edge" in the A3200 help) latches at +# the rising TTL edge (start of the exposure gate), trigger 0 ("rising edge") at the falling +# TTL edge (end of the gate). The opto input is inverted compared with the help. +DATAACQ_TRIGGER_UINT1_TTL_RISING = 3 +DATAACQ_TRIGGER_UINT1_TTL_FALLING = 0 DATAACQ_TRIGGER_PSO_FIRING_EVENT = 6 @@ -782,8 +788,8 @@ class A3200PositionCapture(PSIDeviceBase): """ Latch axis positions on an external trigger (Npaq DATAACQ, drive array). - Each rising edge on the trigger input (default: Input 12 / UINT1, the detector - exposure trigger) stores the encoder counters of the configured channels in the + Each trigger edge (default: rising TTL edge on Input 12 / UINT1, i.e. the start of the + detector exposure gate) stores the encoder counters of the configured channels in the drive array. After a scan line, read_line() reads them back, converts them to user units (CountsPerUnit of the axis) and publishes them as async data (signals enc). @@ -794,7 +800,8 @@ class A3200PositionCapture(PSIDeviceBase): its PositionFeedbackChannel, by default its axis index in the Npaq. Values are in controller coordinates (the BEC motor sign is not applied). drive_axis: any axis of the Npaq, used to address the drive (shared array). - trigger_input: DATAACQ trigger, 0 = Input 12 (UINT1) rising edge. + trigger_input: DATAACQ trigger, 3 = rising TTL edge at UINT1 (gate start, default), + 0 = falling TTL edge (gate end); see DATAACQ_TRIGGER_UINT1_TTL_RISING. """ USER_ACCESS = ["controller", "arm", "read_line", "calibrate", "disarm"] @@ -819,7 +826,7 @@ class A3200PositionCapture(PSIDeviceBase): port=8000, channels=None, drive_axis="X", - trigger_input=DATAACQ_TRIGGER_UINT1_RISING, + trigger_input=DATAACQ_TRIGGER_UINT1_TTL_RISING, socket_cls=SocketIO, device_manager=None, scan_info=None, diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index acd03e82..c6ad6ab5 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -166,7 +166,7 @@ class SimA3200State: if not (min(start, target) <= low and high <= max(start, target)): return self.pso_window_entries += 1 - if acq["armed"] and acq["trigger"] == 0 and acq["inputs"]: + if acq["armed"] and acq["trigger"] in (0, 3) and acq["inputs"]: n_triggers = acq["n_elements"] // len(acq["inputs"]) step = (high - low) / max(n_triggers, 1) for k in range(n_triggers): -- 2.54.0 From 201e6406d285ee86772c00527476cdbc648f9983 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 12:27:26 +0100 Subject: [PATCH 26/52] feat(aerotech): exposure centres, readout gap compensation, positions always captured - readout time from the trigger hardware (DDG2 effective_readout_times()["ab"], PandA device config readout_time, default 0.2 ms), no scan argument; the trigger positions move inward by v * readout / 2 so the gates are centred in their cells and forward / backward lines have the same exposure centres - aerotech_cont_grid always captures the positions (capture_positions removed): aerocapture publishes the captured gate starts (enc) and the exposure centres (enc_centre, + v * gate_width / 2 in the direction of motion) - statistics at the end of the scan (sent to the client): centre - nominal cell centre per direction, backward - forward with slope and peak-peak, scan and line times - aerox acceleration 500 mm/s^2 (1000 left a speed drift in the lines at 20 mm/s); aerocapture enabled, channels X and Y only Hardware 2026-10-29 (PandA, 0.2 ms readout, 6 snake lines each), backward - forward: 2 mm/s +2.3 / +3.2 um, 0.5 mm/s -0.1 / -0.75 um, no slope; 20 mm/s -1.2 um, slope -1.2 um/mm (was +24 um, 5 um/mm with 1000 mm/s^2 and no compensation). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/device_configs/sastt_aerotech.yaml | 12 +- .../device_configs/sastt_aerotech_panda.yaml | 10 +- .../simulated_sastt_aerotech.yaml | 4 +- csaxs_bec/devices/aerotech/README.md | 27 +++- csaxs_bec/devices/aerotech/a3200.py | 23 ++- .../devices/panda_box/panda_line_trigger.py | 9 ++ .../scans/scans_v4/aerotech_cont_grid.py | 148 ++++++++++++++---- tests/tests_devices/test_a3200.py | 33 +++- .../test_aerotech_cont_grid_scan.py | 55 +++++-- 9 files changed, 251 insertions(+), 70 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index c8d444cb..9bf4f75e 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -12,8 +12,8 @@ # controller soft limits: X -50..150, Z -100..100, Y -10..10 mm (active after home) # tolerance: X 1 um, Z/Y 5 um (InPositionDistance X 10 nm, Z/Y 5 um) # velocity / acceleration below = DefaultSpeed / DefaultRampRate (MaxSpeedClamp: -# X 200, Z 10, Y 10 mm/s, U 720 deg/s), except X: acceleration 1000 mm/s^2 (short -# run-up in aerotech_cont_grid), scans up to max_scan_velocity 50 mm/s +# X 200, Z 10, Y 10 mm/s, U 720 deg/s), except X: acceleration 500 mm/s^2 (1000 left a +# speed drift in the lines at 20 mm/s, 2026-10-29), scans up to max_scan_velocity 50 mm/s # Wiring for aerotech_cont_grid: PSO output (J7 "PSO INTERFACE") -> DDG1 EXT, # detector trigger -> UINT1 (J12 "MISC I/O"). # UU (ADRS-150 slipring follower) follows U as gantry slave and is not a BEC device. @@ -27,7 +27,7 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 1000.0 + acceleration: 500.0 host: pc14643.psi.ch port: 8000 velocity: 5.0 @@ -114,7 +114,7 @@ aerorot: deviceTags: - sastt_aerotech aerocapture: - # disabled until the position sampling stage (moves -> continuous scans -> trigger -> capture) + # always used by aerotech_cont_grid (positions at the start and centre of each exposure) description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture deviceConfig: @@ -125,10 +125,8 @@ aerocapture: # keys are quoted: integer map keys break the config message (msgpack strict_map_key) channels: "6": X - "11": Z "2": Y - "9": U - enabled: false + enabled: true onFailure: buffer readOnly: false readoutPriority: async diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index 7fd01781..c87a3910 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -16,7 +16,7 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 1000.0 + acceleration: 500.0 host: pc14643.psi.ch port: 8000 velocity: 5.0 @@ -103,7 +103,7 @@ aerorot: deviceTags: - sastt_aerotech aerocapture: - # disabled until the position sampling stage (moves -> continuous scans -> trigger -> capture) + # always used by aerotech_cont_grid (positions at the start and centre of each exposure) description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) deviceClass: csaxs_bec.devices.aerotech.a3200.A3200PositionCapture deviceConfig: @@ -114,10 +114,8 @@ aerocapture: # keys are quoted: integer map keys break the config message (msgpack strict_map_key) channels: "6": X - "11": Z "2": Y - "9": U - enabled: false + enabled: true onFailure: buffer readOnly: false readoutPriority: async @@ -131,6 +129,8 @@ pandatrig: port: 8888 input_termination: 50-Ohm trigger_edge: rising + # gap between the detector gates, as the DDG2 default at the beamline + readout_time: 0.0002 enabled: true onFailure: buffer readOnly: false diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml index 03b62f73..d81a613d 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_sastt_aerotech.yaml @@ -12,7 +12,7 @@ aerox: axis_nr: 0 pso_input: 6 pso_counts_divider: 1 - acceleration: 1000.0 + acceleration: 500.0 host: sim-a3200-saxstt port: 8000 velocity: 5.0 @@ -113,9 +113,7 @@ aerocapture: # keys are quoted: integer map keys break the config message (msgpack strict_map_key) channels: "6": X - "11": Z "2": Y - "9": U enabled: true onFailure: buffer readOnly: false diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 9d8282c2..b64cceb1 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -81,8 +81,14 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 gates per trigger; DDG1/DDG2 take exp_time and frames_per_trigger from the scan info at stage; after each line the scan checks that the MCS reached the line's last channel), `"panda"` (test setup: PandA configured and armed once, every line checked with - `wait_lines_done`; `readout_time`, `shutter_delay` arguments) or `None` (no trigger - hardware, motion tests) + `wait_lines_done`; `shutter_delay` argument) or `None` (no trigger hardware, motion + tests) +- readout time (gap between the detector gates) from the trigger hardware, no scan + argument: DDG2 `effective_readout_times()["ab"]` (default 0.2 ms) or the PandA device + config `readout_time` (default 0.2 ms). The trigger positions move inward by + `v * readout_time / 2`, so every gate is centred in its cell of `fast_step_size` and + forward and backward lines have the same exposure centres (without it they differ by + `fast_step_size * readout_time / exp_time`, measured 2026-10-29) - `trigger_edge` (DDG1): `"rising"` (default) for the opto output (J7-6/7, the tested wiring), `"falling"` for the active low open collector (J7-8) or differential (J7-1/2) output (not tested). The PandA has the same choice as device config `trigger_edge` @@ -92,13 +98,22 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 ramp at 1 mm/s / 10 mm/s^2). In a snake the run-out of a line is the run-up of the next. - at the turnarounds X does not wait for In Position (`wait_in_position` signal off during the scan); X reaches 1 um within ~60 ms but 10 nm only after 0.5-2 s -- `capture_positions=True` (default False until commissioned): device `aerocapture` latches - the encoder counters on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz with +- positions are always captured: device `aerocapture` (must be enabled) latches the + encoder counters on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz with MXR sources). The Npaq input is inverted compared with the A3200 help (measured 2026-10-29): DATAACQ trigger 3 latches at the rising TTL edge at J12 (gate start, the default), trigger 0 at the falling one (gate end); after each line they are read back (~2600 values/s over the ASCII interface) - and published as async data `aerocapture_positions_enc`. The counter offset is - calibrated in pre_scan with one software PSO fire, before any trigger receiver is armed. + and published as async data, both as captured (`aerocapture_positions_enc`, start + of each exposure) and as exposure centres (`aerocapture_positions_enc_centre`, + + `v * gate_width / 2` in the direction of motion for the moving fast axis). The counter + offset is calibrated in pre_scan with one software PSO fire, before any trigger receiver + is armed. At the end the scan sends statistics to the client: centre - nominal cell + centre per direction, backward - forward offset with slope and peak-peak along the line, + scan and line times. +- snake symmetry measured 2026-10-29 (PandA, 0.2 ms readout, before the readout + compensation): at 0.5 and 2 mm/s no trend along the line, backward - forward constant + within 3 um; at 20 mm/s with 1000 mm/s^2 +24 um with a trend of 5 um/mm, with 500 mm/s^2 + +4.7 um (+4 um of it the readout gap) -> aerox acceleration 500 mm/s^2 Soft test 2026-10-28 (scan sequence on the hardware, trigger_source=None, 1x1 mm, X 1 mm/s, 11 lines): 15.3 s, line 1.11 s, Y step 0.28 s; PSO "inside window" switched on at X = 0.25 diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index b4838345..a7b1e7ba 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -809,7 +809,8 @@ class A3200PositionCapture(PSIDeviceBase): positions = Cpt( AsyncMultiSignal, name="positions", - signals=[f"enc{i}" for i in range(DATAACQ_POSITION_SOURCES)], + signals=[f"enc{i}" for i in range(DATAACQ_POSITION_SOURCES)] + + [f"enc{i}_centre" for i in range(DATAACQ_POSITION_SOURCES)], ndim=1, async_update={"type": "add", "max_shape": [None]}, max_size=1000, @@ -853,7 +854,7 @@ class A3200PositionCapture(PSIDeviceBase): raise ValueError("Configure at least two encoder channels for position capture.") # only the configured channels are published (strict signal validation) self.positions.signal_info.signals = self.positions._unify_signals( - [f"enc{ch}" for ch in sorted(self.channels)] + [f"enc{ch}{suffix}" for ch in sorted(self.channels) for suffix in ("", "_centre")] ) def wait_for_connection(self, all_signals=False, timeout: float = 30.0) -> None: @@ -900,8 +901,16 @@ class A3200PositionCapture(PSIDeviceBase): def disarm(self) -> None: self.controller.dataacq_off(self.drive_axis) - def read_line(self) -> dict[str, list[float]]: - """Read the positions of the armed triggers and publish them. Returns them too.""" + def read_line(self, centre_shift: float = 0.0) -> dict[str, list[float]]: + """ + Read the positions of the armed triggers and publish them, both as captured + (enc: position at the trigger edge, i.e. the start of the exposure) and + as exposure centres (enc_centre). centre_shift is the distance from the + start to the centre of an exposure (the scan passes v * gate_width / 2); it is + applied in the direction of motion to the channels that move during the line + (span of the line larger than 2 * centre_shift), the others are unchanged. + Returns the positions too. + """ channels = sorted(self.channels) raw = self.controller.read_drive_array(self.drive_axis, 0, self._n_triggers * len(channels)) # Npaq interleaves the inputs per trigger, in ascending channel order @@ -910,7 +919,11 @@ class A3200PositionCapture(PSIDeviceBase): result = {} for i, ch in enumerate(channels): cpu = self.controller.counts_per_unit(self.channels[ch]) - result[f"enc{ch}"] = data[:, i] / cpu + self._offsets[ch] + captured = data[:, i] / cpu + self._offsets[ch] + span = captured[-1] - captured[0] if len(captured) > 1 else 0.0 + shift = np.sign(span) * centre_shift if abs(span) > 2 * abs(centre_shift) else 0.0 + result[f"enc{ch}"] = captured + result[f"enc{ch}_centre"] = captured + shift self.positions.put( { f"{self.positions.name}_{key}": {"value": value, "timestamp": timestamp} diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 4924730f..6d9660a0 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -111,6 +111,8 @@ class PandaLineTrigger(Device): output) or "High-Z". trigger_edge: edge of TTLIN3 that starts a line, "rising" (Npaq opto output) or "falling" (active low open collector / differential output). + readout_time: detector readout time in s, gap between the gates (gate width = + exp_time - readout_time). Default 0.2 ms, as the DDG2 default at the beamline. """ USER_ACCESS = [ @@ -121,6 +123,7 @@ class PandaLineTrigger(Device): "software_trigger", "line_status", "wait_lines_done", + "get_readout_time", "panda", ] @@ -148,10 +151,12 @@ class PandaLineTrigger(Device): port: int = 8888, input_termination: str = "50-Ohm", trigger_edge: str = "rising", + readout_time: float = 2e-4, device_manager=None, **kwargs, ): self.panda = PandaControl(host, port) + self.readout_time = float(readout_time) self.input_termination = input_termination if trigger_edge not in ("rising", "falling"): raise ValueError(f"trigger_edge must be 'rising' or 'falling', got {trigger_edge!r}.") @@ -232,6 +237,10 @@ class PandaLineTrigger(Device): ] return fields + def get_readout_time(self) -> float: + """Readout time (gap between the detector gates) used by the scans, in s.""" + return self.readout_time + def setup_layout(self) -> None: """Write the wiring of the line trigger blocks (other blocks are not touched).""" written = [field for field, value in self._layout() if self._set_if_changed(field, value)] diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 5d3fb258..e6e1ea74 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -6,9 +6,17 @@ as a snake (alternating direction). The start of each line is triggered in hardw the A3200 PSO output of the fast axis, which goes high when the axis enters the PSO window. The trigger positions are - trigger_low = fast_start - v * shutter_delay, trigger_high = exposure_end + v * shutter_delay + trigger_low = fast_start - v * shutter_delay + v * readout_time / 2 + trigger_high = exposure_end + v * shutter_delay - v * readout_time / 2 -and the window is open on the far side of the line: [trigger_low, beyond line_end] on +The readout term centres each gate (width exp_time - readout_time) in its cell of +fast_step_size: without it the forward gates start at the low edge of their cells and the +backward gates end at the high edge, a shift of fast_step_size * readout_time / exp_time +between the directions (measured 2026-10-29). The readout time is taken from the trigger +hardware: DDG2 effective_readout_times()["ab"] (beamline, default 0.2 ms) or +get_readout_time() of the PandA device (default 0.2 ms); 0 without trigger hardware. + +The window is open on the far side of the line: [trigger_low, beyond line_end] on forward lines, [beyond line_start, trigger_high] on backward lines, so in both directions the first exposure starts one shutter delay after the trigger at the first point of the line. The axis never leaves the window during a line: with a closed window the exit @@ -37,8 +45,13 @@ Trigger sources (trigger_source): sessions without hardware triggered detectors / MCS, they would wait for frames that never come. -With a position capture device (capture_positions=True), the encoder positions are latched -at each detector trigger (Npaq UINT1) and published per line. +The positions are always captured (device aerocapture, Npaq DATAACQ on UINT1 = detector +gates): per line the captured positions at the start of each exposure +(aerocapture_positions_enc) and the exposure centres +(aerocapture_positions_enc_centre = captured + v * gate_width / 2 in the direction +of motion for the moving fast axis). At the end the scan reports statistics of the +centres of the fast axis (deviation from the nominal cell centres per direction, +backward - forward offset and its slope along the line) and of the line times. Run-up / run-out of each line: twice the ramp distance v^2 / (2 * acceleration) + v * settle_time beyond the trigger positions. The whole range including run-up / run-out is @@ -98,6 +111,7 @@ class AerotechLineParameter(TypedDict): target_velocity: float | None acceleration: float | None shutter_open_delay: float | None + readout_time: float | None trigger_low: float | None trigger_high: float | None exposure_end: float | None @@ -127,7 +141,6 @@ class AerotechContGrid(ScanBase): "exp_time", "relative", "trigger_source", - "capture_positions", ], } @@ -147,18 +160,15 @@ class AerotechContGrid(ScanBase): trigger_source: str | None = "ddg", trigger_edge: str = "rising", trigger_device: str = "pandatrig", - readout_time: float = 0.0, shutter_delay: float | None = None, settle_time: float = 0.15, - capture_positions: bool = False, capture_device: str = "aerocapture", **kwargs, # fmt: on ): """ Continuous grid scan for A3200 stages: the PSO output of the fast axis triggers the - start of each line, optionally the axis positions are latched at each detector - trigger. + start of each line, the axis positions are latched at each detector trigger. Args: fast_axis (DeviceBase): A3200 axis with continuous motion (needs pso_input and acceleration configured). @@ -174,11 +184,9 @@ class AerotechContGrid(ScanBase): trigger_source (str | None): "ddg" (beamline, DDG1/MCS), "panda" (PandABox line trigger) or None (no trigger hardware). Default is "ddg". trigger_edge (str): DDG1 external trigger edge, "rising" (opto output, J7-6/7) or "falling" (open collector / differential output). Default is "rising". trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". - readout_time (float): Detector readout time, gate width = exp_time - readout_time (trigger_source="panda"). Default is 0. shutter_delay (float | None): Delay from the line trigger to the first exposure in s. Default: DDG1 shutter delay for "ddg", 0 otherwise. settle_time (float): Run-up at constant velocity before the trigger position, in s. Default is 0.15. - capture_positions (bool): Latch the axis positions at each detector trigger. Default is False. - capture_device (str): Name of the A3200PositionCapture device. Default is "aerocapture". + capture_device (str): Name of the A3200PositionCapture device (always used). Default is "aerocapture". Returns: ScanReport @@ -231,28 +239,28 @@ class AerotechContGrid(ScanBase): self.relative = relative self.exp_time = exp_time self.trigger_source = trigger_source - self.readout_time = readout_time self.shutter_delay = shutter_delay self.settle_time = settle_time - self.capture_positions = capture_positions devices = self.device_manager.devices - self.ddg1 = self.mcs = self.panda = None + self.ddg1 = self.ddg2 = self.mcs = self.panda = None if trigger_source == "ddg": self.ddg1 = devices["ddg1"] + self.ddg2 = devices["ddg2"] self.mcs = devices["mcs"] elif trigger_source == "panda": if trigger_device not in devices: raise ScanAbortion(f"PandA line trigger device '{trigger_device}' not found.") self.panda = devices[trigger_device] - self.capture = None - if capture_positions: - if capture_device not in devices: - raise ScanAbortion( - f"Position capture device '{capture_device}' not found." - " Use capture_positions=False to scan without it." - ) - self.capture = devices[capture_device] + if capture_device not in devices: + raise ScanAbortion( + f"Position capture device '{capture_device}' not found; aerotech_cont_grid" + " always captures the positions (enable it in the device config)." + ) + self.capture = devices[capture_device] + self._captured_lines: list[dict] = [] + self._line_times: list[float] = [] + self._scan_start_time = None self.motors = [fast_axis, stepper_axis] self.frames_per_line = 0 @@ -330,8 +338,7 @@ class AerotechContGrid(ScanBase): scan velocity and arm the trigger receiver. """ self._premove_motor_status.wait() - if self.capture is not None: - self.capture.calibrate() + self.capture.calibrate() if "target_velocity" not in self._line: # pre_scan without prepare_scan self._compute_line_geometry() line = self._line @@ -343,10 +350,11 @@ class AerotechContGrid(ScanBase): self.ddg1.set_trigger(DDG_TRIGGER_EDGES[self.trigger_edge]) elif self.trigger_source == "panda": self.panda.configure_line( - self.frames_per_line, self.exp_time, self.readout_time, line["shutter_open_delay"] + self.frames_per_line, self.exp_time, line["readout_time"], line["shutter_open_delay"] ) self.panda.arm() self.actions.pre_scan_all_devices() + self._scan_start_time = time.time() @scan_hook def scan_core(self): @@ -375,8 +383,8 @@ class AerotechContGrid(ScanBase): Run one line: arm the position capture, move the fast axis through the PSO window, then check that the line was triggered and read the captured positions. """ - if self.capture is not None: - self.capture.arm(self.frames_per_line) + line_start_time = time.time() + self.capture.arm(self.frames_per_line) move_status = self.actions.set(motors, positions, wait=False) while not move_status.done: self.actions.read_monitored_devices(wait=True) @@ -390,13 +398,14 @@ class AerotechContGrid(ScanBase): elif self.trigger_source == "panda": self.panda.wait_lines_done(line_index + 1, timeout=self._line_duration() + 2) - if self.capture is not None: - self.capture.read_line() + self._line_times.append(time.time() - line_start_time) + self._captured_lines.append(self.capture.read_line(centre_shift=self._centre_shift())) @scan_hook def post_scan(self): """Restore trigger receiver, PSO and fast axis settings, complete devices.""" self._restore() + self._report_statistics() status = self.actions.complete_all_devices(wait=False) if self.relative: self.components.move_and_wait(self.motors, self.start_positions) @@ -429,6 +438,73 @@ class AerotechContGrid(ScanBase): def _is_forward(self, line_index: int) -> bool: return line_index % 2 == 0 + def _readout_time(self) -> float: + """Readout time (gap between the detector gates) of the trigger hardware, in s.""" + if self.trigger_source == "ddg": + return float(self.ddg2.effective_readout_times()["ab"]) + if self.trigger_source == "panda": + return float(self.panda.get_readout_time()) + return 0.0 + + def _centre_shift(self) -> float: + """Distance from the start to the centre of an exposure along the fast axis.""" + line = self._line + return line["target_velocity"] * (self.exp_time - line["readout_time"]) / 2 + + def _report_statistics(self): + """Log and send to the client: centre deviations of the fast axis and line times.""" + try: + summary = self._statistics() + except Exception as exc: # pylint: disable=broad-except + logger.warning(f"aerotech_cont_grid: no statistics: {exc}") + return + if summary: + logger.info(summary) + self.actions.send_client_info(summary) + + def _statistics(self) -> str: + lines = [d for d in self._captured_lines if d] + if not lines: + return "" + # fast axis = the centre channel with the largest span over a line + keys = [k for k in lines[0] if k.endswith("_centre")] + fast_key = max(keys, key=lambda k: abs(lines[0][k][-1] - lines[0][k][0])) + n, step = self.frames_per_line, self.fast_step_size + cells = self.fast_start + step * (np.arange(n) + 0.5) + centres = [] + for line_index, data in enumerate(lines): + c = np.asarray(data[fast_key], dtype=float) + if len(c) != n: + return f"aerotech_cont_grid: line {line_index}: {len(c)} positions, expected {n}." + centres.append(c if self._is_forward(line_index) else c[::-1]) + centres = np.array(centres) + dev = (centres - cells) * 1e3 # um, by cell; fast axis in controller units = mm + fwd, bwd = dev[0::2], dev[1::2] + text = [ + f"aerotech_cont_grid statistics ({len(lines)} lines x {n} points, v" + f" {self._line['target_velocity']:g} / s, readout {self._line['readout_time'] * 1e3:g} ms):", + f" centre - nominal cell centre [um]: forward mean {fwd.mean():+.2f} rms" + f" {fwd.std():.2f} max {np.abs(fwd).max():.2f}", + ] + if len(bwd): + pairs = min(len(fwd), len(bwd)) + diff = (bwd[:pairs] - fwd[:pairs]).mean(axis=0) + slope = np.polyfit(cells - cells.mean(), diff, 1)[0] if n > 1 else 0.0 + text += [ + f" centre - nominal cell centre [um]: backward mean {bwd.mean():+.2f} rms" + f" {bwd.std():.2f} max {np.abs(bwd).max():.2f}", + f" backward - forward [um]: mean {diff.mean():+.2f}, slope {slope:+.2f} / mm," + f" peak-peak {np.ptp(diff):.2f}", + ] + if self._line_times: + total = time.time() - self._scan_start_time if self._scan_start_time else 0.0 + text.append( + f" times: scan {total:.1f} s, line mean {np.mean(self._line_times):.2f} s" + f" (min {np.min(self._line_times):.2f}, max {np.max(self._line_times):.2f})," + f" exposure per line {n * self.exp_time:.2f} s" + ) + return "\n".join(text) + def _trigger_window(self, line_index: int) -> tuple[float, float]: """PSO window of a line: entered at the trigger position, open beyond the line end.""" line = self._line @@ -479,6 +555,12 @@ class AerotechContGrid(ScanBase): line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() else: line["shutter_open_delay"] = 0.0 + line["readout_time"] = self._readout_time() + if not 0 <= line["readout_time"] < self.exp_time: + raise ScanAbortion( + f"Readout time {line['readout_time']} s of the trigger hardware must be shorter" + f" than exp_time {self.exp_time} s." + ) line["original_velocity"] = self.fast_axis.motor_velocity.get() line["acceleration"] = self.fast_axis.motor_acceleration.get() @@ -497,11 +579,13 @@ class AerotechContGrid(ScanBase): ramp_distance = velocity**2 / (2 * line["acceleration"]) margin = RAMP_MARGIN_FRACTION * ramp_distance + velocity * self.settle_time lead = velocity * line["shutter_open_delay"] + # centre each gate (exp_time - readout_time) in its cell: same centres in both directions + gap = velocity * line["readout_time"] / 2 line["target_velocity"] = velocity line["exposure_end"] = self.fast_start + self.frames_per_line * self.fast_step_size - line["trigger_low"] = self.fast_start - lead - line["trigger_high"] = line["exposure_end"] + lead + line["trigger_low"] = self.fast_start - lead + gap + line["trigger_high"] = line["exposure_end"] + lead - gap line["line_start"] = line["trigger_low"] - ramp_distance - margin line["line_end"] = line["trigger_high"] + ramp_distance + margin diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index cba35c3a..a5f8c28a 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -322,7 +322,12 @@ def test_capture_line_publishes_positions(): assert result["enc6"] == pytest.approx([0.0, 0.2, 0.4, 0.6, 0.8]) assert result["enc2"] == pytest.approx([0.3] * 5) msg = capture.positions.get() - assert set(msg.signals) == {"aerocapture_positions_enc2", "aerocapture_positions_enc6"} + assert set(msg.signals) == { + "aerocapture_positions_enc2", + "aerocapture_positions_enc6", + "aerocapture_positions_enc2_centre", + "aerocapture_positions_enc6_centre", + } assert msg.metadata["async_update"] == {"type": "add", "max_shape": [None]} @@ -409,3 +414,29 @@ def test_move_without_in_position_wait(): def test_max_scan_velocity_defaults_to_velocity(): assert _sim_motor(velocity=5.0).max_scan_velocity.get() == 5.0 assert _sim_motor(velocity=5.0, max_scan_velocity=50.0).max_scan_velocity.get() == 50.0 + + +def test_capture_line_centre_shift(): + x, _, capture = _capture_setup() + capture.calibrate() + x.move(-1.0, wait=True) + capture.arm(5) + x.arm_line_trigger(0.0, 1.0) + x.move(2.0, wait=True) + + result = capture.read_line(centre_shift=0.1) + # X moves forward: shifted forward; Y does not move: unchanged + assert result["enc6_centre"] == pytest.approx([0.1, 0.3, 0.5, 0.7, 0.9]) + assert result["enc2_centre"] == pytest.approx(result["enc2"]) + + +def test_capture_line_centre_shift_backward(): + _, _, capture = _capture_setup() + capture.arm(3) + # backward line: X (channel 6) counts down, Y (channel 2) constant; interleaved 2, 6 + capture.controller.read_drive_array = lambda axis, start, count: [ + 3000, 60000, 3000, 40000, 3000, 20000 + ] + result = capture.read_line(centre_shift=0.1) + assert result["enc6_centre"] == pytest.approx([x - 0.1 for x in result["enc6"]]) + assert result["enc2_centre"] == pytest.approx(result["enc2"]) diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index de65cc24..6231dda2 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -59,12 +59,18 @@ def _add_devices( ) ddg1.get_shutter_to_open_delay = mock.MagicMock(return_value=2e-3) ddg1.set_trigger = mock.MagicMock() + ddg2 = MockCustomDevice( + "ddg2", + device_info={"signals": {"readback": _signal("ddg2", "hinted")}}, + signal_read_values={"ddg2": 0.0}, + ) + ddg2.effective_readout_times = mock.MagicMock(return_value={"ab": 2e-4, "cd": 2e-4}) mcs = MockCustomDevice( "mcs", device_info={"signals": {"readback": _signal("mcs", "hinted")}}, signal_read_values={"mcs": 0.0}, ) - devices = [aerox, aeroy, ddg1, mcs] + devices = [aerox, aeroy, ddg1, ddg2, mcs] if panda: pandatrig = MockCustomDevice( "pandatrig", @@ -73,6 +79,7 @@ def _add_devices( ) for method in ("configure_line", "arm", "disarm", "wait_lines_done"): setattr(pandatrig, method, mock.MagicMock()) + pandatrig.get_readout_time = mock.MagicMock(return_value=0.02) devices.append(pandatrig) if capture: aerocapture = MockCustomDevice( @@ -129,13 +136,15 @@ def test_line_geometry(v4_scan_assembler, device_manager): ramp = velocity**2 / (2 * 100.0) margin = 1.0 * ramp + velocity * 0.1 lead = velocity * 2e-3 # DDG1 shutter delay + gap = velocity * 2e-4 / 2 # half the DDG2 readout time: gates centred in their cells + assert line["readout_time"] == pytest.approx(2e-4) assert scan.frames_per_line == 5 assert line["num_lines"] == 3 assert line["target_velocity"] == pytest.approx(velocity) assert line["exposure_end"] == pytest.approx(-1.0 + 5 * 0.5) # PSO window: entered at the low edge going forward, at the high edge going backward - assert line["trigger_low"] == pytest.approx(-1.0 - lead) - assert line["trigger_high"] == pytest.approx(line["exposure_end"] + lead) + assert line["trigger_low"] == pytest.approx(-1.0 - lead + gap) + assert line["trigger_high"] == pytest.approx(line["exposure_end"] + lead - gap) assert line["line_start"] == pytest.approx(line["trigger_low"] - ramp - margin) assert line["line_end"] == pytest.approx(line["trigger_high"] + ramp + margin) assert scan.scan_info.frames_per_trigger == 5 @@ -155,6 +164,7 @@ def test_shutter_delay_without_ddg(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager, trigger_source=None) scan.prepare_scan() assert scan._line["shutter_open_delay"] == 0.0 + assert scan._line["readout_time"] == 0.0 assert scan._line["trigger_low"] == pytest.approx(-1.0) assert scan.ddg1 is None and scan.mcs is None @@ -162,7 +172,8 @@ def test_shutter_delay_without_ddg(v4_scan_assembler, device_manager): def test_shutter_delay_argument(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager, trigger_source="panda", shutter_delay=4e-3) scan.prepare_scan() - assert scan._line["trigger_low"] == pytest.approx(-1.0 - 5.0 * 4e-3) + # PandA readout time 20 ms (device setting): gap 5 mm/s * 10 ms + assert scan._line["trigger_low"] == pytest.approx(-1.0 - 5.0 * 4e-3 + 5.0 * 0.01) def test_invalid_trigger_source_aborts(v4_scan_assembler, device_manager): @@ -238,18 +249,25 @@ def test_missing_acceleration_aborts(v4_scan_assembler, device_manager): def test_missing_capture_device_aborts(v4_scan_assembler, device_manager): device_manager.devices.pop("aerocapture", None) with pytest.raises(ScanAbortion, match="aerocapture"): - _assemble(v4_scan_assembler, device_manager, capture=False, capture_positions=True) + _assemble(v4_scan_assembler, device_manager, capture=False) -def test_capture_is_off_by_default(v4_scan_assembler, device_manager): +def test_capture_is_always_used(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager) - assert scan.capture is None + assert scan.capture is device_manager.devices["aerocapture"] + + +def test_readout_time_longer_than_exposure_aborts(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + device_manager.devices["ddg2"].effective_readout_times.return_value = {"ab": 0.2} + with pytest.raises(ScanAbortion, match="Readout time"): + scan.prepare_scan() def test_pre_scan_arms_once_after_calibration( v4_scan_assembler, device_manager, nth_done_status_mock ): - scan = _assemble(v4_scan_assembler, device_manager, capture_positions=True) + scan = _assemble(v4_scan_assembler, device_manager) scan.prepare_scan() order = mock.MagicMock() scan.capture = order.capture @@ -298,7 +316,6 @@ def test_pre_scan_configures_and_arms_panda( v4_scan_assembler, device_manager, trigger_source="panda", - readout_time=0.02, shutter_delay=0.003, ) scan.prepare_scan() @@ -381,7 +398,7 @@ def test_ddg_missing_line_trigger_aborts(v4_scan_assembler, device_manager, nth_ def test_at_each_point_reads_capture(v4_scan_assembler, device_manager, nth_done_status_mock): scan = _ready_for_core( - _assemble(v4_scan_assembler, device_manager, trigger_source=None, capture_positions=True), + _assemble(v4_scan_assembler, device_manager, trigger_source=None), nth_done_status_mock, ) scan.capture = mock.MagicMock() @@ -389,7 +406,23 @@ def test_at_each_point_reads_capture(v4_scan_assembler, device_manager, nth_done scan.at_each_point([scan.fast_axis], np.array([scan._line["line_end"]]), line_index=0) scan.capture.arm.assert_called_once_with(5) - scan.capture.read_line.assert_called_once_with() + # no readout time without trigger hardware: half of 0.5 mm per exposure + scan.capture.read_line.assert_called_once_with(centre_shift=pytest.approx(0.25)) + + +def test_statistics_of_a_symmetric_snake(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, trigger_source=None) + scan.prepare_scan() + cells = -1.0 + 0.5 * (np.arange(5) + 0.5) + fwd = {"enc6_centre": list(cells + 1e-3), "enc2_centre": [0.0] * 5} + bwd = {"enc6_centre": list((cells + 3e-3)[::-1]), "enc2_centre": [0.5] * 5} + scan._captured_lines = [fwd, bwd, fwd] + scan._line_times = [1.0, 1.2, 1.1] + text = scan._statistics() + assert "forward mean +1.00" in text + assert "backward mean +3.00" in text + assert "backward - forward [um]: mean +2.00" in text and "peak-peak 0.00" in text + assert "line mean 1.10 s" in text def test_on_exception_restores_ddg_pso_and_axis(v4_scan_assembler, device_manager): -- 2.54.0 From 4d95cf755f059825efa75e11fce50f192a33a626 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 12:42:36 +0100 Subject: [PATCH 27/52] fix(aerotech): move settles only after holding the tolerance for 0.1 s After a hard stop (5 mm/s, 500 mm/s^2) X rings at ~10 Hz with ~20 um. The single position error check caught a zero crossing (-0.19 um), the move was declared settled 3.5 um off the target and failed (premove of a scan aborted). Now position error and distance to the target must stay within the tolerance for in_position_hold (0.1 s) without interruption. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/a3200.py | 31 ++++++++++++++++++++++++----- tests/tests_devices/test_a3200.py | 12 +++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index a7b1e7ba..b3eb69b8 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -524,6 +524,9 @@ class A3200Motor(Device, PositionerBase): position error to settle within tolerance before the position is compared with the target. The controller's In Position status is not used: its InPositionDistance can be tighter than the axis settles (X: 10 nm). + in_position_hold: position error and distance to the target must stay within + tolerance this long (s) before the move counts as settled. Default 0.1 s: after + a hard stop X rings at ~10 Hz, a single reading can be a zero crossing. acceleration: ramp rate for moves in user units/s^2; None keeps the controller's ramp settings (DefaultRampRate). max_scan_velocity: highest velocity a continuous scan may set on this axis, in user @@ -580,6 +583,7 @@ class A3200Motor(Device, PositionerBase): egu="mm", tolerance=0.001, in_position_timeout=2.0, + in_position_hold=0.1, require_homed=True, acceleration=None, max_scan_velocity=None, @@ -598,6 +602,7 @@ class A3200Motor(Device, PositionerBase): self.sign = sign self.tolerance = tolerance self.in_position_timeout = in_position_timeout + self.in_position_hold = in_position_hold self.require_homed = require_homed self.pso_input = pso_input self.pso_counts_divider = pso_counts_divider @@ -696,13 +701,29 @@ class A3200Motor(Device, PositionerBase): logger.warning(f"{self.name} stopped with faults {faults}.") self._done_moving(success=not faults) return - # the profile is done, the axis may still be settling + # the profile is done, the axis may still be settling: after a hard stop X rings + # at ~10 Hz with ~20 um, so a single small position error can be a zero crossing. + # Settled = position error and distance to the target within the tolerance for + # in_position_hold without interruption. deadline = time.time() + self.in_position_timeout - while abs(self.controller.position_error(self.axis_Id)) > self.tolerance: - if time.time() > deadline: + settled_since = None + while True: + now = time.time() + in_tolerance = ( + abs(self.controller.position_error(self.axis_Id)) <= self.tolerance + and abs(self.readback.get() - position) <= self.tolerance + ) + if not in_tolerance: + settled_since = None + elif settled_since is None: + settled_since = now + if settled_since is not None and now - settled_since >= self.in_position_hold: + break + if now > deadline: logger.warning( - f"{self.name}: position error not within {self.tolerance}" - f" {self.egu} {self.in_position_timeout} s after the end of the move." + f"{self.name}: not settled within {self.tolerance} {self.egu} for" + f" {self.in_position_hold} s, {self.in_position_timeout} s after the end" + " of the move." ) break time.sleep(0.02) diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index a5f8c28a..e69a744f 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -377,6 +377,18 @@ def test_move_waits_for_position_error_within_tolerance(): assert motor.controller.position_error("X") == 0.0 +def test_move_does_not_settle_on_a_zero_crossing(): + # ringing after the stop: one reading within tolerance, then out again, then settled + motor = _sim_motor(velocity=100.0, tolerance=0.001, in_position_hold=0.1) + errors = iter([0.0] + [0.01] * 5) + motor.controller.position_error = lambda axis: next(errors, 0.0) + start = time.time() + status = motor.move(1.0, wait=True) + # 5 readings out of tolerance (~0.1 s), then held for 0.1 s + assert time.time() - start >= 0.2 + assert status.success + + def test_move_ignores_in_position_tighter_than_tolerance(): # settled within tolerance, but outside the controller's InPositionDistance motor = _sim_motor(velocity=100.0, tolerance=0.001) -- 2.54.0 From 58636421ad49271226ceab9cc61caef6ba28ad1c Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 12:42:36 +0100 Subject: [PATCH 28/52] feat(aerotech): step axis moves during the run-out and turnaround of the fast axis As soon as the fast axis has left the exposure range (beyond the trigger position), the PSO window of the next line is set and the step axis starts; the next line starts when both have arrived. Saves ~0.15 s per line (2 mm/s, 0.8 mm: 1.07 -> 0.92 s per line incl. step; 20 mm/s: 0.85 -> 0.65-0.73 s). settle_time stays 0.15 s: shorter values at 20 mm/s gave -24 / +10 / +4.5 / -21 um backward - forward (0.08 / 0.04 / 0.02 / 0 s), a ~10 Hz oscillation after the ramp. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- .../scans/scans_v4/aerotech_cont_grid.py | 53 ++++++++++++++++--- .../test_aerotech_cont_grid_scan.py | 41 ++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index e6e1ea74..c767770c 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -59,7 +59,13 @@ checked against the soft limits before the scan starts. The scan velocity may be than the normal move velocity, up to max_scan_velocity of the fast axis; the move velocity is restored after the scan, also after an aborted scan. In a snake the run-out of a line is the run-up of the next one, so both sides are equal. At the turnarounds the fast axis does not wait for -In Position. +In Position. The step axis moves during the run-out and turnaround of the fast axis: as +soon as the fast axis has left the exposure range, the PSO window of the next line is set +and the step axis starts; the next line starts when both have arrived. + +settle_time stays 0.15 s also at high speed: at 20 mm/s / 500 mm/s^2 shorter values +(0.08 / 0.04 / 0.02 / 0 s) gave backward - forward offsets of -24 / +10 / +4.5 / -21 um, +a ~10 Hz oscillation after the ramp that decays within ~0.15 s (measured 2026-10-29). Scan procedure: - prepare_scan @@ -103,6 +109,9 @@ RAMP_MARGIN_FRACTION = 1.0 TRIGGER_SOURCES = ("ddg", "panda", None) +# Poll interval of the fast axis position while waiting for the end of the exposures, s +STEP_POLL_INTERVAL = 0.01 + class AerotechLineParameter(TypedDict): """Line geometry of the fast axis, computed in prepare_scan.""" @@ -259,6 +268,7 @@ class AerotechContGrid(ScanBase): ) self.capture = devices[capture_device] self._captured_lines: list[dict] = [] + self._step_status = None self._line_times: list[float] = [] self._scan_start_time = None @@ -358,14 +368,17 @@ class AerotechContGrid(ScanBase): @scan_hook def scan_core(self): - """One continuous line per step axis position.""" + """ + One continuous line per step axis position. The step to the next line is started by + at_each_point while the fast axis runs out and turns; it must be done before the + next line starts. + """ line = self._line + self._step_status = None for line_index in range(line["num_lines"]): - stepper_pos = self.positions[line_index][1] - if line_index > 0: - # fast axis at the turnaround, outside the window of the next line - self.fast_axis.set_line_trigger_window(*self._trigger_window(line_index)) - self.actions.set([self.stepper_axis], [stepper_pos]).wait() + if self._step_status is not None: + self._step_status.wait() + self._step_status = None target = line["line_end"] if self._is_forward(line_index) else line["line_start"] self.at_each_point( motors=[self.fast_axis], positions=np.array([target]), line_index=line_index @@ -381,11 +394,19 @@ class AerotechContGrid(ScanBase): ): """ Run one line: arm the position capture, move the fast axis through the PSO window, + start the step to the next line once the fast axis has left the exposure range, then check that the line was triggered and read the captured positions. """ line_start_time = time.time() self.capture.arm(self.frames_per_line) move_status = self.actions.set(motors, positions, wait=False) + next_line = line_index + 1 if line_index + 1 < self._line["num_lines"] else None + while next_line is not None and not move_status.done: + if self._past_exposures(line_index): + break + time.sleep(STEP_POLL_INTERVAL) + if next_line is not None: + self._start_step(next_line) while not move_status.done: self.actions.read_monitored_devices(wait=True) try: @@ -438,6 +459,24 @@ class AerotechContGrid(ScanBase): def _is_forward(self, line_index: int) -> bool: return line_index % 2 == 0 + def _past_exposures(self, line_index: int) -> bool: + """The fast axis has left the exposure range of the line (beyond its trigger position).""" + position = self.fast_axis.readback.get() + if self._is_forward(line_index): + return position > self._line["trigger_high"] + return position < self._line["trigger_low"] + + def _start_step(self, line_index: int) -> None: + """ + Prepare line line_index while the fast axis runs out: switch the PSO window (the + fast axis is beyond the trigger position, i.e. outside the new window) and start the + step axis. + """ + self.fast_axis.set_line_trigger_window(*self._trigger_window(line_index)) + self._step_status = self.actions.set( + [self.stepper_axis], [self.positions[line_index][1]], wait=False + ) + def _readout_time(self) -> float: """Readout time (gap between the detector gates) of the trigger hardware, in s.""" if self.trigger_source == "ddg": diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 6231dda2..8b3cfe86 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -452,3 +452,44 @@ def test_restore_continues_after_failing_step(v4_scan_assembler, device_manager) scan.on_exception(RuntimeError("test")) scan.fast_axis.disarm_line_trigger.assert_called_once_with() scan.fast_axis.wait_in_position.put.assert_called_once_with(True) + + +def test_step_starts_when_fast_axis_left_the_exposures( + v4_scan_assembler, device_manager, nth_done_status_mock +): + scan = _ready_for_core( + _assemble(v4_scan_assembler, device_manager, trigger_source=None), nth_done_status_mock + ) + line = scan._line + events = [] + fast_moves = [] + + def set_side_effect(motors, positions, **kwargs): + if motors == [scan.fast_axis]: + status = mock.MagicMock() + status.done = False # still running out when the step starts + status.wait.side_effect = lambda *a, **k: setattr(status, "done", True) + fast_moves.append(status) + events.append(("fast", float(positions[0]))) + return status + events.append(("step", float(positions[0]), [s.done for s in fast_moves])) + return nth_done_status_mock(resolve_after=1) + + scan.actions.set = mock.MagicMock(side_effect=set_side_effect) + # beyond the high trigger position after forward lines, below the low one after backward + scan.fast_axis.readback.get.side_effect = [line["line_end"], line["line_start"]] + + scan.scan_core() + + assert events == [ + ("fast", line["line_end"]), + ("step", 0.0, [False]), + ("fast", line["line_start"]), + ("step", 2.0, [True, False]), + ("fast", line["line_end"]), + ] + span = line["line_end"] - line["line_start"] + assert scan.fast_axis.set_line_trigger_window.call_args_list == [ + mock.call(line["line_start"] - span, line["trigger_high"]), + mock.call(line["trigger_low"], line["line_end"] + span), + ] -- 2.54.0 From e79314c77f21e9e22082be9571d2d528d516ea2e Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 12:50:36 +0100 Subject: [PATCH 29/52] fix(panda): rename software_trigger to soft_line_trigger software_trigger is a DeviceBase name in BEC, so the device server refused to load pandatrig ("User access method name {'software_trigger'} is protected"). New test: USER_ACCESS of the PandA and A3200 devices has no protected names. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/aerotech/README.md | 2 +- .../devices/panda_box/panda_line_trigger.py | 4 +-- .../tests_devices/test_panda_line_trigger.py | 35 ++++++++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index b64cceb1..3f772436 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -179,7 +179,7 @@ the wiring at connect. ```python dev.pandatrig.configure_line(n_pulses=100, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) dev.pandatrig.arm() # arm(one_shot=True): only the next edge -dev.pandatrig.software_trigger() # or the PSO edges on TTLIN3, one per line +dev.pandatrig.soft_line_trigger() # or the PSO edges on TTLIN3, one per line dev.pandatrig.wait_lines_done(n_lines=1, timeout=5) dev.pandatrig.line_status() # triggers, lines, pulses, dropped since arm() ``` diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 6d9660a0..5cf85a09 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -120,7 +120,7 @@ class PandaLineTrigger(Device): "configure_line", "arm", "disarm", - "software_trigger", + "soft_line_trigger", "line_status", "wait_lines_done", "get_readout_time", @@ -366,7 +366,7 @@ class PandaLineTrigger(Device): self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "") self.panda.set(self.SOFT_TRIGGER_BIT, "0") - def software_trigger(self) -> None: + def soft_line_trigger(self) -> None: """Line trigger edge from software (accepted like a TTLIN3 edge).""" self.panda.set(self.SOFT_TRIGGER_BIT, "1") self.panda.set(self.SOFT_TRIGGER_BIT, "0") diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 9febf180..65edee41 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -98,7 +98,7 @@ def test_never_toggles_pulse_enable_after_setup(trigger): trigger.configure_line(5, exp_time=0.01) trigger.panda.writes.clear() trigger.arm() - trigger.software_trigger() + trigger.soft_line_trigger() trigger.wait_lines_done(1, timeout=1) trigger.disarm() trigger.stop() @@ -109,7 +109,7 @@ def test_stays_armed_for_many_lines(trigger): trigger.configure_line(5, exp_time=0.01) trigger.arm() for _ in range(4): - trigger.software_trigger() + trigger.soft_line_trigger() status = trigger.wait_lines_done(4, timeout=1) assert (status["lines"], status["pulses"], status["armed"]) == (4, 20, True) @@ -117,9 +117,9 @@ def test_stays_armed_for_many_lines(trigger): def test_one_shot_accepts_one_edge(trigger): trigger.configure_line(5, exp_time=0.01) trigger.arm(one_shot=True) - trigger.software_trigger() + trigger.soft_line_trigger() assert trigger.wait_lines_done(1, timeout=1)["armed"] is False - trigger.software_trigger() + trigger.soft_line_trigger() status = trigger.wait_lines_done(1, timeout=1) assert status["triggers"] == 2 and status["lines"] == 1 @@ -127,9 +127,9 @@ def test_one_shot_accepts_one_edge(trigger): def test_edge_during_line_is_ignored(trigger): trigger.configure_line(5, exp_time=0.01) trigger.arm() - trigger.software_trigger() + trigger.soft_line_trigger() trigger.panda.busy = True - trigger.software_trigger() + trigger.soft_line_trigger() trigger.panda.busy = False status = trigger.wait_lines_done(1, timeout=1) assert status["triggers"] == 2 and status["lines"] == 1 and status["pulses"] == 5 @@ -151,7 +151,7 @@ def test_wait_lines_done_reports_too_many_lines(trigger): trigger.configure_line(5, exp_time=0.01) trigger.arm() for _ in range(3): - trigger.software_trigger() + trigger.soft_line_trigger() with pytest.raises(PandaLineTriggerError, match="expected 2 line"): trigger.wait_lines_done(2, timeout=0.1, poll=0.01) @@ -195,7 +195,7 @@ def test_disarm_closes_the_gate(trigger): trigger.configure_line(5, exp_time=0.01) trigger.arm() trigger.disarm() - trigger.software_trigger() + trigger.soft_line_trigger() status = trigger.line_status() assert (status["armed"], status["lines"], status["pulses"]) == (False, 0, 0) @@ -255,3 +255,22 @@ def test_unchanged_fields_are_not_written(trigger): again.configure_line(5, 0.01, 0.002) values = [w for w in again.panda.writes if not w.split("=", 1)[1] == ""] assert values == [] + + +@pytest.mark.parametrize( + "device_cls", + [ + "csaxs_bec.devices.panda_box.panda_line_trigger.PandaLineTrigger", + "csaxs_bec.devices.aerotech.a3200.A3200Motor", + "csaxs_bec.devices.aerotech.a3200.A3200PositionCapture", + ], +) +def test_user_access_names_not_protected_by_bec(device_cls): + # the device server refuses to load a device whose USER_ACCESS shadows a DeviceBase name + import importlib + + from bec_server.device_server.devices.device_serializer import get_protected_class_methods + + module, name = device_cls.rsplit(".", 1) + cls = getattr(importlib.import_module(module), name) + assert not set(cls.USER_ACCESS) & set(get_protected_class_methods()) -- 2.54.0 From aa49276d8d059bd527373c4f4bf4ba8b5661abaa Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 12:59:53 +0100 Subject: [PATCH 30/52] fix(aerotech): calibrate returns string keys so the scan does not hang calibrate() returned {channel: offset} with integer keys. The scan server cannot decode an RPC reply with integer map keys (msgpack strict_map_key), so the calibrate status never completed and aerotech_cont_grid waited forever in pre_scan. Now {"enc": offset}; new test round-trips the RPC return values of the capture device through the BEC serializer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/aerotech/a3200.py | 8 ++++++-- tests/tests_devices/test_a3200.py | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index b3eb69b8..5360403b 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -890,12 +890,16 @@ class A3200PositionCapture(PSIDeviceBase): frames_per_trigger = getattr(frames, "frames_per_trigger", 1) if frames else 1 self._correlation_group = "monitored" if frames_per_trigger == 1 else "burst_group" - def calibrate(self) -> dict[int, float]: + def calibrate(self) -> dict[str, float]: """ Determine the offset between encoder counts and program position of each channel: one capture triggered by a software PSO fire with the axes at rest. Fires the PSO output once: the line trigger receiver (DDG) must not be armed. + + Returns the offsets by signal name (enc). String keys: the return value + of an RPC call is sent with msgpack, which rejects integer map keys, and the scan + would wait forever for the call to finish. """ channels = sorted(self.channels) self.controller.dataacq_arm( @@ -910,7 +914,7 @@ class A3200PositionCapture(PSIDeviceBase): axis = self.channels[ch] position = self.controller.get_position(axis) self._offsets[ch] = position - counts / self.controller.counts_per_unit(axis) - return dict(self._offsets) + return {f"enc{ch}": offset for ch, offset in self._offsets.items()} def arm(self, n_triggers: int) -> None: """Arm the capture of the next n_triggers trigger edges (start of the array).""" diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index e69a744f..a62e405d 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -305,8 +305,22 @@ def test_line_trigger_needs_pso_input(): def test_capture_calibration_removes_counter_offset(): _, _, capture = _capture_setup() offsets = capture.calibrate() - assert offsets[6] == pytest.approx(-777 / 20000) - assert offsets[2] == pytest.approx(55 / 10000) + assert offsets["enc6"] == pytest.approx(-777 / 20000) + assert offsets["enc2"] == pytest.approx(55 / 10000) + + +def test_capture_rpc_returns_survive_msgpack(): + # an RPC return value that msgpack cannot decode leaves the scan waiting forever + from bec_lib.serialization import MsgpackSerialization + + x, _, capture = _capture_setup() + returns = [capture.calibrate()] + x.move(-1.0, wait=True) + capture.arm(5) + x.move(1.0, wait=True) + returns.append(capture.read_line()) + for value in returns: + assert MsgpackSerialization.loads(MsgpackSerialization.dumps(value)) == value def test_capture_line_publishes_positions(): -- 2.54.0 From e57ae0ddc6073451c94005f1220ee98d988c3b63 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 13:09:48 +0100 Subject: [PATCH 31/52] fix(aerotech): set_line_trigger_window in USER_ACCESS aerotech_cont_grid switches the PSO window per line through RPC, which only exposes USER_ACCESS methods: the scan failed after the first line with "'Positioner' object has no attribute 'set_line_trigger_window'". New test: every method the scan calls on the fast axis, the capture device and the PandA is in the device's USER_ACCESS. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/aerotech/a3200.py | 1 + .../test_aerotech_cont_grid_scan.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index 5360403b..ff34a60e 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -547,6 +547,7 @@ class A3200Motor(Device, PositionerBase): "fault_ack", "status", "arm_line_trigger", + "set_line_trigger_window", "disarm_line_trigger", ] readback = Cpt(A3200ReadbackSignal, signal_name="readback", kind="hinted") diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 8b3cfe86..575a4a48 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -493,3 +493,29 @@ def test_step_starts_when_fast_axis_left_the_exposures( mock.call(line["line_start"] - span, line["trigger_high"]), mock.call(line["trigger_low"], line["line_end"] + span), ] + + +@pytest.mark.parametrize( + ("attribute", "device_cls"), + [ + ("fast_axis", "csaxs_bec.devices.aerotech.a3200.A3200Motor"), + ("capture", "csaxs_bec.devices.aerotech.a3200.A3200PositionCapture"), + ("panda", "csaxs_bec.devices.panda_box.panda_line_trigger.PandaLineTrigger"), + ], +) +def test_scan_calls_only_user_access_methods(attribute, device_cls): + # the scan server reaches device methods through RPC, which only exposes USER_ACCESS + import importlib + import inspect + import re + + from csaxs_bec.scans.scans_v4 import aerotech_cont_grid + + module, name = device_cls.rsplit(".", 1) + cls = getattr(importlib.import_module(module), name) + source = inspect.getsource(aerotech_cont_grid) + called = set(re.findall(rf"self\.{attribute}\.(\w+)\(", source)) + called |= set(re.findall(rf"self\.{attribute}\.(\w+)\)", source)) # passed as callable + signals = {"motor_velocity", "wait_in_position", "max_scan_velocity", "motor_acceleration"} + assert called, f"no calls on self.{attribute} found" + assert not (called - signals) - set(cls.USER_ACCESS) -- 2.54.0 From 06ce7b075116875872b7467ea673820586c13148 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 13:12:45 +0100 Subject: [PATCH 32/52] fix(aerotech): scan checks raise alarms so the user sees the reason The scan worker only logs a ScanAbortion; the client then shows just "Scan ... was interrupted" (seen 2026-10-29 with the run-up limit check). The checks of aerotech_cont_grid now raise LimitError (limits), ValueError (parameters, missing devices) and RuntimeError (missing line trigger), which the scan worker turns into an alarm with the message, and still runs on_exception. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../scans/scans_v4/aerotech_cont_grid.py | 33 ++++++++++--------- .../test_aerotech_cont_grid_scan.py | 24 +++++++------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index c767770c..e07ea041 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -89,7 +89,10 @@ import numpy as np from bec_lib.device import DeviceBase from bec_lib.logger import bec_logger from bec_lib.scan_args import DefaultArgType, ScanArgument -from bec_server.scan_server.errors import ScanAbortion +# Checks raise LimitError / ValueError / RuntimeError, not ScanAbortion: the scan worker +# only logs a ScanAbortion, the client then shows just "Scan ... was interrupted". Other +# exceptions raise an alarm, so the message is shown in the client. +from bec_server.scan_server.errors import LimitError from bec_server.scan_server.scans.scan_base import ScanBase, ScanType from bec_server.scan_server.scans.scan_modifier import scan_hook @@ -227,11 +230,11 @@ class AerotechContGrid(ScanBase): self.components = CsaxsBecScanComponents(self) self._baseline_readout_status = None if trigger_source not in TRIGGER_SOURCES: - raise ScanAbortion( + raise ValueError( f"trigger_source must be one of {TRIGGER_SOURCES}, got {trigger_source!r}." ) if trigger_edge not in DDG_TRIGGER_EDGES: - raise ScanAbortion( + raise ValueError( f"trigger_edge must be one of {list(DDG_TRIGGER_EDGES)}, got {trigger_edge!r}." ) self.trigger_edge = trigger_edge @@ -259,10 +262,10 @@ class AerotechContGrid(ScanBase): self.mcs = devices["mcs"] elif trigger_source == "panda": if trigger_device not in devices: - raise ScanAbortion(f"PandA line trigger device '{trigger_device}' not found.") + raise ValueError(f"PandA line trigger device '{trigger_device}' not found.") self.panda = devices[trigger_device] if capture_device not in devices: - raise ScanAbortion( + raise ValueError( f"Position capture device '{capture_device}' not found; aerotech_cont_grid" " always captures the positions (enable it in the device config)." ) @@ -562,7 +565,7 @@ class AerotechContGrid(ScanBase): deadline = time.time() + timeout while (channel := self.mcs.current_channel.get()) < expected: if time.time() > deadline: - raise ScanAbortion( + raise RuntimeError( f"MCS card at channel {channel} after line {line_index}, expected" f" {expected}. Was the line trigger received? Check the PSO output ->" " DDG1 EXT wiring and trigger_edge." @@ -575,15 +578,15 @@ class AerotechContGrid(ScanBase): def _check_motor_inputs(self): if np.isclose(self.fast_start, self.fast_end, atol=self.fast_step_size): - raise ScanAbortion( + raise ValueError( f"Fast stop {self.fast_end} and fast start {self.fast_start} positions are too close for the given step size {self.fast_step_size}." ) if np.isclose(self.stepper_start, self.stepper_stop, atol=self.stepper_step_size): - raise ScanAbortion( + raise ValueError( f"Stepper stop {self.stepper_stop} and stepper start {self.stepper_start} positions are too close for the given step size {self.stepper_step_size}." ) if self.settle_time < 0: - raise ScanAbortion(f"settle_time must be >= 0, got {self.settle_time}.") + raise ValueError(f"settle_time must be >= 0, got {self.settle_time}.") def _compute_line_geometry(self): """Velocity, PSO window and run-up / run-out of the fast axis.""" @@ -596,7 +599,7 @@ class AerotechContGrid(ScanBase): line["shutter_open_delay"] = 0.0 line["readout_time"] = self._readout_time() if not 0 <= line["readout_time"] < self.exp_time: - raise ScanAbortion( + raise ValueError( f"Readout time {line['readout_time']} s of the trigger hardware must be shorter" f" than exp_time {self.exp_time} s." ) @@ -606,12 +609,12 @@ class AerotechContGrid(ScanBase): velocity = self.fast_step_size / self.exp_time max_velocity = self.fast_axis.max_scan_velocity.get() if velocity > max_velocity: - raise ScanAbortion( + raise ValueError( f"Requested velocity of {velocity} (fast_step_size / exp_time) exceeds the" f" maximum scan velocity {max_velocity} configured for {self.fast_axis.name}." ) if not line["acceleration"] or line["acceleration"] <= 0: - raise ScanAbortion( + raise ValueError( f"No acceleration configured for {self.fast_axis.name}; it is needed for the" " run-up distance of the line." ) @@ -634,7 +637,7 @@ class AerotechContGrid(ScanBase): name = self.fast_axis.name low, high = self.fast_axis.limits if low < high and (self.fast_start < low or self.fast_end > high): - raise ScanAbortion( + raise LimitError( f"Scan range of {name} {self.fast_start:.4f} .. {self.fast_end:.4f} is outside" f" the limits ({low}, {high})." ) @@ -644,7 +647,7 @@ class AerotechContGrid(ScanBase): beyond = f"beyond the high limit {high} by {line['line_end'] - high:.4f}" else: beyond = f"beyond the low limit {low} by {low - line['line_start']:.4f}" - raise ScanAbortion( + raise LimitError( f"The scan range of {name} {self.fast_start:.4f} .. {self.fast_end:.4f} is" f" within the limits, but {name} needs {extra:.4f} extra travel on each side" f" to accelerate to the scan velocity {line['target_velocity']:g} / s and" @@ -656,7 +659,7 @@ class AerotechContGrid(ScanBase): low, high = self.stepper_axis.limits stepper_range = sorted((self.stepper_start, self.stepper_stop)) if low < high and (stepper_range[0] < low or stepper_range[1] > high): - raise ScanAbortion( + raise LimitError( f"{self.stepper_axis.name} range {stepper_range[0]:.4f} .. {stepper_range[1]:.4f}" f" is outside the limits ({low}, {high})." ) diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 575a4a48..c73d3eb1 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -2,7 +2,7 @@ from unittest import mock import numpy as np import pytest -from bec_server.scan_server.errors import ScanAbortion +from bec_server.scan_server.errors import LimitError from bec_server.scan_server.tests.scan_fixtures import MockCustomDevice from bec_server.scan_server.tests.scan_hook_tests import ( DEFAULT_HOOK_TESTS, @@ -177,13 +177,13 @@ def test_shutter_delay_argument(v4_scan_assembler, device_manager): def test_invalid_trigger_source_aborts(v4_scan_assembler, device_manager): - with pytest.raises(ScanAbortion, match="trigger_source"): + with pytest.raises(ValueError, match="trigger_source"): _assemble(v4_scan_assembler, device_manager, trigger_source="rio") def test_missing_panda_aborts(v4_scan_assembler, device_manager): device_manager.devices.pop("pandatrig", None) - with pytest.raises(ScanAbortion, match="pandatrig"): + with pytest.raises(ValueError, match="pandatrig"): _assemble(v4_scan_assembler, device_manager, panda=False, trigger_source="panda") @@ -192,7 +192,7 @@ def test_velocity_above_configured_velocity_aborts(v4_scan_assembler, device_man scan = v4_scan_assembler( "aerotech_cont_grid", "aerox", -10.0, 10.0, 2.0, "aeroy", -2.0, 2.0, 2, exp_time=0.1 ) - with pytest.raises(ScanAbortion, match="exceeds the maximum scan velocity"): + with pytest.raises(ValueError, match="exceeds the maximum scan velocity"): scan.prepare_scan() @@ -218,7 +218,7 @@ def test_run_up_outside_limits_aborts(v4_scan_assembler, device_manager): scan = v4_scan_assembler( "aerotech_cont_grid", "aerox", -4.0, 4.0, 0.1, "aeroy", -2.0, 2.0, 2, exp_time=0.005 ) - with pytest.raises(ScanAbortion, match="within the limits, but aerox needs .* extra travel"): + with pytest.raises(LimitError, match="within the limits, but aerox needs .* extra travel"): scan.prepare_scan() @@ -227,7 +227,7 @@ def test_scan_range_outside_limits_aborts(v4_scan_assembler, device_manager): scan = v4_scan_assembler( "aerotech_cont_grid", "aerox", -1.0, 11.0, 0.5, "aeroy", -2.0, 2.0, 2, exp_time=0.1 ) - with pytest.raises(ScanAbortion, match="Scan range of aerox"): + with pytest.raises(LimitError, match="Scan range of aerox"): scan.prepare_scan() @@ -236,19 +236,19 @@ def test_stepper_outside_limits_aborts(v4_scan_assembler, device_manager): scan = v4_scan_assembler( "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.5, "aeroy", -2.0, 12.0, 2, exp_time=0.1 ) - with pytest.raises(ScanAbortion, match="aeroy range"): + with pytest.raises(LimitError, match="aeroy range"): scan.prepare_scan() def test_missing_acceleration_aborts(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager, acceleration=0.0) - with pytest.raises(ScanAbortion, match="No acceleration"): + with pytest.raises(ValueError, match="No acceleration"): scan.prepare_scan() def test_missing_capture_device_aborts(v4_scan_assembler, device_manager): device_manager.devices.pop("aerocapture", None) - with pytest.raises(ScanAbortion, match="aerocapture"): + with pytest.raises(ValueError, match="aerocapture"): _assemble(v4_scan_assembler, device_manager, capture=False) @@ -260,7 +260,7 @@ def test_capture_is_always_used(v4_scan_assembler, device_manager): def test_readout_time_longer_than_exposure_aborts(v4_scan_assembler, device_manager): scan = _assemble(v4_scan_assembler, device_manager) device_manager.devices["ddg2"].effective_readout_times.return_value = {"ab": 0.2} - with pytest.raises(ScanAbortion, match="Readout time"): + with pytest.raises(ValueError, match="Readout time"): scan.prepare_scan() @@ -305,7 +305,7 @@ def test_ddg_falling_edge_for_open_collector_output( def test_invalid_trigger_edge_aborts(v4_scan_assembler, device_manager): - with pytest.raises(ScanAbortion, match="trigger_edge"): + with pytest.raises(ValueError, match="trigger_edge"): _assemble(v4_scan_assembler, device_manager, trigger_edge="both") @@ -392,7 +392,7 @@ def test_ddg_missing_line_trigger_aborts(v4_scan_assembler, device_manager, nth_ scan = _ready_for_core(_assemble(v4_scan_assembler, device_manager), nth_done_status_mock) scan.mcs = mock.MagicMock() scan.mcs.current_channel.get.return_value = 0 - with pytest.raises(ScanAbortion, match="expected 4"): + with pytest.raises(RuntimeError, match="expected 4"): scan._wait_mcs_frames(0, timeout=0.1, poll=0.02) -- 2.54.0 From 723d13d8cc2e70d9a7a0f9c06add1ce926a97514 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 13:25:15 +0100 Subject: [PATCH 33/52] fix(aerotech): captured positions in the user coordinates of the BEC motors aerocapture published the latched positions (and centres) in controller coordinates; with sign: -1 in the device config they had the opposite sign of the motor readback and the scan statistics compared different frames. The capture device now looks up the A3200Motor of each channel (same controller, same axis) on the device server and applies its sign; without such a motor it keeps sign 1 and logs a warning. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 1 + csaxs_bec/devices/aerotech/a3200.py | 30 ++++++++++++++++-- .../scans/scans_v4/aerotech_cont_grid.py | 2 +- tests/tests_devices/test_a3200.py | 31 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 3f772436..f53944e6 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -105,6 +105,7 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 default), trigger 0 at the falling one (gate end); after each line they are read back (~2600 values/s over the ASCII interface) and published as async data, both as captured (`aerocapture_positions_enc`, start of each exposure) and as exposure centres (`aerocapture_positions_enc_centre`, + in the user coordinates of the BEC motor of the channel, i.e. its `sign` applied, + `v * gate_width / 2` in the direction of motion for the moving fast axis). The counter offset is calibrated in pre_scan with one software PSO fire, before any trigger receiver is armed. At the end the scan sends statistics to the client: centre - nominal cell diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index ff34a60e..56d245fc 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -820,7 +820,9 @@ class A3200PositionCapture(PSIDeviceBase): 0-5 local encoder counter (square wave encoders), 6-11 encoder multiplier (MXR) channel 0-5 (sine encoders, trigger rate max. 4 kHz). The channel of an axis is its PositionFeedbackChannel, by default its axis index in the Npaq. Values are - in controller coordinates (the BEC motor sign is not applied). + published in BEC user coordinates of the A3200Motor with this axis_Id on the + same controller (its sign applied), i.e. in the same coordinates as the motor + readback; without such a motor the sign is 1. drive_axis: any axis of the Npaq, used to address the drive (shared array). trigger_input: DATAACQ trigger, 3 = rising TTL edge at UINT1 (gate start, default), 0 = falling TTL edge (gate end); see DATAACQ_TRIGGER_UINT1_TTL_RISING. @@ -867,6 +869,7 @@ class A3200PositionCapture(PSIDeviceBase): self.drive_axis = drive_axis self.trigger_input = trigger_input self._offsets = {ch: 0.0 for ch in self.channels} + self._signs = None # channel -> sign of the BEC motor, resolved in calibrate() self._n_triggers = 0 self._correlation_group = "monitored" super().__init__( @@ -915,8 +918,28 @@ class A3200PositionCapture(PSIDeviceBase): axis = self.channels[ch] position = self.controller.get_position(axis) self._offsets[ch] = position - counts / self.controller.counts_per_unit(axis) + self._signs = self._motor_signs() return {f"enc{ch}": offset for ch, offset in self._offsets.items()} + def _motor_signs(self) -> dict[int, int]: + """Sign of the BEC motor of each channel: A3200Motor with the same controller and axis.""" + motors = {} + devices = getattr(self.device_manager, "devices", None) or {} + for device in list(devices.values()): + obj = getattr(device, "obj", device) + if isinstance(obj, A3200Motor) and obj.controller is self.controller: + motors[obj.axis_Id] = obj + signs = {} + for ch, axis in self.channels.items(): + motor = motors.get(axis) + if motor is None: + logger.warning( + f"{self.name}: no A3200Motor for axis {axis} on {self.controller}; channel" + f" {ch} is published in controller coordinates (sign 1)." + ) + signs[ch] = motor.sign if motor is not None else 1 + return signs + def arm(self, n_triggers: int) -> None: """Arm the capture of the next n_triggers trigger edges (start of the array).""" self._n_triggers = int(n_triggers) @@ -938,6 +961,8 @@ class A3200PositionCapture(PSIDeviceBase): Returns the positions too. """ channels = sorted(self.channels) + if self._signs is None: + self._signs = self._motor_signs() raw = self.controller.read_drive_array(self.drive_axis, 0, self._n_triggers * len(channels)) # Npaq interleaves the inputs per trigger, in ascending channel order data = np.asarray(raw, dtype=float).reshape(self._n_triggers, len(channels)) @@ -945,7 +970,8 @@ class A3200PositionCapture(PSIDeviceBase): result = {} for i, ch in enumerate(channels): cpu = self.controller.counts_per_unit(self.channels[ch]) - captured = data[:, i] / cpu + self._offsets[ch] + # controller coordinates -> user coordinates of the BEC motor + captured = self._signs[ch] * (data[:, i] / cpu + self._offsets[ch]) span = captured[-1] - captured[0] if len(captured) > 1 else 0.0 shift = np.sign(span) * centre_shift if abs(span) > 2 * abs(centre_shift) else 0.0 result[f"enc{ch}"] = captured diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index e07ea041..5940b142 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -520,7 +520,7 @@ class AerotechContGrid(ScanBase): return f"aerotech_cont_grid: line {line_index}: {len(c)} positions, expected {n}." centres.append(c if self._is_forward(line_index) else c[::-1]) centres = np.array(centres) - dev = (centres - cells) * 1e3 # um, by cell; fast axis in controller units = mm + dev = (centres - cells) * 1e3 # um, by cell; positions in motor user coordinates fwd, bwd = dev[0::2], dev[1::2] text = [ f"aerotech_cont_grid statistics ({len(lines)} lines x {n} points, v" diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index a62e405d..f09ed2e3 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -466,3 +466,34 @@ def test_capture_line_centre_shift_backward(): result = capture.read_line(centre_shift=0.1) assert result["enc6_centre"] == pytest.approx([x - 0.1 for x in result["enc6"]]) assert result["enc2_centre"] == pytest.approx(result["enc2"]) + + +def test_capture_applies_the_motor_sign(): + """Captured positions are in the coordinates of the BEC motors (sign from their config).""" + x = _sim_motor("X", axis_nr=0, pso_input=6, sim_counts_per_unit=20000, sign=-1) + y = _sim_motor("Y", axis_nr=2, pso_input=2, sim_counts_per_unit=10000) + dm = DMMock() + dm.devices["aerox"] = x + dm.devices["aeroy"] = y + capture = SimA3200PositionCapture( + name="aerocapture", host=_HOST, port=_PORT, channels={6: "X", 2: "Y"}, device_manager=dm + ) + capture.wait_for_connection() + capture.calibrate() + assert capture._signs == {6: -1, 2: 1} + capture._offsets = {6: 0.0, 2: 0.0} + capture.arm(3) + # forward line in user coordinates = X counts going down; interleaved channels 2, 6 + capture.controller.read_drive_array = lambda axis, start, count: [ + 3000, -20000, 3000, -24000, 3000, -28000 + ] + result = capture.read_line(centre_shift=0.1) + assert result["enc6"] == pytest.approx([1.0, 1.2, 1.4]) + assert result["enc6_centre"] == pytest.approx([1.1, 1.3, 1.5]) + assert result["enc2"] == pytest.approx([0.3] * 3) + + +def test_capture_without_motor_uses_controller_coordinates(): + _, _, capture = _capture_setup() # DMMock without motors + capture.calibrate() + assert capture._signs == {6: 1, 2: 1} -- 2.54.0 From 96c30dbd3a4f31a2748a2bc9c5e4758305be5ced Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 13:31:10 +0100 Subject: [PATCH 34/52] fix(panda): ignored trigger edges logged per line as info, not as a growing warning wait_lines_done warned after every line with the total of ignored edges since arm (20/10, 23/11, 26/13 ... in a 2026-10-29 scan), which read like an escalating trigger error. These edges are harmless (encoder noise at the PSO window entry while the line runs; lines and pulses are checked and raise on a mismatch), so only the edges new since the last line are logged, at info level. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../devices/panda_box/panda_line_trigger.py | 16 +++++++++---- .../tests_devices/test_panda_line_trigger.py | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 5cf85a09..6bd4a9e4 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -163,6 +163,7 @@ class PandaLineTrigger(Device): self.trigger_edge = trigger_edge self._pulse_params = None # parameters written to the PULSE blocks self._dropped_at_arm = 0 # DROPPED is only zeroed by ENABLE, which is never toggled + self._ignored_reported = 0 # ignored edges since arm() already logged self.device_manager = device_manager super().__init__(prefix, name=name, **kwargs) @@ -359,6 +360,7 @@ class PandaLineTrigger(Device): self.panda.set(f"{counter}.ENABLE", "ZERO") self.panda.set(f"{counter}.ENABLE", "ONE") self._dropped_at_arm = int(self.panda.get(f"{self.DETECTOR_PULSE}.DROPPED")) + self._ignored_reported = 0 self.panda.set(f"{gate}.FORCE_SET", "") def disarm(self) -> None: @@ -415,9 +417,15 @@ class PandaLineTrigger(Device): f"{self.name}: expected {n_lines} line(s) with {n_lines * n_pulses} pulses," f" got {status['lines']} with {status['pulses']}." ) - if status["triggers"] != status["lines"]: - logger.warning( - f"{self.name}: {status['triggers'] - status['lines']} trigger edge(s) ignored" - f" (edge while a line was running or while not armed): {status}." + # Ignored edges are harmless (lines and pulses are checked above): typically encoder + # noise at the PSO window entry, one extra edge per line while the line runs. Log only + # the new ones, as info, so a scan does not print a growing warning after each line. + ignored = status["triggers"] - status["lines"] + if ignored > self._ignored_reported: + logger.info( + f"{self.name}: {ignored - self._ignored_reported} trigger edge(s) ignored in" + f" line {n_lines} (edge while a line was running or while not armed)," + f" {ignored} since arm." ) + self._ignored_reported = ignored return status diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 65edee41..95feadef 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -274,3 +274,26 @@ def test_user_access_names_not_protected_by_bec(device_cls): module, name = device_cls.rsplit(".", 1) cls = getattr(importlib.import_module(module), name) assert not set(cls.USER_ACCESS) & set(get_protected_class_methods()) + + +def test_ignored_edges_logged_once_per_line_as_info(trigger): + from unittest import mock + + from csaxs_bec.devices.panda_box import panda_line_trigger + + trigger.configure_line(5, exp_time=0.01) + trigger.arm() + with mock.patch.object(panda_line_trigger, "logger") as logger: + for n_lines in (1, 2): + trigger.soft_line_trigger() + trigger.panda.busy = True + trigger.soft_line_trigger() # entry noise during the line + trigger.panda.busy = False + trigger.wait_lines_done(n_lines, timeout=1) + trigger.soft_line_trigger() + trigger.wait_lines_done(3, timeout=1) # clean line: nothing logged + logger.warning.assert_not_called() + messages = [call.args[0] for call in logger.info.call_args_list] + assert len(messages) == 2 + assert "1 trigger edge(s) ignored in line 1" in messages[0] + assert "1 trigger edge(s) ignored in line 2" in messages[1] and "2 since arm" in messages[1] -- 2.54.0 From 51ec4fcad800dd9f88372ce2019d2c7f67c71019 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 13:42:37 +0100 Subject: [PATCH 35/52] fix(aerotech): readout_time in scan_info, statistics report the real line period - scan_info / data file recorded readout_time 0.0 although the gates and the centres used the trigger hardware's 0.2 ms; now the value the scan uses - statistics: "line mean" was line start .. trigger checked (0.60 s for a 0.78 s line in scan 8477) and "scan" ran from pre_scan only (10.0 s for a 13.5 s file); now line period start to start incl. capture readout and step, with motion / readout split, and the total of the lines labelled as such Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../scans/scans_v4/aerotech_cont_grid.py | 29 ++++++++++++++----- .../test_aerotech_cont_grid_scan.py | 13 ++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 5940b142..075daf4b 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -272,8 +272,10 @@ class AerotechContGrid(ScanBase): self.capture = devices[capture_device] self._captured_lines: list[dict] = [] self._step_status = None - self._line_times: list[float] = [] - self._scan_start_time = None + self._line_times: list[float] = [] # line start .. trigger checked + self._line_starts: list[float] = [] + self._readout_times: list[float] = [] # capture readout per line + self._last_line_end = None self.motors = [fast_axis, stepper_axis] self.frames_per_line = 0 @@ -324,6 +326,7 @@ class AerotechContGrid(ScanBase): frames_per_trigger=self.frames_per_line, computed_positions=positions, num_lines=num_lines, + readout_time=self._line["readout_time"], ) if self.mcs is not None: @@ -367,7 +370,6 @@ class AerotechContGrid(ScanBase): ) self.panda.arm() self.actions.pre_scan_all_devices() - self._scan_start_time = time.time() @scan_hook def scan_core(self): @@ -401,6 +403,7 @@ class AerotechContGrid(ScanBase): then check that the line was triggered and read the captured positions. """ line_start_time = time.time() + self._line_starts.append(line_start_time) self.capture.arm(self.frames_per_line) move_status = self.actions.set(motors, positions, wait=False) next_line = line_index + 1 if line_index + 1 < self._line["num_lines"] else None @@ -423,7 +426,10 @@ class AerotechContGrid(ScanBase): self.panda.wait_lines_done(line_index + 1, timeout=self._line_duration() + 2) self._line_times.append(time.time() - line_start_time) + readout_start = time.time() self._captured_lines.append(self.capture.read_line(centre_shift=self._centre_shift())) + self._last_line_end = time.time() + self._readout_times.append(self._last_line_end - readout_start) @scan_hook def post_scan(self): @@ -538,12 +544,19 @@ class AerotechContGrid(ScanBase): f" backward - forward [um]: mean {diff.mean():+.2f}, slope {slope:+.2f} / mm," f" peak-peak {np.ptp(diff):.2f}", ] - if self._line_times: - total = time.time() - self._scan_start_time if self._scan_start_time else 0.0 + if self._line_starts and self._last_line_end is not None: + # line period: start to start of the next line (motion, trigger check, capture + # readout, wait for the step); the last line ends with its capture readout + ends = self._line_starts[1:] + [self._last_line_end] + periods = np.subtract(ends, self._line_starts) + lines_total = self._last_line_end - self._line_starts[0] text.append( - f" times: scan {total:.1f} s, line mean {np.mean(self._line_times):.2f} s" - f" (min {np.min(self._line_times):.2f}, max {np.max(self._line_times):.2f})," - f" exposure per line {n * self.exp_time:.2f} s" + f" times: lines {lines_total:.1f} s (first line start to last line end," + f" without staging / premove / close), line period mean {periods.mean():.2f} s" + f" (min {periods.min():.2f}, max {periods.max():.2f}), of which motion and" + f" trigger check {np.mean(self._line_times):.2f} s, capture readout" + f" {np.mean(self._readout_times):.2f} s; exposure per line" + f" {n * self.exp_time:.2f} s" ) return "\n".join(text) diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index c73d3eb1..833a6120 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -418,11 +418,22 @@ def test_statistics_of_a_symmetric_snake(v4_scan_assembler, device_manager): bwd = {"enc6_centre": list((cells + 3e-3)[::-1]), "enc2_centre": [0.5] * 5} scan._captured_lines = [fwd, bwd, fwd] scan._line_times = [1.0, 1.2, 1.1] + scan._readout_times = [0.2, 0.2, 0.2] + scan._line_starts = [100.0, 101.5, 103.0] # periods 1.5, 1.5, last line ends at 104.4 + scan._last_line_end = 104.4 text = scan._statistics() assert "forward mean +1.00" in text assert "backward mean +3.00" in text assert "backward - forward [um]: mean +2.00" in text and "peak-peak 0.00" in text - assert "line mean 1.10 s" in text + assert "lines 4.4 s" in text + assert "line period mean 1.47 s (min 1.40, max 1.50)" in text + assert "motion and trigger check 1.10 s, capture readout 0.20 s" in text + + +def test_readout_time_in_scan_info(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + assert scan.scan_info.readout_time == pytest.approx(2e-4) def test_on_exception_restores_ddg_pso_and_axis(v4_scan_assembler, device_manager): -- 2.54.0 From c86e825e61e19bad41e8f0b74d070af38592b443 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 13:53:54 +0100 Subject: [PATCH 36/52] docs(aerotech): user page for the SAXS-TT stages, developer page on design and triggering User guide (SAXS / Aerotech stages): initialisation on the A3200 PC, config, moves, aerotech_cont_grid with copy-paste examples for the planned fields of view, run-up and soft limits, data written, timing / positioning section (results of the 2026-10-29 series pending; overhead and uneven-positions estimate defined). Developer guide: interface, moves, scan design and measured choices, position capture, PandABox line trigger, pitfalls, commissioning. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- docs/developer/aerotech_stages.md | 210 ++++++++++++++++++ .../saxs/aerotech_stages/aerotech_stages.md | 167 ++++++++++++++ docs/user/saxs/saxs.md | 5 + zensical.toml | 2 + 4 files changed, 384 insertions(+) create mode 100644 docs/developer/aerotech_stages.md create mode 100644 docs/user/saxs/aerotech_stages/aerotech_stages.md diff --git a/docs/developer/aerotech_stages.md b/docs/developer/aerotech_stages.md new file mode 100644 index 00000000..9d7799d7 --- /dev/null +++ b/docs/developer/aerotech_stages.md @@ -0,0 +1,210 @@ +# Aerotech stages (SAXS-TT): design and triggering + +Developer notes for the Aerotech A3200 integration and the continuous grid scan +`aerotech_cont_grid`. User commands are on the +[user page](../user/saxs/aerotech_stages/aerotech_stages.md). + +| Part | Code | +|------|------| +| Controller, motors, position capture | `csaxs_bec/devices/aerotech/a3200.py` | +| Scan | `csaxs_bec/scans/scans_v4/aerotech_cont_grid.py` | +| PandABox line trigger (test setup) | `csaxs_bec/devices/panda_box/panda_line_trigger.py` | +| Simulation | `csaxs_bec/devices/sim/sim_a3200.py` | +| Configs | `device_configs/sastt_aerotech.yaml`, `sastt_aerotech_panda.yaml`, `simulated_omny/simulated_sastt_aerotech.yaml` | +| Hardware notes, wiring, channel table | `csaxs_bec/devices/aerotech/README.md` | + +## Hardware and interface + +Npaq-B-40B-40B/ULTRA drive rack, commanded over FireWire by the A3200 6.04.004 +software on the Windows PC `pc14643.psi.ch`. The Npaq Ethernet port is for Modbus +I/O only, so BEC talks to the A3200 ASCII command interface on the PC (TCP port 8000, +`CommandSetup = 0x00010004`: interface enabled and "Always Send EOS"). + +- One AeroBasic line per command. Reply: `%` (+ data) success, `!` invalid syntax, + `#` task fault (`~LASTERROR` gives the text). +- `~STATUS (X, ProgramPositionFeedback) (X, AxisStatus) ...` reads several items in + one round trip. +- Only asynchronous motion commands (`MOVEABS`, `HOMEASYNC`, `ABORT`); a blocking + command like `HOME` would block the socket until it finishes. +- Motion commands are never retried, status reads are retried once. +- A second client may only use `~STATUS` in parallel; its controller commands would + be refused while BEC's are running, unless it switches to its own task (`~TASK 3`). +- BEC uses `$global[0..199]` for drive array reads and `$global[254]` as scratch + variable (`GlobalDoubles` >= 255). + +## Initialisation of the stages + +!!! note "Current expectation" + The user initialises the stages on the A3200 PC in Motion Composer: enable the + axes, acknowledge faults, home. BEC does not do this at connect. + +The motors refuse moves while the axis is not homed (`require_homed=True`). The +methods exist in BEC (`enable()`, `disable()`, `home()`, `fault_ack()` on the motors, +`describe()` and `axis_status()` on the controller) but are not part of any +automatic procedure. UU follows U as gantry slave (`GantryMasterAxis = 3`) and is not +a BEC device; check the slip ring cabling before the first rotation. + +## Moves + +`A3200Motor.move()` sends `MOVEABS` with the axis velocity, after setting the ramp +rate (`acceleration`) on the controller. The move is complete when, after the end of +the motion profile, the position error stays within `tolerance` for 0.1 s (timeout +`in_position_timeout`, 2 s). + +The controller's own In Position bit is not used: `InPositionDistance` of X is +10 nm, which the stage reaches only after seconds or not at all (measured +2026-10-29: 10-30 nm within 0.5 s, then creeping), so every move logged a timeout. + +Velocities and acceleration of X: moves at 5 mm/s, ramp 500 mm/s^2 (decided value; +1000 mm/s^2 left a speed drift in the lines at 20 mm/s). Scans may set up to +`max_scan_velocity` (50 mm/s; the controller clamp is 200 mm/s) and restore the move +velocity in `post_scan` and `on_exception`. + +## Scan design + +### Snake and line trigger + +X runs continuously through each line, Y steps between lines, the lines alternate +direction. The start of a line is triggered by the PSO output of X when it enters a +position window: + +```text +trigger_low = fast_start - v * shutter_delay + v * readout_time / 2 (forward) +trigger_high = exposure_end + v * shutter_delay - v * readout_time / 2 (backward) +``` + +- **Window open on the far side** (`[trigger_low, beyond line_end]` forward). The axis + never leaves the window during a line. With a closed window the exit coincided with + the end of the exposures, and encoder noise at the exit edge started extra lines. + Noise at the entry edge falls into the running line, which DDG and PandA ignore + (typically one extra edge per line, logged at info level). +- **Armed once** in `pre_scan` (one position reference); before each line only the + range is switched (`PSOWINDOW RANGE`), while the axis is outside both windows. +- **Readout gap centred:** each gate (`exp_time - readout_time`) is centred in its + cell, so forward and backward lines expose the same intervals. Without it the + directions differ by `fast_step_size * readout_time / exp_time`. The readout time + comes from the trigger hardware (DDG2 `effective_readout_times()["ab"]`, PandA + config `readout_time`, both 0.2 ms) and is stored in `scan_info.readout_time`. + +### Run-up, settling and limits + +Run-up / run-out per side: `2 * v^2 / (2 * acceleration) + v * settle_time`. In a +snake the run-out of a line is the run-up of the next. `settle_time` stays 0.15 s: at +20 mm/s and 500 mm/s^2 there is a ~10 Hz oscillation after the ramp that decays within +~0.15 s; shorter values (0.08 / 0.04 / 0.02 / 0 s) gave backward minus forward offsets +of -24 / +10 / +4.5 / -21 um (2026-10-29). + +`prepare_scan` checks the whole travel range including run-up, and the Y range, +against the soft limits before anything moves. The message separates the scan range +from the run-up so a user can see why a scan inside the limits does not fit. + +### Step during the run-out + +As soon as X has left the exposure range of a line, the PSO window of the next line +is set and Y starts its step; the next line starts when both axes have arrived. X +does not wait for In Position at the turnarounds (`wait_in_position` off during the +scan). + +### Triggering and detectors + +The scan is hardware triggered: MCS and detectors are armed once at stage for all +`num_lines x frames_per_line` frames (`num_points` = lines, `frames_per_trigger` = +frames per line). + +| `trigger_source` | Line trigger | Per line check | +|------------------|--------------|----------------| +| `"ddg"` (beamline) | PSO -> DDG1 EXT, one burst of `frames_per_line` gates | MCS reached the line's last channel | +| `"panda"` (test setup) | PSO -> PandABox TTLIN3, shutter pulse + gates | `wait_lines_done`: lines and pulses | +| `None` | none (motion tests) | - | + +`trigger_edge` of DDG1: `"rising"` for the opto output (J7-6/7, tested), `"falling"` +for open collector / differential (active low, not tested). + +### Errors reach the user as alarms + +The scan's own checks raise `LimitError`, `ValueError` or `RuntimeError`, not +`ScanAbortion`. The scan worker only logs a `ScanAbortion`, and the client then shows +just "Scan ... was interrupted"; other exceptions raise an alarm with the message and +still run `on_exception` (restore velocity, PSO off, trigger receiver disarmed). + +## Position capture + +`aerocapture` (`A3200PositionCapture`) latches the encoder counters of X, Z, Y, U at +every detector gate: Npaq DATAACQ on UINT1 (J12), trigger 3 = rising TTL edge = gate +start (the Npaq input is inverted compared with the A3200 help, measured). After each +line the drive array is read back (~2600 values/s over the ASCII interface) and +published as async data: + +- `aerocapture_positions_enc`: position at the start of each gate, +- `aerocapture_positions_enc_centre`: centre of each gate, `+ v * gate / 2` + in the direction of motion for the moving fast axis. + +The counter offset is calibrated in `pre_scan` with one software PSO fire, before any +trigger receiver is armed. Positions are in the user coordinates of the BEC motor of +each channel (its `sign` applied). DATAACQ sources: 0-5 local counters, 6-11 MXR +channels 0-5 (max. 4 kHz with MXR sources). + +At the end the scan sends statistics to the client: centre minus nominal cell +centre per direction, backward minus forward offset with slope and peak-to-peak +along the line, and line period with the motion and readout parts. + +## PandABox as line trigger (test setup) + +Without beamline timing, `sastt_aerotech_panda.yaml` adds `pandatrig` +(`omny-panda.psi.ch`) as a DDG stand-in. It stays armed like the DDG, and every +accepted line trigger edge produces: + +| Output | Signal | +|--------|--------| +| TTLOUT2 "shutter" | from the trigger edge to the end of the last gate | +| TTLOUT3 "detector" | n gates, width `exp_time - readout_time`, period `exp_time`, first one `shutter_delay` after the edge; also to Npaq J12 for the position capture | + +Trigger path: LUT8 = TTLIN3 | BITS.A (software trigger); SRGATE1 arm gate; LUT7 = +rising edge of LUT8 while armed and while no line is running -> PULSE3 (shutter), +PULSE4 (gates). Edges during a running line are ignored, like on the DDG. Counters: +COUNTER6 raw edges, COUNTER7 accepted lines, COUNTER8 gates. Layout +`devices/panda_box/layouts/sastt_line_trigger.ini`; the device writes the wiring at +connect. + +```python +dev.pandatrig.configure_line(n_pulses=100, exp_time=0.01, readout_time=0.002, shutter_delay=0.005) +dev.pandatrig.arm() +dev.pandatrig.soft_line_trigger() # or the PSO edge on TTLIN3 +dev.pandatrig.wait_lines_done(n_lines=1, timeout=5) +dev.pandatrig.line_status() # triggers, lines, pulses, dropped since arm() +``` + +PandA FPGA 4.1 PULSE block quirk: an ENABLE edge, a parameter write or a dropped +trigger often makes it replay the previous pulse train without a trigger. The device +keeps the PULSE blocks enabled, writes their parameters only when they change, waits +until such a replay is over (both outputs low for 1 s), and the trigger path filters +edges during a line, so no PULSE block ever drops a trigger. + +## Pitfalls found during commissioning + +- **No integer map keys in anything sent over Redis.** BEC's msgpack decoder rejects + them (`strict_map_key`). An integer-keyed YAML map in a device config made the + config load hang; an integer-keyed RPC return value made the scan wait forever for + the call. Tests round-trip configs and RPC return values through the serializer. +- **`USER_ACCESS` must contain every method the scan calls**, the scan server reaches + devices only through RPC. A test checks the scan source against the lists. +- **`USER_ACCESS` must not use names of BEC's `DeviceBase`** (e.g. `software_trigger`); + the device server then refuses to load the device. A test checks this. +- **Device code reloads with the config, scan code does not:** after a change in + `a3200.py` or the PandA device reload the config; after a change in the scan + restart the BEC services. + +## Commissioning checklist + +1. A3200 runs on the Windows PC; all axes enable and move in Motion Composer. +2. Export the parameter file and keep a copy. +3. Configuration Manager: `CommandSetup = 0x00010004`, `CommandPort = 8000`, reset + the controller. Windows firewall: allow inbound TCP 8000 from the BEC host. +4. `nc pc14643.psi.ch 8000`, send `~VERSION`, expect `%6.04...`. +5. Host, axes, velocities and limits in `sastt_aerotech.yaml` follow the parameter + file. +6. Channels (`pso_input`, `pso_counts_divider`, `aerocapture.channels`): counter + direction and scale match the axis (the calibration only fixes the offset). +7. PSO output selection and polarity; PSO interlock defeated (`IOSetup` 0x80). +8. Check one line with the scope: PSO edge at the trigger position, number of UINT1 + edges = frames per line, captured positions vs. requested positions. diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md new file mode 100644 index 00000000..c16f884f --- /dev/null +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -0,0 +1,167 @@ +# Aerotech stages (SAXS-TT) + +The SAXS-TT sample stage is an Aerotech stack driven by an A3200 controller. BEC +controls it for positioning and for fast continuous scans: the sample moves +continuously along X while the detector takes one frame per cell, and the encoder +positions are recorded for every frame. + +| BEC device | Axis | Stage | Units | Use | +|------------|------|-------|-------|-----| +| `aerox` | X | ALS25020 linear stage | mm | horizontal, fast axis of the scans | +| `aeroy` | Y | AVSI125 vertical lift | mm | vertical, step axis of the scans | +| `aeroz` | Z | ATS20010 linear stage | mm | along the beam | +| `aerorot` | U | ABRT-200 rotation stage | deg | rotation (UU follows as slave) | +| `aerocapture` | - | position capture | - | encoder positions at each exposure | + +## Before you start + +!!! warning "Initialise the stages on the A3200 PC" + BEC does not initialise the stages. Before using them in BEC, enable all axes, + acknowledge any faults and home them in the Aerotech software (Motion Composer) + on the A3200 PC `pc14643.psi.ch`. BEC refuses to move an axis that is not homed. + +Load the device configuration in the BEC client: + +```python +bec.config.update_session_with_file( + "/opt/bec_deployments/production/csaxs_bec/csaxs_bec/device_configs/sastt_aerotech.yaml" +) +``` + +Check the stages: + +```python +dev.aerox.controller.describe() # all axes: enabled, homed, moving, faults, position +dev.aerox.status() # one axis in detail +``` + +## Moving the stages + +```python +umv(dev.aerox, 143.25) # absolute move, waits until the axis has settled +umvr(dev.aeroy, -0.5) # relative move +dev.aerox.wm # position and limits +dev.aerox.limits = [-50, 150] +``` + +Normal moves of X run at 5 mm/s with an acceleration of 500 mm/s^2. A move is done +when the position stays within the tolerance of the axis (X: 1 um, Y and Z: 5 um). +The soft limits are checked before every move. + +## Continuous grid scan: `aerotech_cont_grid` + +X moves continuously through each line, Y steps between the lines, and the lines run +as a snake (alternating direction). The start of each line is triggered in hardware +by the X position, so the detector frames sit at the same positions in both +directions. + +```python +scans.aerotech_cont_grid( + dev.aerox, fast_start, fast_end, fast_step_size, + dev.aeroy, stepper_start, stepper_stop, stepper_step_size, + exp_time=..., relative=True, +) +``` + +| Argument | Meaning | +|----------|---------| +| `fast_start`, `fast_end` | first and last cell of a line along X | +| `fast_step_size` | X distance travelled during one exposure (cell size) | +| `stepper_start`, `stepper_stop`, `stepper_step_size` | Y range and line spacing | +| `exp_time` | exposure time per cell in s; the X velocity is `fast_step_size / exp_time` | +| `relative` | `True`: positions are offsets from the current X / Y position, and both axes return there after the scan | +| `trigger_source` | `"ddg"` (default, beamline timing) | +| `settle_time` | constant velocity time before the first exposure of a line, default 0.15 s (leave it) | + +The number of points per line is `ceil(|fast_end - fast_start| / fast_step_size) + 1`, +the number of lines `ceil(|stepper_stop - stepper_start| / stepper_step_size) + 1`. +The scan velocity can be up to 50 mm/s (`max_scan_velocity` of `aerox`). After the +scan, also after an aborted one, X returns to its normal move velocity. + +With `trigger_source="ddg"` the client shows a progress bar of the frames. + +### Examples + +Fields of view centred on the current position (`relative=True`), X fast, Y step: + +```python +# 0.8 mm x 1.2 mm (horizontal x vertical) +scans.aerotech_cont_grid(dev.aerox, -0.4, 0.4, 0.1, dev.aeroy, -0.6, 0.6, 0.1, exp_time=0.005, relative=True) # quick: 100 um, 5 ms (20 mm/s), 9 x 13 +scans.aerotech_cont_grid(dev.aerox, -0.4, 0.4, 0.04, dev.aeroy, -0.6, 0.6, 0.04, exp_time=0.02, relative=True) # overview: 40 um, 20 ms (2 mm/s), 21 x 31 +scans.aerotech_cont_grid(dev.aerox, -0.4, 0.4, 0.01, dev.aeroy, -0.6, 0.6, 0.01, exp_time=0.02, relative=True) # high resolution: 10 um, 20 ms (0.5 mm/s), 81 x 121 + +# 2 mm x 2 mm +scans.aerotech_cont_grid(dev.aerox, -1.0, 1.0, 0.1, dev.aeroy, -1.0, 1.0, 0.1, exp_time=0.005, relative=True) # quick: 21 x 21 +scans.aerotech_cont_grid(dev.aerox, -1.0, 1.0, 0.04, dev.aeroy, -1.0, 1.0, 0.04, exp_time=0.02, relative=True) # overview: 51 x 51 + +# 1 mm x 1 mm +scans.aerotech_cont_grid(dev.aerox, -0.5, 0.5, 0.01, dev.aeroy, -0.5, 0.5, 0.01, exp_time=0.02, relative=True) # high resolution: 101 x 101 + +# 3.5 mm x 3.0 mm (largest field of view) +scans.aerotech_cont_grid(dev.aerox, -1.75, 1.75, 0.1, dev.aeroy, -1.5, 1.5, 0.1, exp_time=0.005, relative=True) # quick: 36 x 31 +scans.aerotech_cont_grid(dev.aerox, -1.75, 1.75, 0.04, dev.aeroy, -1.5, 1.5, 0.04, exp_time=0.02, relative=True) # overview: 89 x 76 +scans.aerotech_cont_grid(dev.aerox, -1.75, 1.75, 0.01, dev.aeroy, -1.5, 1.5, 0.01, exp_time=0.02, relative=True) # high resolution: 351 x 301 +``` + +### Run-up and soft limits + +To pass through the field of view at constant velocity, X travels beyond it on both +sides: twice the acceleration distance `v^2 / (2 * 500 mm/s^2)` plus `v * 0.15 s` to +settle. The run-up per side is + +| Scan velocity | 0.5 mm/s | 2 mm/s | 20 mm/s | +|---------------|----------|--------|---------| +| Run-up per side | 0.08 mm | 0.31 mm | 3.8 mm | + +Before anything moves, the scan checks the whole range including the run-up against +the soft limits. If it does not fit, the scan stops with a message that says by how +much and on which side, for example + +```text +The scan range of aerox 146.5000 .. 150.0000 is within the limits, but aerox needs +3.8400 extra travel on each side to accelerate to the scan velocity 20 / s and settle +... +``` + +Move the field of view away from the limit, or scan slower. + +### Data + +Besides the detector data, each scan file contains, per frame: + +| Data | Content | +|------|---------| +| `aerocapture_positions_enc6`, `aerocapture_positions_enc2` | X and Y at the start of each exposure | +| `aerocapture_positions_enc6_centre`, `aerocapture_positions_enc2_centre` | X and Y at the centre of each exposure | + +They are stored one line after the other in the order of the measurement, so the +backward lines run from high to low X. The positions are in the coordinates of +`aerox` / `aeroy`. Use the centre positions to place the frames; they are measured, +not nominal. The nominal positions are in the scan metadata (`computed_positions`). + +At the end of the scan the client shows a short summary: deviation of the measured +centres from the nominal cells, backward minus forward offset, and the line times. + +## Timing and positioning + +!!! note "Results pending" + The measured values of the test series of 2026-10-29 are added here once the + series is complete. + +The target resolution is about 20 um; the positioning accuracy should be well below +that. Columns of the result table: + +- **Positioning accuracy:** measured exposure centres minus nominal cell centres + (rms), and the backward minus forward offset between neighbouring lines. A constant + offset only shifts the field of view; a slope along the line would distort it. +- **Line period:** time from the start of one line to the start of the next. +- **Exposure per line:** points per line times `exp_time`. +- **Positioning overhead:** line period minus exposure per line, as an absolute + time per line and as a fraction of the scan time. It contains the run-up and + run-out, the turnaround, the Y step (done during the run-out of X), and the + readout of the captured positions. +- **Estimate with uneven positions:** if the data analysis could place frames at + unevenly spaced positions, the exposures would not have to wait for X to settle at + constant velocity, and the run-up could shrink to the braking distance. That saves + about `v / a + 2 * settle_time` per line (0.30-0.34 s at 0.5-20 mm/s), on top of + any reduction of the other overheads. diff --git a/docs/user/saxs/saxs.md b/docs/user/saxs/saxs.md index 727d8d19..74eb45a7 100644 --- a/docs/user/saxs/saxs.md +++ b/docs/user/saxs/saxs.md @@ -8,6 +8,11 @@ Welcome to the SAXS section of the cSAXS beamline. Setup of online radial integration on Jungfraujoch. +- ![](../../assets/biotech.svg){ width="32" height="32" .middle .card-icon } **[Aerotech stages](aerotech_stages/aerotech_stages.md)** + { .card-heading } + + SAXS-TT sample stages and continuous grid scans. + ## Reference documents diff --git a/zensical.toml b/zensical.toml index 37fe217e..1d80efc8 100644 --- a/zensical.toml +++ b/zensical.toml @@ -36,6 +36,7 @@ nav = [ { "SAXS" = [ { "Overview" = "user/saxs/saxs.md" }, { "Online integration" = "user/saxs/online_integration.md" }, + { "Aerotech stages" = "user/saxs/aerotech_stages/aerotech_stages.md" }, ] }, { "Data analysis" = [ { "Overview" = "user/data_analysis/data_analysis.md" }, @@ -48,6 +49,7 @@ nav = [ { "LamNI smear aid architecture" = "developer/lamni_smear_architecture.md" }, { "PandaBox free-running setup" = "developer/panda_box_free_running_setup.md" }, { "PandaBox data schema" = "developer/panda_box_free_running_data_schema.md" }, + { "Aerotech stages: design and triggering" = "developer/aerotech_stages.md" }, ] }, ] -- 2.54.0 From 3c551ba38ea317b57eb139dc556ecdf2b7111484 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 13:58:31 +0100 Subject: [PATCH 37/52] feat(aerotech): minimal_run_up option, run-up only the ramp distance aerotech_cont_grid(..., minimal_run_up=True): run-up / run-out = v^2 / (2a), without the 2x margin and settle_time, for analysis with unevenly spaced (captured) positions; at least 0.01 so the fast axis never stops and reverses at the edge of the next line's PSO window (0.25 um at 0.5 mm/s otherwise). Shown in the statistics header and the limit message. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../scans/scans_v4/aerotech_cont_grid.py | 34 ++++++++++++++++--- .../test_aerotech_cont_grid_scan.py | 33 ++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 075daf4b..71626551 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -63,6 +63,9 @@ In Position. The step axis moves during the run-out and turnaround of the fast a soon as the fast axis has left the exposure range, the PSO window of the next line is set and the step axis starts; the next line starts when both have arrived. +minimal_run_up=True reduces the run-up / run-out to the ramp distance alone (no margin, no +settle_time; at least MIN_RUN_UP = 0.01 so the axis never turns at a PSO window edge), for data analysis that works with unevenly spaced (captured) positions. + settle_time stays 0.15 s also at high speed: at 20 mm/s / 500 mm/s^2 shorter values (0.08 / 0.04 / 0.02 / 0 s) gave backward - forward offsets of -24 / +10 / +4.5 / -21 um, a ~10 Hz oscillation after the ramp that decays within ~0.15 s (measured 2026-10-29). @@ -110,6 +113,12 @@ DDG_TRIGGER_EDGES = {"rising": DDG_EXT_RISING_EDGE, "falling": DDG_EXT_FALLING_E # servo following error at the end of the ramp) RAMP_MARGIN_FRACTION = 1.0 +# Smallest run-up / run-out with minimal_run_up, in units of the fast axis: at low speed the +# ramp distance is sub-um (0.5 mm/s, 500 mm/s^2: 0.25 um), and the fast axis would stop and +# reverse right at the edge of the next line's PSO window, where ringing or overshoot could +# start the line before the axis moves. +MIN_RUN_UP = 0.01 + TRIGGER_SOURCES = ("ddg", "panda", None) # Poll interval of the fast axis position while waiting for the end of the exposures, s @@ -153,6 +162,7 @@ class AerotechContGrid(ScanBase): "exp_time", "relative", "trigger_source", + "minimal_run_up", ], } @@ -174,6 +184,7 @@ class AerotechContGrid(ScanBase): trigger_device: str = "pandatrig", shutter_delay: float | None = None, settle_time: float = 0.15, + minimal_run_up: bool = False, capture_device: str = "aerocapture", **kwargs, # fmt: on @@ -198,6 +209,7 @@ class AerotechContGrid(ScanBase): trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". shutter_delay (float | None): Delay from the line trigger to the first exposure in s. Default: DDG1 shutter delay for "ddg", 0 otherwise. settle_time (float): Run-up at constant velocity before the trigger position, in s. Default is 0.15. + minimal_run_up (bool): Run-up / run-out only the ramp distance v^2 / (2 * acceleration), at least 0.01, without the 2x margin and settle_time: faster lines, but the exposures start right after the ramp while the fast axis may still oscillate, so the captured positions are less evenly spaced. Default is False. capture_device (str): Name of the A3200PositionCapture device (always used). Default is "aerocapture". Returns: @@ -253,6 +265,7 @@ class AerotechContGrid(ScanBase): self.trigger_source = trigger_source self.shutter_delay = shutter_delay self.settle_time = settle_time + self.minimal_run_up = minimal_run_up devices = self.device_manager.devices self.ddg1 = self.ddg2 = self.mcs = self.panda = None @@ -530,7 +543,8 @@ class AerotechContGrid(ScanBase): fwd, bwd = dev[0::2], dev[1::2] text = [ f"aerotech_cont_grid statistics ({len(lines)} lines x {n} points, v" - f" {self._line['target_velocity']:g} / s, readout {self._line['readout_time'] * 1e3:g} ms):", + f" {self._line['target_velocity']:g} / s, readout {self._line['readout_time'] * 1e3:g} ms" + f"{', minimal run-up' if self.minimal_run_up else ''}):", f" centre - nominal cell centre [um]: forward mean {fwd.mean():+.2f} rms" f" {fwd.std():.2f} max {np.abs(fwd).max():.2f}", ] @@ -632,7 +646,11 @@ class AerotechContGrid(ScanBase): " run-up distance of the line." ) ramp_distance = velocity**2 / (2 * line["acceleration"]) - margin = RAMP_MARGIN_FRACTION * ramp_distance + velocity * self.settle_time + if self.minimal_run_up: + # exposures start right after the ramp; the captured positions show the result + margin = max(MIN_RUN_UP - ramp_distance, 0.0) + else: + margin = RAMP_MARGIN_FRACTION * ramp_distance + velocity * self.settle_time lead = velocity * line["shutter_open_delay"] # centre each gate (exp_time - readout_time) in its cell: same centres in both directions gap = velocity * line["readout_time"] / 2 @@ -660,12 +678,18 @@ class AerotechContGrid(ScanBase): beyond = f"beyond the high limit {high} by {line['line_end'] - high:.4f}" else: beyond = f"beyond the low limit {low} by {low - line['line_start']:.4f}" + if self.minimal_run_up: + why = f"(acceleration {line['acceleration']:g} / s^2, minimal run-up)" + else: + why = ( + f"and settle (acceleration {line['acceleration']:g} / s^2, settle_time" + f" {self.settle_time:g} s)" + ) raise LimitError( f"The scan range of {name} {self.fast_start:.4f} .. {self.fast_end:.4f} is" f" within the limits, but {name} needs {extra:.4f} extra travel on each side" - f" to accelerate to the scan velocity {line['target_velocity']:g} / s and" - f" settle (acceleration {line['acceleration']:g} / s^2, settle_time" - f" {self.settle_time:g} s). It would travel {line['line_start']:.4f} .." + f" to accelerate to the scan velocity {line['target_velocity']:g} / s" + f" {why}. It would travel {line['line_start']:.4f} .." f" {line['line_end']:.4f}, {beyond}. Move the scan away from the limit or" " scan slower (larger exp_time or smaller fast_step_size)." ) diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 833a6120..7eec820f 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -530,3 +530,36 @@ def test_scan_calls_only_user_access_methods(attribute, device_cls): signals = {"motor_velocity", "wait_in_position", "max_scan_velocity", "motor_acceleration"} assert called, f"no calls on self.{attribute} found" assert not (called - signals) - set(cls.USER_ACCESS) + + +def test_minimal_run_up_is_the_ramp_distance_only(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, settle_time=0.1, minimal_run_up=True) + scan.prepare_scan() + line = scan._line + ramp = (0.5 / 0.1) ** 2 / (2 * 100.0) + assert ramp > 0.01 + assert line["line_start"] == pytest.approx(line["trigger_low"] - ramp) + assert line["line_end"] == pytest.approx(line["trigger_high"] + ramp) + + +def test_minimal_run_up_has_a_floor_at_low_speed(v4_scan_assembler, device_manager): + # 0.5 mm/s at 100 mm/s^2: ramp 1.25 um, run-up raised to 10 um + _add_devices(device_manager) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -1.0, 1.0, 0.01, "aeroy", -2.0, 2.0, 2, + exp_time=0.02, minimal_run_up=True, + ) + scan.prepare_scan() + line = scan._line + assert line["trigger_low"] - line["line_start"] == pytest.approx(0.01) + assert line["line_end"] - line["trigger_high"] == pytest.approx(0.01) + + +def test_minimal_run_up_limit_message(v4_scan_assembler, device_manager): + _add_devices(device_manager, max_scan_velocity=50.0) + scan = v4_scan_assembler( + "aerotech_cont_grid", "aerox", -9.5, 9.5, 0.1, "aeroy", -2.0, 2.0, 2, + exp_time=0.005, minimal_run_up=True, + ) + with pytest.raises(LimitError, match="minimal run-up"): + scan.prepare_scan() -- 2.54.0 From b1fb215fa5e93c46fb9fd6bc0227f13e7a4183e6 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 14:40:53 +0100 Subject: [PATCH 38/52] docs(aerotech): measured timing and positioning of the default series (scans 8486-8494) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../saxs/aerotech_stages/aerotech_stages.md | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index c16f884f..eb699855 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -72,6 +72,7 @@ scans.aerotech_cont_grid( | `relative` | `True`: positions are offsets from the current X / Y position, and both axes return there after the scan | | `trigger_source` | `"ddg"` (default, beamline timing) | | `settle_time` | constant velocity time before the first exposure of a line, default 0.15 s (leave it) | +| `minimal_run_up` | `True`: run-up only the acceleration distance (at least 10 um), no settling. Faster lines, but the exposures are less evenly spaced; use the captured positions. Default `False` | The number of points per line is `ceil(|fast_end - fast_start| / fast_step_size) + 1`, the number of lines `ceil(|stepper_stop - stepper_start| / stepper_step_size) + 1`. @@ -144,9 +145,38 @@ centres from the nominal cells, backward minus forward offset, and the line time ## Timing and positioning -!!! note "Results pending" - The measured values of the test series of 2026-10-29 are added here once the - series is complete. +Measured 2026-10-29 (scans 8486-8494, default run-up, PandABox as trigger, readout +0.2 ms, X acceleration 500 mm/s^2). Positions in um; "rms" is the scatter of the +measured exposure centres around the nominal cells (forward and backward lines +alike), "bwd - fwd" the offset between neighbouring lines with its slope along X +and its peak-to-peak over the line. + +| Field of view (mm) | Step / exposure (speed) | Points x lines | rms | bwd - fwd | slope (um/mm) | peak-to-peak | Line period | Exposure per line | Positioning overhead per line | Scan time | Estimate with uneven positions (line, scan) | +|---|---|---|---|---|---|---|---|---|---|---|---| +| 0.8 x 1.2 | 100 um / 5 ms (20 mm/s) | 9 x 13 | 2.9 | +1.2 | +3.4 | 11.4 | 0.79 s | 0.04 s | 0.74 s (94 %) | 0.2 min | 0.45 s, 0.1 min | +| 0.8 x 1.2 | 40 um / 20 ms (2 mm/s) | 21 x 31 | 1.0 | +3.6 | +0.2 | 2.9 | 1.03 s | 0.42 s | 0.61 s (59 %) | 0.6 min | 0.73 s, 0.4 min | +| 0.8 x 1.2 | 10 um / 20 ms (0.5 mm/s) | 81 x 121 | 0.2 | +0.1 | 0.0 | 0.8 | 2.21 s | 1.62 s | 0.59 s (27 %) | 4.5 min | 1.95 s, 3.9 min | +| 2 x 2 | 100 um / 5 ms (20 mm/s) | 21 x 21 | 5.6 | +2.3 | +0.2 | 25.6 | 0.83 s | 0.10 s | 0.72 s (87 %) | 0.4 min | 0.49 s, 0.2 min | +| 2 x 2 | 40 um / 20 ms (2 mm/s) | 51 x 51 | 0.6 | +2.8 | 0.0 | 2.4 | 1.64 s | 1.02 s | 0.62 s (38 %) | 1.5 min | 1.34 s, 1.1 min | +| 1 x 1 | 10 um / 20 ms (0.5 mm/s) | 101 x 101 | 0.2 | 0.0 | 0.0 | 0.8 | 2.61 s | 2.02 s | 0.59 s (23 %) | 4.5 min | 2.35 s, 4.0 min | +| 3.5 x 3.0 | 100 um / 5 ms (20 mm/s) | 36 x 31 | 5.7 | +0.8 | +0.1 | 30.6 | 0.91 s | 0.18 s | 0.73 s (80 %) | 0.6 min | 0.57 s, 0.3 min | +| 3.5 x 3.0 | 40 um / 20 ms (2 mm/s) | 89 x 76 | 0.5 | +2.8 | 0.0 | 2.9 | 2.40 s | 1.78 s | 0.62 s (26 %) | 3.1 min | 2.10 s, 2.7 min | +| 3.5 x 3.0 | 10 um / 20 ms (0.5 mm/s) | 351 x 301 | 0.1 | -0.7 | 0.0 | 0.9 | 7.75 s | 7.02 s | 0.73 s (9 %) | 38.9 min | 7.49 s, 37.6 min | + +- **Positioning:** at 0.5 and 2 mm/s the exposure centres are within about 1 um + rms of the nominal cells, and the snake is symmetric (no slope). A constant + backward minus forward offset of up to ~4 um remains; it only shifts the field of + view. At 20 mm/s an oscillation of X after the acceleration ramp reaches into the + line: up to 25-30 um peak-to-peak on the larger fields. That is small compared with + the 100 um cells of these quick scans, but above the 20 um resolution target. +- **Overhead:** about 0.6-0.75 s per line at every speed: run-up and run-out, + turnaround, the Y step (during the run-out of X), and the readout of the captured + positions (0.03-0.3 s, growing with the points per line). It dominates the quick + scans (80-94 % at 20 mm/s) and is small for the long high resolution lines. +- **Uneven positions:** the last column is an estimate, not a measurement: the line + period minus the run-up and run-out that could be saved if the frames did not have + to sit on an even grid (see below). It will be replaced by measured values of the + same scans with `minimal_run_up=True`. The target resolution is about 20 um; the positioning accuracy should be well below that. Columns of the result table: -- 2.54.0 From 7fd40c69dce9cdf0ff3ea0c4123dfbaad72d3221 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 14:43:26 +0100 Subject: [PATCH 39/52] feat(csaxs): csaxs.aerotech.plot() shows the exposure centres Y vs X live Opens (or reuses) the GUI window "aerotech" with a Waveform of the async aerocapture centres, enc2_centre vs enc6_centre, updated per line during aerotech_cont_grid; scan_number=... shows a finished scan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../plugins/cSAXS/aerotech.py | 63 +++++++++++++++++++ .../bec_ipython_client/plugins/cSAXS/cSAXS.py | 3 + .../saxs/aerotech_stages/aerotech_stages.md | 8 +++ .../test_cSAXS_aerotech.py | 50 +++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py create mode 100644 tests/tests_bec_ipython_client/test_cSAXS_aerotech.py diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py new file mode 100644 index 00000000..a6531ef0 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py @@ -0,0 +1,63 @@ +"""Aerotech SAXS-TT stage tools of the cSAXS session (csaxs.aerotech).""" + +from __future__ import annotations + +from bec_lib import bec_logger + +logger = bec_logger.logger + + +class cSAXSAerotech: + """Tools for the Aerotech SAXS-TT stages and the aerotech_cont_grid scan.""" + + WINDOW = "aerotech" + PLOT = "aerotech_positions" + GUI_RPC_TIMEOUT = 20 + CAPTURE_DEVICE = "aerocapture" + # exposure centres of the fast (X) and step (Y) axis, published per line + SIGNAL_X = "aerocapture_positions_enc6_centre" + SIGNAL_Y = "aerocapture_positions_enc2_centre" + + def __init__(self, client): + self.client = client + self.gui = getattr(client, "gui", None) + + def plot(self, scan_number: int | None = None): + """ + Open a window that plots the measured exposure centres Y versus X of + aerotech_cont_grid, line by line while the scan runs. + + Without scan_number the plot follows the running (or next) scan; with + scan_number it shows that scan from the history. Calling it again reuses the + window. + + Examples + -------- + csaxs.aerotech.plot() # live, follows the scans + csaxs.aerotech.plot(scan_number=8494) # a finished scan + """ + if self.gui is None: + raise RuntimeError("No BEC GUI available: start the BEC client with the GUI.") + if self.WINDOW in self.gui.windows: + window = self.gui.windows[self.WINDOW] + else: + window = self.gui.new(self.WINDOW, timeout=self.GUI_RPC_TIMEOUT) + waveform = window.new("Waveform", object_name=self.PLOT, timeout=self.GUI_RPC_TIMEOUT) + waveform.clear_all() + waveform.title = "aerotech_cont_grid: exposure centres" + waveform.x_label = "X centre (aerox)" + waveform.y_label = "Y centre (aeroy)" + waveform.lock_aspect_ratio = True + curve = waveform.plot( + device_x=self.CAPTURE_DEVICE, + signal_x=self.SIGNAL_X, + device_y=self.CAPTURE_DEVICE, + signal_y=self.SIGNAL_Y, + label="exposure centres", + scan_number=scan_number, + ) + # markers for the exposures, a thin line shows the snake order + curve.set_symbol("o") + curve.set_symbol_size(4) + curve.set_pen_width(1) + return waveform diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py index da747954..aa36fc6e 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py @@ -3,6 +3,7 @@ import inspect from bec_lib import bec_logger from typeguard import typechecked +from csaxs_bec.bec_ipython_client.plugins.cSAXS.aerotech import cSAXSAerotech from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission from csaxs_bec.bec_ipython_client.plugins.cSAXS.intensity_map_predict_gap import ( @@ -32,6 +33,7 @@ class cSAXS( self.OMNYTools = OMNYTools(self.client) self.diagnostics = cSAXSDiagnostics() + self.aerotech = cSAXSAerotech(client) super().__init__(client=client) # ------------------------------------------------------------------ @@ -104,6 +106,7 @@ class cSAXS( ("csaxs.diagnostics.bim", "BIM diagnostics — .show_all(), .gain(val)"), ("csaxs.diagnostics.beamstop", "Beamstop diode — .show_all(), .gain(val)"), ("csaxs.diagnostics.polarization", "Polarization diodes — .show_all(), .gain(val)"), + ("csaxs.aerotech.plot()", "Aerotech scan: exposure centres Y vs X, live"), ("csaxs.OMNYTools.*", "OMNY instrument tools"), ]: ns.add_row(access, desc) diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index eb699855..d16c3ddf 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -81,6 +81,14 @@ scan, also after an aborted one, X returns to its normal move velocity. With `trigger_source="ddg"` the client shows a progress bar of the frames. +To watch the measured positions while the scan runs, open the live plot of the +exposure centres (Y versus X, one line after the other): + +```python +csaxs.aerotech.plot() # follows the running and the next scans +csaxs.aerotech.plot(scan_number=8494) # a finished scan +``` + ### Examples Fields of view centred on the current position (`relative=True`), X fast, Y step: diff --git a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py new file mode 100644 index 00000000..3dbda618 --- /dev/null +++ b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py @@ -0,0 +1,50 @@ +from unittest import mock + +import pytest + +from csaxs_bec.bec_ipython_client.plugins.cSAXS.aerotech import cSAXSAerotech + + +def _client(windows=None): + gui = mock.MagicMock() + gui.windows = windows if windows is not None else {} + return mock.MagicMock(gui=gui) + + +def test_plot_opens_window_and_plots_centres(): + client = _client() + waveform = client.gui.new.return_value.new.return_value + + assert cSAXSAerotech(client).plot() is waveform + + client.gui.new.assert_called_once_with("aerotech", timeout=20) + client.gui.new.return_value.new.assert_called_once_with( + "Waveform", object_name="aerotech_positions", timeout=20 + ) + waveform.clear_all.assert_called_once_with() + assert waveform.lock_aspect_ratio is True + waveform.plot.assert_called_once_with( + device_x="aerocapture", + signal_x="aerocapture_positions_enc6_centre", + device_y="aerocapture", + signal_y="aerocapture_positions_enc2_centre", + label="exposure centres", + scan_number=None, + ) + waveform.plot.return_value.set_symbol.assert_called_once_with("o") + + +def test_plot_reuses_open_window_and_shows_history(): + window = mock.MagicMock() + client = _client(windows={"aerotech": window}) + + cSAXSAerotech(client).plot(scan_number=8494) + + client.gui.new.assert_not_called() + assert window.new.return_value.plot.call_args.kwargs["scan_number"] == 8494 + + +def test_plot_without_gui_raises(): + client = mock.MagicMock(spec=["device_manager"]) + with pytest.raises(RuntimeError, match="No BEC GUI"): + cSAXSAerotech(client).plot() -- 2.54.0 From b7365eceb60988f051c3a117b0489b9d6286ca9b Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 15:08:00 +0100 Subject: [PATCH 40/52] fix(csaxs): aerotech plot sends the positions itself, no downsampling artefacts The Waveform's async device curves switch on automatic "peak" downsampling and clip-to-view above 1000 points; both assume X increasing along the curve, so a snake scan (8494, 105651 points) showed a diamond pattern and paired markers. csaxs.aerotech.plot() now reads the scan file (history) or follows the aerocapture async stream of the running scan (live, from_start, redraw in a worker thread at <= 2 Hz) and sends plain X/Y data: forward and backward lines as two curves, points ordered by cell column, alternately up and down, so X increases along each curve (clip-to-view works when zoomed) and the thin line runs along the columns. csaxs.aerotech.stop_plot() stops following. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../plugins/cSAXS/aerotech.py | 198 ++++++++++++++++-- .../test_cSAXS_aerotech.py | 133 ++++++++++-- 2 files changed, 292 insertions(+), 39 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py index a6531ef0..28820542 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py @@ -2,7 +2,12 @@ from __future__ import annotations +import threading + +import h5py +import numpy as np from bec_lib import bec_logger +from bec_lib.endpoints import MessageEndpoints logger = bec_logger.logger @@ -13,23 +18,30 @@ class cSAXSAerotech: WINDOW = "aerotech" PLOT = "aerotech_positions" GUI_RPC_TIMEOUT = 20 + SCAN_NAME = "aerotech_cont_grid" CAPTURE_DEVICE = "aerocapture" - # exposure centres of the fast (X) and step (Y) axis, published per line + CAPTURE_SIGNAL = "aerocapture_positions" + # exposure centres of the fast (X) and step (Y) axis, published once per line SIGNAL_X = "aerocapture_positions_enc6_centre" SIGNAL_Y = "aerocapture_positions_enc2_centre" + COLORS = {"forward": "#1f77b4", "backward": "#ff7f0e"} + REDRAW_INTERVAL = 0.5 # s def __init__(self, client): self.client = client self.gui = getattr(client, "gui", None) + self._live: _LivePositions | None = None def plot(self, scan_number: int | None = None): """ - Open a window that plots the measured exposure centres Y versus X of - aerotech_cont_grid, line by line while the scan runs. + Open a window with the measured exposure centres Y versus X of + aerotech_cont_grid, forward and backward lines in two colours. - Without scan_number the plot follows the running (or next) scan; with - scan_number it shows that scan from the history. Calling it again reuses the - window. + Without scan_number the plot shows the last scan and then follows the running + and the next aerotech_cont_grid scans, line by line; with scan_number it shows + that scan. Calling it again reuses the window. The points are sent as plain + data, so the plot shows every exposure (no downsampling); the thin lines + connect the points of each cell column. Examples -------- @@ -38,6 +50,22 @@ class cSAXSAerotech: """ if self.gui is None: raise RuntimeError("No BEC GUI available: start the BEC client with the GUI.") + self.stop_plot() + waveform, curves = self._setup_plot() + if scan_number is not None: + _draw(curves, self._history_lines(scan_number)) + return waveform + self._live = _LivePositions(self, curves) + self._live.start() + return waveform + + def stop_plot(self) -> None: + """Stop following the scans in the plot window (the plot stays).""" + if self._live is not None: + self._live.stop() + self._live = None + + def _setup_plot(self) -> dict: if self.WINDOW in self.gui.windows: window = self.gui.windows[self.WINDOW] else: @@ -48,16 +76,150 @@ class cSAXSAerotech: waveform.x_label = "X centre (aerox)" waveform.y_label = "Y centre (aeroy)" waveform.lock_aspect_ratio = True - curve = waveform.plot( - device_x=self.CAPTURE_DEVICE, - signal_x=self.SIGNAL_X, - device_y=self.CAPTURE_DEVICE, - signal_y=self.SIGNAL_Y, - label="exposure centres", - scan_number=scan_number, + curves = {} + for direction, color in self.COLORS.items(): + curve = waveform.plot(x=[np.nan], y=[np.nan], label=f"{direction} lines") + curve.set_color(color) + curve.set_symbol("o") + curve.set_symbol_size(4) + curve.set_pen_width(1) + curves[direction] = curve + return waveform, curves + + def _history_lines(self, scan_number: int) -> list[tuple[np.ndarray, np.ndarray]]: + scan = self.client.history.get_by_scan_number(scan_number) + if isinstance(scan, list): + scan = scan[-1] if scan else None + if scan is None: + raise ValueError(f"Scan {scan_number} not found in the history.") + return _lines_from_file(scan._msg.file_path) + + +def _lines_from_file(file_path: str) -> list[tuple[np.ndarray, np.ndarray]]: + """Exposure centres per line from an aerotech_cont_grid data file.""" + with h5py.File(file_path, "r") as f: + md = f["entry/collection/metadata/bec"] + n = int(md["frames_per_trigger"][()]) + cap = f[f"entry/collection/devices/{cSAXSAerotech.CAPTURE_DEVICE}"] + x = cap[f"{cSAXSAerotech.SIGNAL_X}/value"][()] + y = cap[f"{cSAXSAerotech.SIGNAL_Y}/value"][()] + return [(x[i : i + n], y[i : i + n]) for i in range(0, len(x), n)] + + +def _column_order(lines: list[tuple[np.ndarray, np.ndarray]], backward: bool): + """ + Points of lines of one direction, ordered by cell column, the columns alternately + up and down. X then increases along the curve (up to the jitter within a column), + which the curve's clip-to-view (bisection on X) needs, and the connecting line runs + along the columns instead of zigzagging. Lines are in measurement order, so a + backward line is reversed to cell order. + """ + cells_x = np.array([line[0][::-1] if backward else line[0] for line in lines]) + cells_y = np.array([line[1][::-1] if backward else line[1] for line in lines]) + rows = np.arange(len(lines)) + columns = [ + (cells_x[order, c], cells_y[order, c]) + for c in range(cells_x.shape[1]) + for order in [rows if c % 2 == 0 else rows[::-1]] + ] + return np.concatenate([c[0] for c in columns]), np.concatenate([c[1] for c in columns]) + + +def _draw(curves: dict, lines: list[tuple[np.ndarray, np.ndarray]]) -> None: + """Even lines are forward, odd lines backward (snake), one curve per direction.""" + for direction, first in (("forward", 0), ("backward", 1)): + selected = lines[first::2] + if not selected or len({len(line[0]) for line in selected}) != 1: + curves[direction].set_data([np.nan], [np.nan]) + continue + x, y = _column_order(selected, backward=first == 1) + curves[direction].set_data(x, y) + + +class _LivePositions: + """Follows aerotech_cont_grid scans and redraws the exposure centres per line.""" + + def __init__(self, parent: cSAXSAerotech, curves: dict): + self.parent = parent + self.connector = parent.client.connector + self.curves = curves + self._lock = threading.Lock() + self._lines: list[tuple[np.ndarray, np.ndarray]] = [] + self._scan_id = None + self._stream_topic = None + self._dirty = threading.Event() + self._stop = threading.Event() + self._worker = threading.Thread(target=self._redraw_loop, daemon=True) + + def start(self) -> None: + self._show_last_scan() + self.connector.register(MessageEndpoints.scan_status(), cb=self._on_scan_status) + status = self.connector.get(MessageEndpoints.scan_status()) + if status is not None and status.status == "open": + self._follow(status) + self._worker.start() + + def stop(self) -> None: + self._stop.set() + self.connector.unregister(MessageEndpoints.scan_status(), cb=self._on_scan_status) + self._unfollow() + + def _show_last_scan(self) -> None: + try: + self._set_lines(_lines_from_file(self.parent.client.history[-1]._msg.file_path)) + except Exception: # no scan yet, or the last one is not an aerotech_cont_grid + pass + + def _on_scan_status(self, msg, **_kwargs) -> None: + status = msg.value + if status.status == "open" and status.scan_id != self._scan_id: + self._follow(status) + + def _follow(self, status) -> None: + if status.scan_name != self.parent.SCAN_NAME: + return + self._unfollow() + self._scan_id = status.scan_id + self._set_lines([]) + self._stream_topic = MessageEndpoints.device_async_signal( + scan_id=status.scan_id, + device=self.parent.CAPTURE_DEVICE, + signal=self.parent.CAPTURE_SIGNAL, ) - # markers for the exposures, a thin line shows the snake order - curve.set_symbol("o") - curve.set_symbol_size(4) - curve.set_pen_width(1) - return waveform + # from_start: the lines published before the subscription are read too + self.connector.register(self._stream_topic, cb=self._on_line, from_start=True) + + def _unfollow(self) -> None: + if self._stream_topic is not None: + self.connector.unregister(self._stream_topic, cb=self._on_line) + self._stream_topic = None + + def _on_line(self, msg: dict, **_kwargs) -> None: + signals = msg["data"].signals + try: + x = np.asarray(signals[self.parent.SIGNAL_X]["value"], dtype=float) + y = np.asarray(signals[self.parent.SIGNAL_Y]["value"], dtype=float) + except KeyError: + return + with self._lock: + self._lines.append((x, y)) + self._dirty.set() + + def _set_lines(self, lines) -> None: + with self._lock: + self._lines = list(lines) + self._dirty.set() + + def _redraw_loop(self) -> None: + # RPC calls to the GUI are made here, never in the Redis callback thread: that + # thread also delivers the RPC replies + while not self._stop.is_set(): + if self._dirty.wait(timeout=self.parent.REDRAW_INTERVAL) and not self._stop.is_set(): + self._dirty.clear() + with self._lock: + lines = list(self._lines) + try: + _draw(self.curves, lines) + except Exception as exc: # window closed, GUI restarted + logger.warning(f"csaxs.aerotech.plot: redraw failed: {exc}") + self._stop.wait(self.parent.REDRAW_INTERVAL) diff --git a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py index 3dbda618..2970bcfb 100644 --- a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py +++ b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py @@ -1,50 +1,141 @@ +import time +from types import SimpleNamespace from unittest import mock +import h5py +import numpy as np import pytest -from csaxs_bec.bec_ipython_client.plugins.cSAXS.aerotech import cSAXSAerotech +from csaxs_bec.bec_ipython_client.plugins.cSAXS.aerotech import ( + _column_order, + _lines_from_file, + cSAXSAerotech, +) + +X0 = 100.0 + + +def _snake(n_lines=4, n_points=5, step=0.01): + """Exposure centres per line in measurement order (backward lines reversed).""" + cells = X0 + step * np.arange(n_points) + lines = [] + for i in range(n_lines): + x = cells if i % 2 == 0 else cells[::-1] + lines.append((x.copy(), np.full(n_points, 0.1 * i))) + return lines def _client(windows=None): gui = mock.MagicMock() gui.windows = windows if windows is not None else {} - return mock.MagicMock(gui=gui) + client = mock.MagicMock(gui=gui) + client.connector.get.return_value = None + return client -def test_plot_opens_window_and_plots_centres(): +def _waveform(client): + return client.gui.new.return_value.new.return_value + + +def test_column_order_runs_up_and_down_the_columns_with_increasing_x(): + lines = _snake() + x, y = _column_order(lines[0::2], backward=False) + assert len(x) == 2 * 5 + assert np.all(np.diff(x) >= 0) + # column 0 upwards (lines 0, 2), column 1 downwards (lines 2, 0) + np.testing.assert_allclose(y[:4], [0.0, 0.2, 0.2, 0.0]) + xb, _ = _column_order(lines[1::2], backward=True) + assert np.all(np.diff(xb) >= 0) + + +def test_lines_from_file(tmp_path): + lines = _snake() + path = tmp_path / "scan.h5" + with h5py.File(path, "w") as f: + f["entry/collection/metadata/bec/frames_per_trigger"] = 5 + cap = f.create_group("entry/collection/devices/aerocapture") + cap["aerocapture_positions_enc6_centre/value"] = np.concatenate([l[0] for l in lines]) + cap["aerocapture_positions_enc2_centre/value"] = np.concatenate([l[1] for l in lines]) + loaded = _lines_from_file(str(path)) + assert len(loaded) == 4 + np.testing.assert_allclose(loaded[1][0], lines[1][0]) + + +def test_plot_history_draws_forward_and_backward_curves(): client = _client() - waveform = client.gui.new.return_value.new.return_value - - assert cSAXSAerotech(client).plot() is waveform + waveform = _waveform(client) + forward, backward = mock.MagicMock(), mock.MagicMock() + waveform.plot.side_effect = [forward, backward] + tool = cSAXSAerotech(client) + with mock.patch.object(tool, "_history_lines", return_value=_snake()): + assert tool.plot(scan_number=8494) is waveform client.gui.new.assert_called_once_with("aerotech", timeout=20) client.gui.new.return_value.new.assert_called_once_with( "Waveform", object_name="aerotech_positions", timeout=20 ) - waveform.clear_all.assert_called_once_with() assert waveform.lock_aspect_ratio is True - waveform.plot.assert_called_once_with( - device_x="aerocapture", - signal_x="aerocapture_positions_enc6_centre", - device_y="aerocapture", - signal_y="aerocapture_positions_enc2_centre", - label="exposure centres", - scan_number=None, - ) - waveform.plot.return_value.set_symbol.assert_called_once_with("o") + for curve in (forward, backward): + x, y = curve.set_data.call_args.args + assert len(x) == 10 and np.all(np.diff(x) >= 0) + client.connector.register.assert_not_called() # history: no live subscription -def test_plot_reuses_open_window_and_shows_history(): +def test_plot_reuses_open_window(): window = mock.MagicMock() client = _client(windows={"aerotech": window}) - - cSAXSAerotech(client).plot(scan_number=8494) - + tool = cSAXSAerotech(client) + with mock.patch.object(tool, "_history_lines", return_value=_snake()): + tool.plot(scan_number=1) client.gui.new.assert_not_called() - assert window.new.return_value.plot.call_args.kwargs["scan_number"] == 8494 + window.new.assert_called_once() def test_plot_without_gui_raises(): client = mock.MagicMock(spec=["device_manager"]) with pytest.raises(RuntimeError, match="No BEC GUI"): cSAXSAerotech(client).plot() + + +def test_live_follows_an_aerotech_scan_line_by_line(): + client = _client() + waveform = _waveform(client) + forward, backward = mock.MagicMock(), mock.MagicMock() + waveform.plot.side_effect = [forward, backward] + client.history.__getitem__.side_effect = IndexError # no scan yet + tool = cSAXSAerotech(client) + tool.plot() + live = tool._live + status = SimpleNamespace( + status="open", scan_id="abc", scan_name="aerotech_cont_grid", scan_number=1 + ) + live._on_scan_status(SimpleNamespace(value=status)) + topic = client.connector.register.call_args_list[-1] + assert topic.kwargs["from_start"] is True + + for x, y in _snake(n_lines=2): + signals = { + "aerocapture_positions_enc6_centre": {"value": x}, + "aerocapture_positions_enc2_centre": {"value": y}, + } + live._on_line({"data": SimpleNamespace(signals=signals)}) + deadline = time.time() + 3 + while time.time() < deadline and not backward.set_data.call_args_list: + time.sleep(0.05) + tool.stop_plot() + + x, _ = backward.set_data.call_args.args + assert len(x) == 5 + client.connector.unregister.assert_called() + + +def test_live_ignores_other_scans(): + client = _client() + client.history.__getitem__.side_effect = IndexError + tool = cSAXSAerotech(client) + tool.plot() + registered = client.connector.register.call_count + status = SimpleNamespace(status="open", scan_id="x", scan_name="line_scan", scan_number=2) + tool._live._on_scan_status(SimpleNamespace(value=status)) + assert client.connector.register.call_count == registered + tool.stop_plot() -- 2.54.0 From bff159095cc3943fa7aabdd7ccb291d155596b8b Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 15:08:09 +0100 Subject: [PATCH 41/52] docs(aerotech): live plot colours, stop_plot Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- docs/user/saxs/aerotech_stages/aerotech_stages.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index d16c3ddf..6fe14aa3 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -82,13 +82,18 @@ scan, also after an aborted one, X returns to its normal move velocity. With `trigger_source="ddg"` the client shows a progress bar of the frames. To watch the measured positions while the scan runs, open the live plot of the -exposure centres (Y versus X, one line after the other): +exposure centres (Y versus X, forward and backward lines in two colours, updated +after each line): ```python -csaxs.aerotech.plot() # follows the running and the next scans +csaxs.aerotech.plot() # shows the last scan, then follows the running and the next scans csaxs.aerotech.plot(scan_number=8494) # a finished scan +csaxs.aerotech.stop_plot() # stop following (the plot stays) ``` +Every exposure is shown. The thin lines connect the points of each cell column; they +only guide the eye. + ### Examples Fields of view centred on the current position (`relative=True`), X fast, Y step: -- 2.54.0 From 8c19ed0807182658a3882f28afb82a797e5359d7 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 15:16:44 +0100 Subject: [PATCH 42/52] fix(csaxs): aerotech plot shows markers only and reuses its plot - the column-wise connecting lines suggested a vertical fast axis; a NaN after every point now breaks the curve, so only the exposure markers are drawn (X stays finite and sorted for clip-to-view) - a second plot() call added a second Waveform to the window (new() with the same object_name does not reuse it); the existing one is now looked up with widget_map() and cleared Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../plugins/cSAXS/aerotech.py | 31 +++++++++++++------ .../saxs/aerotech_stages/aerotech_stages.md | 4 +-- .../test_cSAXS_aerotech.py | 16 +++++++--- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py index 28820542..738469e7 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py @@ -40,8 +40,7 @@ class cSAXSAerotech: Without scan_number the plot shows the last scan and then follows the running and the next aerotech_cont_grid scans, line by line; with scan_number it shows that scan. Calling it again reuses the window. The points are sent as plain - data, so the plot shows every exposure (no downsampling); the thin lines - connect the points of each cell column. + data, so the plot shows every exposure (no downsampling), as markers. Examples -------- @@ -70,7 +69,10 @@ class cSAXSAerotech: window = self.gui.windows[self.WINDOW] else: window = self.gui.new(self.WINDOW, timeout=self.GUI_RPC_TIMEOUT) - waveform = window.new("Waveform", object_name=self.PLOT, timeout=self.GUI_RPC_TIMEOUT) + # reuse the plot of an earlier call (new() would add a second one) + waveform = window.widget_map().get(self.PLOT) + if waveform is None: + waveform = window.new("Waveform", object_name=self.PLOT, timeout=self.GUI_RPC_TIMEOUT) waveform.clear_all() waveform.title = "aerotech_cont_grid: exposure centres" waveform.x_label = "X centre (aerox)" @@ -82,7 +84,6 @@ class cSAXSAerotech: curve.set_color(color) curve.set_symbol("o") curve.set_symbol_size(4) - curve.set_pen_width(1) curves[direction] = curve return waveform, curves @@ -108,11 +109,10 @@ def _lines_from_file(file_path: str) -> list[tuple[np.ndarray, np.ndarray]]: def _column_order(lines: list[tuple[np.ndarray, np.ndarray]], backward: bool): """ - Points of lines of one direction, ordered by cell column, the columns alternately - up and down. X then increases along the curve (up to the jitter within a column), - which the curve's clip-to-view (bisection on X) needs, and the connecting line runs - along the columns instead of zigzagging. Lines are in measurement order, so a - backward line is reversed to cell order. + Points of lines of one direction, ordered by cell column. X then increases along + the curve (up to the jitter within a column), which the curve's clip-to-view + (bisection on X) needs. Lines are in measurement order, so a backward line is + reversed to cell order. """ cells_x = np.array([line[0][::-1] if backward else line[0] for line in lines]) cells_y = np.array([line[1][::-1] if backward else line[1] for line in lines]) @@ -133,7 +133,18 @@ def _draw(curves: dict, lines: list[tuple[np.ndarray, np.ndarray]]) -> None: curves[direction].set_data([np.nan], [np.nan]) continue x, y = _column_order(selected, backward=first == 1) - curves[direction].set_data(x, y) + curves[direction].set_data(*_markers_only(x, y)) + + +def _markers_only(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """ + A NaN after every point: the curve draws no connecting lines (a non-finite value is a + gap), only the markers. X stays finite and sorted for the clip-to-view. + """ + x2 = np.repeat(x, 2) + y2 = np.full(2 * len(y), np.nan) + y2[0::2] = y + return x2, y2 class _LivePositions: diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index 6fe14aa3..6cc71d5b 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -91,8 +91,8 @@ csaxs.aerotech.plot(scan_number=8494) # a finished scan csaxs.aerotech.stop_plot() # stop following (the plot stays) ``` -Every exposure is shown. The thin lines connect the points of each cell column; they -only guide the eye. +Every exposure is shown as a marker. Forward and backward lines alternate, so the two +colours alternate in Y by one line spacing. ### Examples diff --git a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py index 2970bcfb..b53ed302 100644 --- a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py +++ b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py @@ -28,6 +28,7 @@ def _snake(n_lines=4, n_points=5, step=0.01): def _client(windows=None): gui = mock.MagicMock() gui.windows = windows if windows is not None else {} + gui.new.return_value.widget_map.return_value = {} client = mock.MagicMock(gui=gui) client.connector.get.return_value = None return client @@ -77,18 +78,23 @@ def test_plot_history_draws_forward_and_backward_curves(): assert waveform.lock_aspect_ratio is True for curve in (forward, backward): x, y = curve.set_data.call_args.args - assert len(x) == 10 and np.all(np.diff(x) >= 0) + # markers only: a NaN after every point, X finite and increasing + assert len(x) == 20 and np.all(np.diff(x) >= 0) and np.isfinite(x).all() + assert np.isfinite(y[0::2]).all() and np.isnan(y[1::2]).all() client.connector.register.assert_not_called() # history: no live subscription -def test_plot_reuses_open_window(): +def test_plot_reuses_open_window_and_plot(): window = mock.MagicMock() + existing = mock.MagicMock() + window.widget_map.return_value = {"aerotech_positions": existing} client = _client(windows={"aerotech": window}) tool = cSAXSAerotech(client) with mock.patch.object(tool, "_history_lines", return_value=_snake()): - tool.plot(scan_number=1) + assert tool.plot(scan_number=1) is existing client.gui.new.assert_not_called() - window.new.assert_called_once() + window.new.assert_not_called() + existing.clear_all.assert_called_once_with() def test_plot_without_gui_raises(): @@ -125,7 +131,7 @@ def test_live_follows_an_aerotech_scan_line_by_line(): tool.stop_plot() x, _ = backward.set_data.call_args.args - assert len(x) == 5 + assert len(x) == 2 * 5 client.connector.unregister.assert_called() -- 2.54.0 From 2c5ea7e6bff841f2362fafbca04cc5ea06570e5b Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 15:21:51 +0100 Subject: [PATCH 43/52] fix(csaxs): aerotech plot stops following once its window is closed A closed plot made every redraw fail with a warning for each line of each scan. The first failure now stops following the scans (unsubscribes) with one message to reopen the plot with csaxs.aerotech.plot(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../plugins/cSAXS/aerotech.py | 12 +++++++++-- .../test_cSAXS_aerotech.py | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py index 738469e7..22882e59 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py @@ -231,6 +231,14 @@ class _LivePositions: lines = list(self._lines) try: _draw(self.curves, lines) - except Exception as exc: # window closed, GUI restarted - logger.warning(f"csaxs.aerotech.plot: redraw failed: {exc}") + except Exception as exc: # window or plot closed, GUI restarted + # stop following instead of failing on every line of every scan + logger.warning( + f"csaxs.aerotech.plot: the plot is gone ({exc}); stopped following" + " the scans. Call csaxs.aerotech.plot() to open it again." + ) + self.stop() + if self.parent._live is self: + self.parent._live = None + return self._stop.wait(self.parent.REDRAW_INTERVAL) diff --git a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py index b53ed302..49e8a053 100644 --- a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py +++ b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py @@ -145,3 +145,24 @@ def test_live_ignores_other_scans(): tool._live._on_scan_status(SimpleNamespace(value=status)) assert client.connector.register.call_count == registered tool.stop_plot() + + +def test_live_stops_once_when_the_plot_is_closed(): + client = _client() + waveform = _waveform(client) + forward, backward = mock.MagicMock(), mock.MagicMock() + forward.set_data.side_effect = RuntimeError("widget has been deleted") + waveform.plot.side_effect = [forward, backward] + client.history.__getitem__.side_effect = IndexError + tool = cSAXSAerotech(client) + with mock.patch( + "csaxs_bec.bec_ipython_client.plugins.cSAXS.aerotech.logger" + ) as logger: + tool.plot() + live = tool._live + live._set_lines(_snake(n_lines=2)) + live._worker.join(timeout=3) + assert not live._worker.is_alive() + assert tool._live is None + assert logger.warning.call_count == 1 + client.connector.unregister.assert_called() -- 2.54.0 From 171fc17cd21e3193d92cec9901f9d17e2e4c2eef Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 29 Oct 2026 16:15:18 +0100 Subject: [PATCH 44/52] docs(aerotech): measured comparison default vs minimal run-up, position plots Both series of 2026-10-29 (8486-8494 default, 8501-8509 minimal_run_up): timing and positioning per case, why the saving is about half the shorter travel (Y step no longer hidden in the run-out), the constant direction offset after the velocity transient and the 20 mm/s oscillation, deviation and zoom plots, and the split of the line period with the next levers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../saxs/aerotech_stages/aerotech_stages.md | 150 +++++++++++++----- .../run_up_deviation_1x1_10um_20ms.png | Bin 0 -> 97896 bytes .../run_up_deviation_2x2_100um_5ms.png | Bin 0 -> 160473 bytes .../run_up_deviation_2x2_40um_20ms.png | Bin 0 -> 97609 bytes .../run_up_deviation_3p5x3_100um_5ms.png | Bin 0 -> 129335 bytes .../run_up_zoom_1x1_10um_20ms.png | Bin 0 -> 68101 bytes .../run_up_zoom_2x2_100um_5ms.png | Bin 0 -> 58723 bytes .../run_up_zoom_2x2_40um_20ms.png | Bin 0 -> 62926 bytes 8 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 docs/user/saxs/aerotech_stages/run_up_deviation_1x1_10um_20ms.png create mode 100644 docs/user/saxs/aerotech_stages/run_up_deviation_2x2_100um_5ms.png create mode 100644 docs/user/saxs/aerotech_stages/run_up_deviation_2x2_40um_20ms.png create mode 100644 docs/user/saxs/aerotech_stages/run_up_deviation_3p5x3_100um_5ms.png create mode 100644 docs/user/saxs/aerotech_stages/run_up_zoom_1x1_10um_20ms.png create mode 100644 docs/user/saxs/aerotech_stages/run_up_zoom_2x2_100um_5ms.png create mode 100644 docs/user/saxs/aerotech_stages/run_up_zoom_2x2_40um_20ms.png diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index 6cc71d5b..4b664527 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -158,53 +158,119 @@ centres from the nominal cells, backward minus forward offset, and the line time ## Timing and positioning -Measured 2026-10-29 (scans 8486-8494, default run-up, PandABox as trigger, readout -0.2 ms, X acceleration 500 mm/s^2). Positions in um; "rms" is the scatter of the -measured exposure centres around the nominal cells (forward and backward lines -alike), "bwd - fwd" the offset between neighbouring lines with its slope along X -and its peak-to-peak over the line. +Measured 2026-10-29 with the PandABox as trigger (readout 0.2 ms), X acceleration +500 mm/s^2, X centred at 123.25, and the same nine scans in both run-up modes: +scans 8486-8494 with the default run-up, 8501-8509 with `minimal_run_up=True`. +The target resolution is about 20 um; the positioning accuracy should be well below +that. -| Field of view (mm) | Step / exposure (speed) | Points x lines | rms | bwd - fwd | slope (um/mm) | peak-to-peak | Line period | Exposure per line | Positioning overhead per line | Scan time | Estimate with uneven positions (line, scan) | -|---|---|---|---|---|---|---|---|---|---|---|---| -| 0.8 x 1.2 | 100 um / 5 ms (20 mm/s) | 9 x 13 | 2.9 | +1.2 | +3.4 | 11.4 | 0.79 s | 0.04 s | 0.74 s (94 %) | 0.2 min | 0.45 s, 0.1 min | -| 0.8 x 1.2 | 40 um / 20 ms (2 mm/s) | 21 x 31 | 1.0 | +3.6 | +0.2 | 2.9 | 1.03 s | 0.42 s | 0.61 s (59 %) | 0.6 min | 0.73 s, 0.4 min | -| 0.8 x 1.2 | 10 um / 20 ms (0.5 mm/s) | 81 x 121 | 0.2 | +0.1 | 0.0 | 0.8 | 2.21 s | 1.62 s | 0.59 s (27 %) | 4.5 min | 1.95 s, 3.9 min | -| 2 x 2 | 100 um / 5 ms (20 mm/s) | 21 x 21 | 5.6 | +2.3 | +0.2 | 25.6 | 0.83 s | 0.10 s | 0.72 s (87 %) | 0.4 min | 0.49 s, 0.2 min | -| 2 x 2 | 40 um / 20 ms (2 mm/s) | 51 x 51 | 0.6 | +2.8 | 0.0 | 2.4 | 1.64 s | 1.02 s | 0.62 s (38 %) | 1.5 min | 1.34 s, 1.1 min | -| 1 x 1 | 10 um / 20 ms (0.5 mm/s) | 101 x 101 | 0.2 | 0.0 | 0.0 | 0.8 | 2.61 s | 2.02 s | 0.59 s (23 %) | 4.5 min | 2.35 s, 4.0 min | -| 3.5 x 3.0 | 100 um / 5 ms (20 mm/s) | 36 x 31 | 5.7 | +0.8 | +0.1 | 30.6 | 0.91 s | 0.18 s | 0.73 s (80 %) | 0.6 min | 0.57 s, 0.3 min | -| 3.5 x 3.0 | 40 um / 20 ms (2 mm/s) | 89 x 76 | 0.5 | +2.8 | 0.0 | 2.9 | 2.40 s | 1.78 s | 0.62 s (26 %) | 3.1 min | 2.10 s, 2.7 min | -| 3.5 x 3.0 | 10 um / 20 ms (0.5 mm/s) | 351 x 301 | 0.1 | -0.7 | 0.0 | 0.9 | 7.75 s | 7.02 s | 0.73 s (9 %) | 38.9 min | 7.49 s, 37.6 min | +Terms used below (positions in um): + +- **rms:** scatter of the measured exposure centres around the nominal cells. +- **bwd - fwd:** offset between neighbouring backward and forward lines, with its + slope along X and its peak-to-peak over the line. A constant offset only shifts the + field of view; a slope would distort it. +- **Line period:** time from the start of one line to the start of the next. +- **Positioning overhead:** line period minus the exposure per line (points times + `exp_time`), per line and as a fraction of the scan time. It contains the run-up and + run-out, the turnaround, the Y step, and the readout of the captured positions. + +### Default run-up + +| Field of view (mm) | Step / exposure (speed) | Points x lines | rms | bwd - fwd | slope (um/mm) | peak-to-peak | Line period | Exposure per line | Positioning overhead per line | Scan time | +|---|---|---|---|---|---|---|---|---|---|---| +| 0.8 x 1.2 | 100 um / 5 ms (20 mm/s) | 9 x 13 | 2.9 | +1.2 | +3.4 | 11.4 | 0.79 s | 0.04 s | 0.74 s (94 %) | 0.2 min | +| 0.8 x 1.2 | 40 um / 20 ms (2 mm/s) | 21 x 31 | 1.0 | +3.6 | +0.2 | 2.9 | 1.03 s | 0.42 s | 0.61 s (59 %) | 0.6 min | +| 0.8 x 1.2 | 10 um / 20 ms (0.5 mm/s) | 81 x 121 | 0.2 | +0.1 | 0.0 | 0.8 | 2.21 s | 1.62 s | 0.59 s (27 %) | 4.5 min | +| 2 x 2 | 100 um / 5 ms (20 mm/s) | 21 x 21 | 5.6 | +2.3 | +0.2 | 25.6 | 0.83 s | 0.10 s | 0.72 s (87 %) | 0.4 min | +| 2 x 2 | 40 um / 20 ms (2 mm/s) | 51 x 51 | 0.6 | +2.8 | 0.0 | 2.4 | 1.64 s | 1.02 s | 0.62 s (38 %) | 1.5 min | +| 1 x 1 | 10 um / 20 ms (0.5 mm/s) | 101 x 101 | 0.2 | 0.0 | 0.0 | 0.8 | 2.61 s | 2.02 s | 0.59 s (23 %) | 4.5 min | +| 3.5 x 3.0 | 100 um / 5 ms (20 mm/s) | 36 x 31 | 5.7 | +0.8 | +0.1 | 30.6 | 0.91 s | 0.18 s | 0.73 s (80 %) | 0.6 min | +| 3.5 x 3.0 | 40 um / 20 ms (2 mm/s) | 89 x 76 | 0.5 | +2.8 | 0.0 | 2.9 | 2.40 s | 1.78 s | 0.62 s (26 %) | 3.1 min | +| 3.5 x 3.0 | 10 um / 20 ms (0.5 mm/s) | 351 x 301 | 0.1 | -0.7 | 0.0 | 0.9 | 7.75 s | 7.02 s | 0.73 s (9 %) | 38.9 min | - **Positioning:** at 0.5 and 2 mm/s the exposure centres are within about 1 um rms of the nominal cells, and the snake is symmetric (no slope). A constant backward minus forward offset of up to ~4 um remains; it only shifts the field of - view. At 20 mm/s an oscillation of X after the acceleration ramp reaches into the - line: up to 25-30 um peak-to-peak on the larger fields. That is small compared with - the 100 um cells of these quick scans, but above the 20 um resolution target. -- **Overhead:** about 0.6-0.75 s per line at every speed: run-up and run-out, - turnaround, the Y step (during the run-out of X), and the readout of the captured - positions (0.03-0.3 s, growing with the points per line). It dominates the quick + view. At 20 mm/s a ~10 Hz oscillation of X after the acceleration ramp reaches into + the line: up to 25-30 um peak-to-peak on the larger fields. That is small compared + with the 100 um cells of these quick scans, but above the 20 um resolution target. +- **Overhead:** about 0.6-0.75 s per line at every speed. It dominates the quick scans (80-94 % at 20 mm/s) and is small for the long high resolution lines. -- **Uneven positions:** the last column is an estimate, not a measurement: the line - period minus the run-up and run-out that could be saved if the frames did not have - to sit on an even grid (see below). It will be replaced by measured values of the - same scans with `minimal_run_up=True`. -The target resolution is about 20 um; the positioning accuracy should be well below -that. Columns of the result table: +### Minimal run-up (`minimal_run_up=True`) -- **Positioning accuracy:** measured exposure centres minus nominal cell centres - (rms), and the backward minus forward offset between neighbouring lines. A constant - offset only shifts the field of view; a slope along the line would distort it. -- **Line period:** time from the start of one line to the start of the next. -- **Exposure per line:** points per line times `exp_time`. -- **Positioning overhead:** line period minus exposure per line, as an absolute - time per line and as a fraction of the scan time. It contains the run-up and - run-out, the turnaround, the Y step (done during the run-out of X), and the - readout of the captured positions. -- **Estimate with uneven positions:** if the data analysis could place frames at - unevenly spaced positions, the exposures would not have to wait for X to settle at - constant velocity, and the run-up could shrink to the braking distance. That saves - about `v / a + 2 * settle_time` per line (0.30-0.34 s at 0.5-20 mm/s), on top of - any reduction of the other overheads. +With the minimal run-up X only accelerates to the scan velocity before the first +exposure (at least 10 um), without the settling distance. The exposures are then +less evenly spaced, which is acceptable if the data analysis uses the measured +positions. + +| Field of view (mm) | Step / exposure (speed) | Line period default -> minimal | Saved per line | Scan time default -> minimal | rms default -> minimal | bwd - fwd default -> minimal | max deviation default -> minimal | +|---|---|---|---|---|---|---|---| +| 0.8 x 1.2 | 100 um / 5 ms (20 mm/s) | 0.78 -> 0.61 s | 0.17 s (22 %) | 0.2 -> 0.2 min | 2.9 -> 7.0 | +1.2 -> +5.6 | 5.8 -> 17.1 | +| 0.8 x 1.2 | 40 um / 20 ms (2 mm/s) | 1.02 -> 0.88 s | 0.15 s (14 %) | 0.6 -> 0.5 min | 1.0 -> 3.1 | +3.6 -> +13.3 | 4.6 -> 15.9 | +| 0.8 x 1.2 | 10 um / 20 ms (0.5 mm/s) | 2.20 -> 2.09 s | 0.11 s (5 %) | 4.5 -> 4.3 min | 0.2 -> 1.1 | +0.1 -> -8.9 | 1.0 -> 5.5 | +| 2 x 2 | 100 um / 5 ms (20 mm/s) | 0.84 -> 0.65 s | 0.19 s (22 %) | 0.4 -> 0.3 min | 5.6 -> 10.0 | +2.3 -> -11.1 | 15.6 -> 24.7 | +| 2 x 2 | 40 um / 20 ms (2 mm/s) | 1.63 -> 1.48 s | 0.16 s (10 %) | 1.5 -> 1.3 min | 0.6 -> 2.1 | +2.8 -> +13.6 | 4.5 -> 15.9 | +| 1 x 1 | 10 um / 20 ms (0.5 mm/s) | 2.60 -> 2.50 s | 0.10 s (4 %) | 4.5 -> 4.3 min | 0.2 -> 1.0 | 0.0 -> -9.1 | 1.1 -> 5.4 | +| 3.5 x 3.0 | 100 um / 5 ms (20 mm/s) | 0.91 -> 0.74 s | 0.17 s (19 %) | 0.6 -> 0.5 min | 5.8 -> 14.0 | +0.8 -> -42.9 | 17.5 -> 44.9 | +| 3.5 x 3.0 | 40 um / 20 ms (2 mm/s) | 2.39 -> 2.23 s | 0.15 s (6 %) | 3.1 -> 2.9 min | 0.5 -> 1.7 | +2.8 -> +12.8 | 3.9 -> 16.6 | +| 3.5 x 3.0 | 10 um / 20 ms (0.5 mm/s) | 7.75 -> 7.55 s | 0.19 s (2 %) | 38.9 -> 38.0 min | 0.1 -> 0.6 | -0.7 -> -10.2 | 0.8 -> 5.6 | + +- **Time:** 0.10-0.19 s less per line, 19-22 % of the quick 20 mm/s scans, 6-14 % + at 2 mm/s, 2-5 % at 0.5 mm/s. About half of what the shorter travel alone would + give: with the default run-up the Y step is hidden in the run-out of X; with the + minimal run-up X turns around almost at once and waits for Y. +- **Positions, 0.5 and 2 mm/s:** the first exposure of each line is still at its + nominal position (the line trigger is a position), but within the next 2-3 cells + the exposures jump to a constant offset, then stay evenly spaced along the line. + This is the velocity transient of X after the ramp: the gates are timed, so a + velocity below (2 mm/s) or above (0.5 mm/s) the scan velocity right after the + trigger shifts all later exposures. The offset has opposite signs in the two + directions, so backward and forward lines are 9-14 um apart, without a slope. +- **Positions, 20 mm/s:** the ~10 Hz oscillation after the ramp is now in the whole + line: up to 25 um deviation on 2 mm and 45 um on 3.5 mm fields, with backward + minus forward offsets up to 43 um. Well above the 20 um resolution target. +- **For the data analysis:** use the measured exposure centres, not the nominal + grid. In the first 2-3 cells of each line the cells are compressed or stretched, + and the centre estimate (start of the gate plus half a gate at the scan velocity) + is less accurate there, since X is not yet at constant velocity. + +Measured minus nominal X centre along the line, all lines thin, the mean bold +(left: default run-up, right: minimal run-up): + +![2 x 2 mm, 100 um / 5 ms (20 mm/s): the oscillation after the ramp](run_up_deviation_2x2_100um_5ms.png) + +![3.5 x 3.0 mm, 100 um / 5 ms (20 mm/s)](run_up_deviation_3p5x3_100um_5ms.png) + +![2 x 2 mm, 40 um / 20 ms (2 mm/s): constant offset after the first cells](run_up_deviation_2x2_40um_20ms.png) + +![1 x 1 mm, 10 um / 20 ms (0.5 mm/s)](run_up_deviation_1x1_10um_20ms.png) + +Measured exposure centres of the first cells of the first lines against the nominal +cell centres (crosses), forward lines blue, backward lines orange: + +![2 x 2 mm, 100 um / 5 ms (20 mm/s)](run_up_zoom_2x2_100um_5ms.png) + +![2 x 2 mm, 40 um / 20 ms (2 mm/s)](run_up_zoom_2x2_40um_20ms.png) + +![1 x 1 mm, 10 um / 20 ms (0.5 mm/s)](run_up_zoom_1x1_10um_20ms.png) + +### Where the time per line goes + +Split of the line period, measured by the scan (`motion` = X through the line and the +trigger check, `between lines` = waiting for Y and switching the trigger window): + +| Case | Run-up | Line period | Motion | Capture readout | Between lines | +|---|---|---|---|---|---| +| 0.8 mm, 20 mm/s | default -> minimal | 0.77 -> 0.58 s | 0.60 -> 0.29 s | 0.03 s | 0.14 -> 0.26 s | +| 0.8 mm, 2 mm/s | default -> minimal | 1.03 -> 0.88 s | 0.87 -> 0.62 s | 0.04-0.05 s | 0.12 -> 0.21 s | +| 2 x 2 mm, 2 mm/s | default -> minimal | 1.64 -> 1.48 s | 1.47 -> 1.21 s | 0.05-0.06 s | 0.12 -> 0.21 s | + +The next levers to reduce the overhead: + +- **Y step:** Y moves with 10 mm/s^2, so a 100 um step takes ~0.2 s, plus a 0.1 s + hold within the tolerance before the next line starts. +- **Software latency:** roughly 0.15-0.2 s per line in the motion phase for the end + of the move, the trigger check and the communication. +- **Capture readout:** 0.03-0.3 s per line, growing with the points per line. diff --git a/docs/user/saxs/aerotech_stages/run_up_deviation_1x1_10um_20ms.png b/docs/user/saxs/aerotech_stages/run_up_deviation_1x1_10um_20ms.png new file mode 100644 index 0000000000000000000000000000000000000000..8e96747a459474f52f7c334d557fcd688d221f6b GIT binary patch literal 97896 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 literal 0 HcmV?d00001 diff --git a/docs/user/saxs/aerotech_stages/run_up_deviation_2x2_100um_5ms.png b/docs/user/saxs/aerotech_stages/run_up_deviation_2x2_100um_5ms.png new file mode 100644 index 0000000000000000000000000000000000000000..3e4e16079332f971d8be2186865420431f1d9c45 GIT binary patch literal 160473 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≺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 literal 0 HcmV?d00001 diff --git a/docs/user/saxs/aerotech_stages/run_up_deviation_2x2_40um_20ms.png b/docs/user/saxs/aerotech_stages/run_up_deviation_2x2_40um_20ms.png new file mode 100644 index 0000000000000000000000000000000000000000..615c24d203ed047e864d7644ae81225e0f0488a1 GIT binary patch literal 97609 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 Jl+g10e*g%%{lx$P literal 0 HcmV?d00001 diff --git a/docs/user/saxs/aerotech_stages/run_up_deviation_3p5x3_100um_5ms.png b/docs/user/saxs/aerotech_stages/run_up_deviation_3p5x3_100um_5ms.png new file mode 100644 index 0000000000000000000000000000000000000000..09e7e5cbd373d77b0065e67627747f589fd67195 GIT binary patch literal 129335 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 z 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

^`T6Lnw>hd3E?pzkfcynHorJCF=uYHCV< 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%0AcbQwt_Aa4l> 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

JMOvpys;@kwrY=*=Q3QIH8T?J^1_i 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 literal 0 HcmV?d00001 diff --git a/docs/user/saxs/aerotech_stages/run_up_zoom_2x2_40um_20ms.png b/docs/user/saxs/aerotech_stages/run_up_zoom_2x2_40um_20ms.png new file mode 100644 index 0000000000000000000000000000000000000000..4662d513e200e9d4495d30b44f43db0619ad960a GIT binary patch literal 62926 zcmeFZhd-C^8$NuaG__?V(hw<;k*z^dk;urZ5R#F-DGeo}L`f70$zItsD3ZN18dmn+ zJjd1d`}_T#-}5|w!1KIbkJo2J-tYH)U)On^=W!m#ao+dNDxF|h&AOVRCG_kd{vJvO!H~*i%alq2rn7>Hd?mOOPmDNct8;aVzNdBc+5h!+xq9()@jvrBT z4DD}q&{Qj#6B%~t)sG3veNO<<@3RZQT_v}6IEw;P8y}e_48nbM;n={PUPUb zzvk4bQ`;o$*W#j*($hb$mznppXw7Ckxlnsm_+5NF4fXjx<1sIKhK|8OKb!9FKc`22 zQ=Y3f@9FscQ09RDEF)t;K)_Qe7ZIUjpYN{c`0(|sf@Q&GZY9pWdsi+kOmSSTdwcus z`M6(=$p(i#79~rA_WK;WyRN9LY+pfKxMhM)*3A^7vIv){!N#OZ$1>)+I`eEM{+<`I z_`$4^s3#Yt_~dM~68oo;lHZ^1ZML$r`@Fa?r;_Jvml|qtL@URB|A!ABHn6k%X12R) zKi6waJTfDv(0=I8k1U&&D_6=ExGyAK{=E87tv~Ku&-o~p;&owyZqBYv`cviBMH?Qs}t+wVUc6&-zQVRpg@|2_%|dXsAY@eadfQ&S$w5PQA+TtP%f z<*Q?tFJ0PUnLqP}+CYsC|2&q5wMw6!n_ELYlXlA%k=nWQ7(D~)F6uSHXGUwnRn%cn zhlaB(n7`tBdv2}+R(~I5INVY6C?Md~nQ#&F5X%2=U0mO5CcCTY7fOTq`FAS@`UeJX zd0~Egfi+M+a6Iqbt-E)96r&ztVV@@b+tRlko6B%NhJswQ9v&W{QZ8HfslO}qq@i1# zt>}D*f$h#`A0~s!mq#?SZB378L{}=DIU_gFoIZlf4V89d#ir6-oW9rT%O)Ce;^Fq; zsi~6Qn&?;OV$)h4J$|h6<}{0><1pp1v>s%6hEZGT+n>A2pI60ei{>s& zws`ybiHPf~yr+M5pJ%qsk6ofNL04ieo5)(9t@{P9Gu-p`?lCG63&Q`u7~;HKt$(+Q zlCNlSnwvBaJfO?U`((Au(tK*$ z`v(NvP?lNPvbeaIl$m)qJe=drndkKB7Z|9Mr%!(>yg{deb!lvBdZqM2yfMYdr^s4( zjESyLwB6OKIo<4IQ4x)vo*uK5bDF*0@42ZVgKtj-_G)MF_4oI0*s#H~vn;qfe`AUO zKAPNX(aq&n*ROxIZ!do?N=G3iZEu!R9UUE>h&5VD#Jt&Xd?eiRF_YAixZx9v(gtqs z_1h&2hYFVbw(h_1hbLyO;Zj>wnoXufD_$7l`s&pyDlpx~%IY9iU~;h42h0BI%yW^w z>hG9;{POl7`T5gKs%Q@xe;3LZpj;>FG$>CTrhTQs&&@VaeERv@u()(9)_uxz znL23w=VTy{D*fc-WQbnn`=U6t1A-&!1aZw;j^bdWqlg>qn!`?Kpgx z87ZsiwZh}G`L0f6Wvx3)pGr6+*tb9CR*t}7-m4O^Y}>YNmAOtf+oW9&m(563kzE?v5D#EZUnnHTcxEorw|PGMojs;a6O_4o_<4MTEn zv*Sf1TdTcQTNSUJS=^fAu#u7RHa2}T(*Ye>B)Q=d4iA#Kgs;{X;=0I{^f#q)mkGAJ zP3h)N)N2oqj~~ymAMg(exj!@39f_m!-eG7vtMJuZSn9~@zl%*8lXm?2{ae7KlGAl= zATvw+?z?SL&RfDg7R3-451dEOA2POU&9-B(Xn1WlS+>5*(Jf_ktU~5kym}>4ce*OAy(OX-S$xpC>4ue0An3(K+qa5zHnvwUVU0?k)B5AlcI{fk{ z?~US}hF7jcs75O>AG)&kfX;hQq=E?Jim;e-QLDd)N%_RY?2YUT3=MtoLc-xgT^*y{ z-&z${B`2qcGC1zOtim72Sxqxl6*8%$qLp48b#y$8#k);z?tGm3Nkzq@xcjqj)p)L< zMjSbE*NGi-c%xy*ROBRKm3{{Egw%4{5N;~SA`Zx#Ep{r+%!jY(^^ zFcRED#2LAAdb)bgpZN~Iiy3MYblJ!ng-JTytdG}@#2t`Q_x**$NV#Zx&l3Y|?aP;( zlJ4`&xPWs%^hjB|zwe9FVWjq+&J=@^bu^r)kJbybc5+VKXF_&SyAB`Ll+d`gw79U` z^VY3fPhY!dTDET$6&0o7?5mGIB;JlwoNmJ~H#b*?EST1Uh~6zCB0_RIvc@JBme@lL z1E@Ob-4k4Ao`r>lSx@$Bna|e6X+#~rw~1C3Rc#LC~_pI zy1M$cyf5po!NCJK)75d2uf&3c%~{yk%#yxgCD%}~+L;Me6P4R4Hk9{u^!1q?Y^_aH zR&pNi*+K0R5Ky_^#$+_r7OR_+LHXm-SiiaSHww^jBBP7WI%3gjW%>B{3{jYx$5ACr zn^K}OhVmN2kgYQ{wY5o|K&>&W0iuX=i|3GXer>~-eqle>O)%S{CDW3Lsz%a{95eYd z$(nYpNzk0bd1PU7qCtNRH8MF#Rw!?B^}2QS&y97C8&-r#H4A#(yt#{qM`38&i_p+d z4GkJy-=0Ob1_cD{`1$iEtt|bzZ6-gV< z%*<#wyDP#)HQ(%URL`k;#NvmEnu~OS$q$r}Gh0JR4zoG0u9t8mq|jP@NOb-B zhud2EKJ#-6-7s3SX3f2a4;#vun3?xduT5*-IJcmfrZ*HY8P!e6`)%W)Y-!tzD|3~x z^jkW~!u?WXM$88q^YHNW)WtC^&UAB-bWro=jH=F^Cr=pjmlj>#XJ=n7e!#*kX1(j( zyLU;>6ITalt({0!xYm@i9AN4Lf;J_zL{y@_zTO0Jqvw2kH6tbYBD21@Sl;+~T-?6< zO#FW1MXBTe%ls*@+^zJdYL@Z%aTH2@Ts8b_#8u|oBWF#WL|-n-BhkgryiX6BzXDe0!Wm;&$p{^=pO3T&F`oCk*x7 zav@{oWoGhNkiG)NkSDW)eiHZDID#CmpJXlMT2bSwt zq`dE_PRT==Mdom~Nfy+zB)2*H!2Q}H8l8^L&L41}{HrrA~wTYLNOs_ec-2@NIw3~VBlY*?3u$Yh?u zcl+-NL!Gr@6`2|UT>{46x1q22jih!Qox<7JbGz4Wmt@w?xOT&vSvML-n8|ab+o06! ztdiTVoXK;eiy|4PkNNCEtwanQyjVy93rA{aS`aV*%yrkH-HjG_~z-+*njp)Re7L~1GWPM{lnt9gw7?XAKU<}a$lS= z7P{7WPQuPBXzQ@ z8`I60g7~y9tFhp)(o#PPJY-rHzFlYF?SIhgLPJC2IFvV)WY?$u0~dm0aVI{0zx4d@ zF~mgQK%!~)qur-M;@e1Rw7z`t85Y30Gd{EJ1G<~D8P~M>+w$0erkr?(z69)iD(NH` z8X8J+#5=82g7lbW+$EJl55Ib!D{NMOv8VD?QgU(;;Dc>gev~mFHK#I@t;n?U@1Gy< zZrJ!j>?!KWYHIrDU5@cL;wP1qih&$xWK}5jnuh=yahc{=DEVLmB{oruW&U%f2LtmR zga)pewCQXtLV6riK>N7fx><=WJt3E`N%XPc*xz$&Aa|K&KY zn~0FV1Cgf2JNJ&~<< zaB@-rfhW-bg-g4ogzDG(15Cg2SXw0M2-i}T?=~k{m$+9e-&IVF_lf;=85?J`E*(Qd z58uB(JU`^IRP)C8%9YDVYL3GlG~)LCF($Jc%P(ICxC!`Py9y<>1Ss*0#*Ad+`|CRu zgZS>D+2H;D{P~p9#fzM1_k8}_LWry0JT51<kC zASmb#yM#UES=DfF&zX?I;$r#}e%roUTU=4q_qu<-f3^*f$_K4aUzC2H^ZB{X&{y{2 zuG0sp;WANVhjZWFzT78^<&i7HUW_yuhtp61TZx;`y6WLvrz!Jx8BP!Xd~e&kNzY@! zZhWX+|G6|GyT37+PG(_zv;4#DzEP)wyEN+uS~53o-HLX}7ol?*t*~Z+dtRIS_U+rJ z=f^61-|DMGtP=0C9}(Qmw}1c0!c_ZCU}mINKoYjIQHqR&Pe47r`|#oVt^9iLV!qwm z%>4_{`V;=1ENfw*EPC*co~p>F2QSe|FHRdAdf|H=1(3kT9mnqM4YL&GA9?{;{}PZ| zGsSS#FwM~p|Z)BKM&($pSwv9_Rr%F%%T?CbaZu9GA(oidC#wYxLxYQ zt7CVq05Q}ObXah>MBnnQSh0d2YMjE)RgouD67{5B40OGxXIw5ek%y{=8cQoJEgf?{ zZYys34mQ;Q(D9^#0?>9$o2~4#XU`flEja)Ns!&Ad#Tl{U&Isnb<1nZD!h}Zc+w*IliwP`vi>e*dT1y4kixUMtoQqkjHIFdw$+pg?f}occ_)xva+vynK9osycm{1!&0% zA`s)4G(~V$W4^==<&Axp7@nB;jCZ*H=gY=0SDGr>Lo^ z=|MU-aA0tNV#=%=3+d<>#Za9br-yfZ50_c*;xczL;rl7>g9kS-@#}p;pMvgHqq5G) z)X32AC7Mb!As!r6+RwE-D>cMn4^meldT>3#g+d_PUJ=S z+QRcbD)Qeu{QEZMHWfJbhF3kiMkqUz-lI>Oa_`S$IbV7T|iua9<{RKBD^ej&I=O_VD@ln!)5#hTWsyt zj*g9aDMlSdE2Pr9d|@R+ zZ533~l5~mg=FOY9?$zAf+=O7jNh-JYD0ktC^Vli>;Glwp3CIgBEDnzM9baDt&|g5C zG!)o^TeyVL&ayMkr)|pm9C-^&{u*C%K9b`)BZ#{=n_{S-dEr7=wk6uNWq5IYSakB# zGghtEyNsQ4hpLUvS2A*zeg|7K+-#Oes7jOCn2o&W-!4NN2FSnFwvLiye^01=bRM@4 zSRLih#1_0#4wpU~R|ocpl~vT@!Ryz%wn;i}AY~5kFoJSc*xD*pCWx8wdyhgjF z@bT>8wuIVShqhD5tbQ%Z#$nr@4Q;MtXa3dM#p#`VeB%Xic&md!{QB!ooH&tY-m*0| zHkM#NG(~rp7KS{4&aA^)jvD+f_GK2nx`ohY`EI1W1GWFru#?STs4Wlu+itAqY3rH= z@J9w<0nf&&Z3xKK@TVxt+!1>4j<-7Ln1Xl^1S>PEEzjD+UfZ->h&Ka{)L;RE>4ySX=;E$}G*RNl<2KIQ&Ug0n0GP#mk zwR&}_jTT7X4PaqowNl>*nK!FZUn*plLZ=i%Zr~m$e=G&t6Fq+bg3qO;ia5uec<-zO z-RPHp_4Mq0A#PV4)(tU<@kugzB^pi?Iq_c9{X|^ISKt>LT^X)Y4nPTifL4W5r^;-! zOifKUF)?xDX9tgze*;E4Nqq^Ea#@|tJN_ugeIc8WMYnI?zSf+kV62T~0LoXM`m(gp zzSMiIY+#{@!()C2MedWd1!t5`pWed3!9l|*Ge7L5mp|{W4Ao1A`U09@JVUp@UH8|o zUw_&?mW0jNQ&i?!}|fU{2@J{)x^SSdR) z?bMuRN-K*x`sQ(%{*aL{a~-ee z0+~?$0~Pax*aU_03WC>428Qx~X_W6m+Dhu6Wt$`lvsor`!aDS#yD6et)j!|#yGB`N zIpsR~m3w$(r0Pw>JFU%+_nfJ2D%p~fqvz=8_!6?!%;GW|y<9}ad%M1^Xv%2!?OQDT zqGr*Sy9&SQRC&)d0+WSco_kYh?WQ>#6A4ay-&xXkT zOeZKkUHGkf{CzODPhVa+F4qFY6c85X2ey!0y?6`gZvl|+RMSI;CzTHrKxEzmzPVZh zt}5=(oBbwDz7EKmzT^YY2+!!o>j+Fa{yqPumm8Uj-sAk$2cRPfqku+ldiwXnGg?xV z+}PrSwFVcTS$9H&>#2_Nu^)`Z2mV9``zFv1fOYWtuj6WQ>I_g*tnBUOvB1NGbe-=v zLYhHRK~bitcbdsZC88f*S;y1SYfDn{aamakH%ss0;-VIRfpI8*?f|q(v!r!R$Nc>>S~qeOu5b?(kkDc-?wF-`r7Z`zmxoj-YNe5fu<$^^K835 zdN78K&CL;xBfm(}COOxudjX2qw-DhqI3g!doro-htG*1W4C(6S_k0Qa{zCM_U_Q>g z&&eU85_sVf?~OZ&#+oEu5x#_Ah32<{WM5nyxRZ~#kS#C53QLwGkrkzP_5sA8B+5VGJ1K)BS#sL(;prJpmue3=UoavKZv0Sl6lkU$o@K2blsJDVy* zRzK)I??@u%z<~o`j&6cu!}8w=cb`>I(OSBk>rV~(9=>2x4AAmJS9=$cTYT`!U?4ZN9%gr6IsKTeBJ(q$6W(bZA|Z?YOP2 z5Y8>VgoFg?QPFG_qt9k+5RGFb~EzJ5nmtjsXiCSn}a zjlEhayI@5a`7SZk0cpf}u07w44PZkWWh+*fKN4AsS>W=mw$O~Mtdi0*;T1}HB{_<( z-@N&SP@$v72U;{Nkwsfc&4xDp6PlTmw4Q~@Y$sx#M$)bD*jRqmc-_0fx-exK`+nwZ zhao?b=#Z*&22p#INkXkn)aQUe?fK#bI~KVzLT(L+VAbgDkk4pex;zYfp1$L>RG7Ha z=q~#8+aI7|D?$iCyEp;WOTegf3%Dy~%Owarm(VSf&9@J&$+9tiT)xBO=ZEEMIi$Cp zf2(#QGIA&S&$F+NF28mAHc$Rij@z6F*Up_PVCM%KbLD)tPQzef_iJxR-{rGsH&CrE zo1(_10NB`3do`0T(%{8gI)yXlLkBBJCkR=vsHR4B&Pmc^ah|UXX!;YF)4R~NNG2jn ztDi5~=00csBipX*;`XS9bAm4~ay)$#VFH%svYFXF>NS#6HO{Ij^l-`_>nTwdzWsA? z@%`xNXnPmvJg>oB{_5}NqjpJ3>f~7J=!61fnRqPf;vUcd!h^8#nXp-qy$nw9b$k0D za3P8(PkMt%RRLubyU12*XkzaL6YU_41Z^9u^nB+Q+nU;|?56`Shr+ zXjSkTOivVd-?(w3I@40$wEo?*Np4NCg+R%;v8va~AwmkMHBkVt)o~j9MA4znLF~?g zQl~Oei(I~eN`V}3t)lyL_)XJlNSOOUtDcKf-{0GknFw4iOL;>=LdoQPdrBRJkBcI; zIz-5ni^yFd*A--CJt==sSbhB-z16#8HIpMK|I456D`2g53kj(q3+)Bube0);d-l~$ z96AMTeH2v4QNVA7rWB*)o=CN)aE5~hMhA0-?mv0*q&8k#193)HnN}9upCLk9KHT8K zg^=l)na;zE^6E8vkoVS5TJNo1c}qUeFRD~y4hdh5>k}-SZER`jt1|*xZc<9z!9|2O z>JQFEy+T7;gO5ybX(@o`dDQ^DgdF(;L#$30yu2wolHlnvJFDGLuZ`!>r+2HPEwhm~r-lo)O;O=`bUc8tL z8RgoodPxT`YVHD-UI|CXa|s^4EXxNc?TkH&z_&4Q?j`I%CZlCObLtKl*ln4w>> zq6`N!ZSDljAWp3!^{6j9k(S8|2b9;Gkz$lr>c#SJhD(I!{M*3s!B%bHt;hwP?c%mE zMFytsA507kc7hRo(w669){+2NG1?vO@wY#5{DPv|7L#Im#2q;SXl(r93^J8@h}Z8# zKkx*rc`q|F^VXd^im-1Sk2<%CBFdfKGcnzzJxZZ_5mwbmhHw@IAeEaW9zh7yeKq)Y zI1R7<_2JYn`yg56xNZ8^XM9(NHGw_t@~T6R1mczz0E43N zzL17riAtB2OQ~DTy4nNm?88E9Dc$~wzzGTp+5{FV^-K*vw1q#B52YW0?)r+tV^|d- zXFb++#;fGMuP-BYF4u8{uMg>uord$dsAau^$D*_`sAWMk$uq+r*H1`m=)X2id^Z@U zhyIZdnWHkl+hyW!6#GX&@p~uhr>=&KgCq6!_4Nh0sWm&^yAwJ{0N|}0z%^05Det(r zII^x}fdHt^p+8PePp^x;`^se;X-F&AFvHo*p!XN_7oVG(FG#r8J?Iv$nM8o< zWm!krFTs-W@4git?;-vt-Y1GK;p4fXgE`?2zdo%Ra1`R?l*M7)jRq$WB9l*gdOC9V zj~*Kdm&tb^bT#!?@wx zZTdF$ptT~0kcmH9^84Z8;Yj=nXV3a;YHF^eW_ltYj{7|h3fe>z=R_4*q z&!sk4T3Ti~jzlQ^%iIrc7@Z@>kfc2?%Pt^-tL;7f>*lx1xW8k#khc$&&LIc zBs~obe4V-k#GQ6S!51*;JgJ+t08;I|lnc2#%us!f?@4NBb$F7Rl?s>j3ih6YZVTUjr$ zh1?auvzXg#n#leFx-#lD%98kzF;qG;%R73m?2AK7i^|kVH1M!K=^3|xwq5QS$gi(g zT~m|ZtC$dVT<&()!tdVR*v%c;&J%9~_o^J7d)AD44dYv##nRun#AS}NMvXq!*^8i{ zzarPA_o6?phM8*M%-EPUt0nCQq{soL5H2nlUvQS$l9Q6ikM2EqP@7e+9C}#AGg?0_ zZ!f<=$d6?~0RNo;0f*iz>lJrXuc20p_lW^KoC2oe_Wa4eEafpQ1n92c=@;O7?{c`K z|1B#!HPj{-%dH&djhug2OsrS8MlOtA!Ll_wYC!@H3${S#?iUWWvl(A%sCIpig{a3t z&t2aA7@zrPCTQl*=&Ot4#%ZaB+XlT;04OnZhT7w!lI{F@^N0&>q=6{CFr;%+8SYe4 zB4{{a$EhJ%ru(x8ScFFafGm8<|A~M>2AkD<$_GmRKlAIrYV79*LcVT6814>y16SJo+-USuvD9%6Q-F?#W~O#Kg3KFGzi` z)V{Qq&v@#Vo*rGI#)V$e#u-9Kx}4uyI@L=9)J#>&dGgh*{$#IdeU zF*-#OG0R>fCe5uI2Y&5^qqeSPDO`Ql$Ld7iNvLmLLHl@h#2`*;PY7 z4k$33H!y;*nSiqTBL_*0$J)hUuBdF30koTzmOO;c?w ztPA>?7B56%W+Zpt=nU9-Vl~y2bjj%IqyLowq!Zq%My=5bw z8)c?J=Mg4dxLI>dEI=%M&j?NM*u3X-rG)zF(+?=+@OjrT0h-$G0^8ckWdk@d(M&?F z!yPw7V&V)#18Sb`5xvpbA68jrcF_q8qw|dPT>l{&PH+xF*$G@u>-p{yRSDIkcJbHm z7R!S3YT-<+`EJ@+3@z5*gyW{F(P>GtYwVgMr^m@)+hHr&IiFMI1befHTzmRGcol0y zqJA(awlXyHf3T^eu&mtk^?kB&r@RTr%~>Q2+fkFPp%wr;K@%(2uV)0~R?iXb;PH-^Pu$H#`hjOkx&&2!O!MfFp8 zd3i@-O1Q_%(_>{;=w(V-yEXU70Yk3n=>4I3r|U>xdqF{wD}=n|6cjq(U*~l>26Pt$%E}utfWl^_dnA0-0}nTXaKj*_|cXp8XHhhEHY9+pTq>K1I$u| zU|7oSb%^_%;Z4EfY#-$bHc+afBnJMS#f2&Twc8GFfOuQ^%;v9LONKe!a9TUBmMyn{ zz{WLO`3li{3mAUcKz@mkOVIBH4;{LWj?imnZQfQ$&u|@|xNzY@kWD?y=*RUkFM$pJ zYX}br48&b5bDbY4X-qTSj85g|)2GZYB=y}U7$^Iiys%{^bDjdy(;akF;jLAhiDdw( zdX1>XkHTPm_j~ZXpUp|qv0%F6SVcG$$cDu0xre8M8OTcDy}vfdb@=~p?bpuec&scf z(3k5BW2WP8U(vG>$5#zRcsoV){>D^*z}2PDgu49~($FB@widFHM>DnxAWz=f+B%`M z3EcYeW%%2JhmCdM?nT_+MQ;2F89XZ?5e`YwgBCvyFI%>(1B!Nqjg!+qIU9wHzqK-5 z3NjPSCdc97R!P1r=heyLtouag)V=#5VAFb|Ot*fEgQ>7BG#)^-x$i-B@?)NoOK^EL zXU&I4$GYI-K;_$JWMyX)dhDdKvgqOOUj}f}z6rObL%4xtJYK!#YHmxSzD%Cb6U8X@ z72S5Z+PFQi-dnM4g)m#(MY=M|DY(K%xEHySuLDv+JahAqi#KlBatoq9u?Z3F9iAMB zs)vcc2T-Z@yVyHowuV<41)Db#_M$wc_Op)fWYaH@0?pu zb?rcm|3;2|4^)`Y7dL`hLx?JqmW=pcVyS21jmm=PyDx~^N;Ty;@WZ1-NJ+1;1Z_=C zlZG|bxe4H-0)-D8Qoe6;Kwaz1eAxNyE0GMDEdAhDW2?>O)6>ONhUmh*D?^y&;}_ zC@EQi_dAA0trESl`9-NjRJ&WCwlCqw;^xno6zAS|UC;#;eQm0&?C4gg9gSG*{Dnzv z+rD}Rb#--O)POoT0v2!Q!Go1C)*Ho)nD1aMXVH#pZ{sTI;6 zQ2>&yejYvjOmI2O=Py)@{9wHRs4hYKU^P8_X@J(ai#an0#2|>b4LBW}U8WAeREdEh6ScKJfs32_ zLgM1gFp6W8JBR)LiDLqr9~CcC=tD~j zTdHw|JnCTjzE(QV;9w>gGhX%Z#NqHpP6_XZGeoEJ(3|{XX*p-~(?pQqZ>^}P=qa>5 zlHNW>OG|4t`bVL^IeiVP%SX^VBftii@X6ly?|(q?Bk7bnD=9k*ul#V;367%ApO?cJ zMyPG1r1*inLk<~ve4gE8!@Do;aELEkvBK=(Pc$N}#;}+$AG}D3Tx%p62o6^yC_-ZA4-qk6 zO~vTtW!rm{7YgTay9$e{cg13P4Z46T2!d5+WA9_k_{ ziL7jGYo$kw4=4hrH$YoRp%1rp)q^uAeJSw|sG?fKsqKPw#_3|EWwCs_OG^9xQ(Vql zw-EvIa2w<$5%DA)=HeV!kcjV;Y|l^BTe4Njmp;;8YfPd+@`nUnVDB1b%je(V!lybh z12&9SHc0>1)IbXpsTWYGeGk-p_&`g{RX7Z`VfI1|@6%s~g;Xfo4;3LHy#hNn-5IQ( zWY%z?zrBD1OZgG;S($BTcC96&eAjx_r_Pd3T9OkWB!D7$K9CQ1alh0(45V|0MeBWl zmo;$Y7Qv-OP8w1D;h+0JtU|Uul{&$e>+k{pOk_|Y*Rl%jEn9#oa3!$i>CnR)(aRpx zcC$Xz#()tt$RDY#ndse{mo)wx{P#`rX-=I!E%aV$$Bv?l*>Hvs{SqU7WFlfuY#GLu zmUZ-2%;nhjMx+ja#%NyuhDl3Xd#98XJBn-dTyKt;M^~73$dS-4?a}UMDfwVR#M&ec zHJA+kK2|OGS|<9gA{8Z_@Gcy-7g>%3%pCsH_ zA(tA-(#X1+m#>3?AMQU^fu=N;w0h5Q17MawQ+G-r>#R#tOv|4eu-6$rVJN^YJJG0Akx7v%<{=I__?{dS^*2fJ!$H3!TbQMuo&#-zF^^B*YBOziM zn9O6BFGuKc^6cKdJX$%No{IF)dQz9X`p|zZ!30)Pl*i%>H)7LAH^=@GdetrH44HNC~Z*(ogqobwv9_!byC(;%IgpAS6zRLLVRwFTN(k=MWqnpqTS)uiO z1{1k~l{Mt|M;hq|ljlNzhuWM=@6@V7La81reo8OHnDDP+U~rD4lM2j9G)bM=Qy z1bHH!S(+dd(tG)X0}yA4c@Zz$r9vB$Ffj?a2W+D2JT1aY%Y@N;?I>$F zxjAhK33CUR{Q9RtZj#aAsaK!=zF@uo+>TJLs7e!rqntP2ae2fT0Hp*$%F!(r(US4~=;lScN>{3G>?~_Ncq#w0griE?(!= zAh0829ymbYef--v_lnM(S$s z7p8=V>t`fBECv&|gX=bL68a`Hz+(f1zLxYd<9F{8fOYAE6as)8KGxK12DkMoI^6wj zs`-Cy!$qzr5yzeh8YP)jagi2{zB^h1opjI)LzQHC1|ZIhHM-ZXy(&k4=p3jccFETq_ShK3#{Mt3EzWV*C`!~hJ4ndqqI5gA%7i>CH?tDaS86wCo%sCG( z4QTL>ek;>KfqsDc()T)EFOL<;d>=5^_*O;?=ZeGMU8TIVo15DJjUhc#G`L`WO43D< zg(3nr!i5VCV>Gz+a~lAR5+>QSI<&&I!Ivv>ibF3aL%%9^Lbu^~lZ`s}6Eh@*=zi>G zJs_AD`?0U-=v7|9Q2sfl7GRu#eh`Vl7ey4NR~CKOk=t$dS4ywmDY~JhzU)G;&B}_Q%_v4UB2zJLLN9f_} zw4N5vuY7K3U_vtUybxcERA$>&04V+eohXf@u2APp^rXHpnn&2s1ToB3i8085G~S9+ zaQUMbdI;XYbV(i1IU*uD>4B-xX)$Th^SZFxzfARR0vJgP-`_L?kd=gy#TRL0Py>wW zP1qBhrv~Fw%|Q>M!;oKLx@q&~WW%qUypHTS*3cu`=12z=SPVLbAe1~OzeJs^h}19U z7DLIW&!i?b!*n;1KP(Mh`Hj+x)fCLTHBA|gBbU8Yl|Oy@WYQvysqUe#>g7*ufxE=@ zt@N|3mt(OhYQRGnF2y+A)6-5wkSD4%sqlo|Vp75!BXQ(h9j19Op8-gR@6TKIhhX%Z zeT;>5sQB~ePW|1fBVge^R#$K0aG%}*e{fvp0+veL=DG3})M1Tr8${LP z=+ARx^kA&JqAx03(rMGZd-njnzF6zccc)`w3swH}!rYVqNV*q&6NuAvpLZwZ<-4*I z_~}RqfPoHEl?TwITpC`5;e!PxB=zb^8^Yt<*}#*AVI^>g5Rzc9_@~%H38TIsy)W~e zo}K;M2_X1H(5Q4J#RIn<9PbxUCb)r(CSUsL~wgUq>e0Y=8l=H~mU*HH5| zZQZ&D9LE+Q8S>6}yVX>d>r9GLJ}8mvwzlxffqUp}&B?5d(@@4)0^X^*xWKQUAKK|O z(NlFSXFs%WoI)-v{K2>TF}$|S69iAf^ny3JP$hv)4NZ`06eBdG`@@@Ry$)nA#Pl!ks4{rMxBEjp|>tBI1QX1 z>FZ2!hy>rh`{((K#VN^XF@v8F)NHW7|Av1u`35xWVpbipK}OJPS5lCN9|6yv!uj}w z2|Vhgnp$9yWXL2MgbNrM=wE4GI{22oUC*XCR3HDG+5j71FMMRQP- z_JL|hLV&J^JLvwjh88r0r{H+l^&ba8%);r1K0ccRe&~Yay4hbWFP2w;SEyJol*Jy< z(haxHw&)z!x)O7si?xQ?WuEDov+IG;YxA47Y%#Rel{*ev=09EnsIjkcRpb&db0Zi4oJ$BNO8FxVeTIz#of3wr!=#A$HZ6;Ui1{vN1sd{H4!3ec>qbGS;T#Q zI$0?U&A>e{=%Bw&5&aZ6<~Gk+c^?G1&sJAW`6gn&+b8wKG{IgWlxnZM)mKE62Y6L5@D(p}29TijR=! z@BoMXnAatm5XlUa`Md zh7;r8=pYZ0eaHO#*AC%66rCDB55NRYl3lQxia-wFp3hKU&??!D?#&uXfb2QxTn~{A zqY}Xw;5Ki^(&kom+oMw+86Bxb}B+$u>D)B0Q1kMsw$Pg z4T=0{W8D1cav`9Uk_slp7_8B<4 zKQ%QGXT@NnD?t4DwvVwute1e06i8BZ8KSN|oVw9+zVSDtvFva@|$iUYi zMt}A8@*r-B){d-6(-o$Y;Vn z9R=@cHLP>@{QQD&Grj$ZfA-kG<{^l>OQIl>@|@FS>aNOl2L%T1 zLTz}AK7tr?;J#9XNLHyio=#8MyOiioUhJJ5U{hG>32KR;NlxzGa(LY(JonL&Za;|k#J zQQJrs6W%iPnx)|FI?=?4&vkWeFWYv*Xk!3)q*K7{BzM76^BJFo2UqL@%&vF0kQ#b! znI8a6MFEqiuaQnJ!T00$pIr6f+%}4=ncn5QU4UtJ%4U( z)x{i5N}fBfROS!ALwH2Y9&{3$VelZ5GsuWj*bXCvto2ZvRNeyY4%|CW_ywW~@X5=| zBin_prZc@k?=d>x3;Yis%?)DySqpBvJ!s?~;SI>)2ebj_r~3yS#CccZt9pb%Lj}AN zjy`!3v9Rr?xXxJTy-fKM$D&zJ1|VUxl_RAV!1&kCpF1!m8we7BoR+k?PLK$*YFu2L zT!N99$|0k{E|dKN&KK6;nKKR!4rB}wI>vpCdDzgnxJvmkJ9H9WQpCD%*whpVKk{S| zu$w05So;c(W5Pwnjm+)~B5yk&zJ`GR8 zzCLPRXG-2YVjgmt-|+D8%>??58&wiU?irCE|L5!6Et`J>av;?46~HAod#@AxDw#UM z3BC^NNj^iLjp0HTES@ui#_36i?QUDkkSvm*!`c|z<*VKo-?E? z1D^s>Lbx*W#CklqK;y1E6JzW`U>qn$e@b58? zF@VfM9z;T({sOM(Nw>}aST!y#-qrW})tJf5wf98~@`(3>W4#TZasJ$3&bG43WlwBi z?7E)!Qjmgx_Y&vhFhqZqPE5A^_bYGhJ3R6%i*fU2I(P<%&62wnZ8$Qk_q}@pF6>0@ zgtrjZU|!`;_yE}p=114yxUa`?+Fa*HRu%rG2jmv;FL)o`p1--{hZ9IV2|oOAIAMRv zjn2M?L<|Vg!Q$rJf_bj%!yQNPJfNcD;^J#|(~z{S@CitGpL-->)AndUz`LS_%#1f-^v(kMuNqEP;74jDFtq@mT`| zuqIo4x!=IZ+LohCEytKzezRoUK9F(uKt{Fqu|RNUy+N=*|=iGiXZuIEgczn zR?2{M7l|@><5(~tK_2`h;&1){rTZm9Sll$kkIdUaO#4z>Cwu`M2s#K#R5Vj#vHS(o z|G35~p+>(qMfO4r8IgkU|51geETi|{yfCYcxd94=$;#ZCd;fk0z_`0PIXPM$3z}+4 zQel_0doH`+AIXmym7gc^kK>+9bCsvX>2e}T{hgh+0YdPwqVq#vJ5j5Ou!nr?o`i#k}j$$*;mA8#dR$QtB%UYG@Y%rQnjyXFl8 zGGVfD#dc)NkokX;_pt8a>$YwH1K-`-w~ye#PbiGsZXhM`2mpD)@0wz2_c(G$n4be! zS2EiMfIzecWa!}N(f_6g{O|r-E(u0*S`S{W-TN(Eh66Ww9E}hj*m4(7SK-1)IpAuu z8^F6S-P{jO>iQbPOd{oetbOEU>Fa+Az};iWg*ZXMx>Xo@d@Q9)+1Br11-^yMc`Sd zWMM=UNkZ;0A_^{b5KsQ{GQtGfvm5^_w>t0M(p#7UO$I*ojVvtB2HatpqbHByF{`}} zkUkM+zvo{<;jIpnq%Ie%_-?WM+#+57eVX&)G&kI6TrzJaurmrxt}k6ZN6+%XeMFJB z8gFm&^KZ=SqI_9al&_n&Z!e#t9m$qG&VScn-wplt<+VK7y{qPK=pOP^3a7K1Wjfs& z)4@_B^Y$(y{&X?f(tpSNX(yxqJadOU2~aF!Q{%jPeM=orMaCi7@xKBo9-4+RG~s?T z8vMMD{cMKbk1Dp%2kVQADIfpu6B+sMa<31QK7Zvw{O=pwOCjmvYG3yDa0C^cxyd^r zb4bH`H3I_b_=s^S-B|tIQFc1=TsnLu-fyg%l{x=iadxw2=zLM`#H$Z-N0!dUh8HgO z%i}j!uO~12X}ntLoq(m=-uCOfL=`}qp^bQ!Da8%5gIbzL&{$#kLS4)}>(=f}8pdN= zI(O83naZcxJIIl-Du;TY8hH0~WRku_qv&d-{7Ht}3$!EJp5n&>J6|33#nSD5U&>>e z#J{1?T~;}nV}pvyqln?|Ud5^Ch%-O?%mXGjTz>QR?2eE5NA3(C>U>gCC>W6Ze|g1r zqq*@O8zJRDmK=|u{`=1yJOrP)4zYRuYLKWYW_g>-@x=Po|N4CQH@TxA%>x94;zF+o zJNRFDQe&`hEK@IVsgY^rMVXh;jqAN*_P(uFs2Y6sMEq$im)c&QevVl8_;R(4yta*^ zUpC+l&PzOx-mZ3$S#wY6Dvj@Z7!n*$QFAmj)cvNFb2J+n&mdCvN&KEn&p2@f**AVY z~4~>B}oRVMxA_fu(VK{pmW z|K&A3K|F7!^VUA&$MhPxyFC{?Dax$k0wRxKfU126$%s&^&TG{j_cNE_UZ{=&Ax7*e z`O2&Q7dY>&MNF+eYtuHa@8OxG<0?H-{GCoYar-1g>AJZUMz`I(PTxRW8(e>)T76NU z6Q2-<75;Z>G`8U2DO&wM{KW=b^uLq!?>}+~tpEFWyvu+;1Y9Psfr@4XpM$Z;BZh{% zfHw6}Lz?{9jNw zjOO4<&Jc`x8@Fv+i!J~XAPv=35&jS&^>5^Opo2THN*zE58VTDlO6CR7&Iv}SMG(V<-jM(3C@5uqaA6r`4S_k1MlnE! zbbTC>7q5_A%IfW$%*Eopl^v-)vfgp2HX}m#LTHzZNPGbW1#Hmv9yziAmF)pTzJtk^ zZ%kyMv#cQ28)s)d(q17QOL!C;s=R1TI(~GQQh0;$<(7 z9jO81n?ZBp&M&NScX0`*&6Up`Z&y}UCcIb=t>#csq#vpR;YK1V6b)#_ul*(dg)My< z6ck2A=bO%$k4wny`P(mpv~5tD_xR5*gj{edNNFnYeOG3fb}qmjJ<$Cqu37nxp+=Cn z;KqYF*O1cGH#8hQdl!uk&)ZdILPP90yv=j=pL5UCV6tYY;TGP0B&Id>nNgQ0b8C)Q z%(StFwZVIoT7Y0 zH1qY)>b!zRPrnKf%d90!?sXhhc%VEB@{p%sqe9j4W|*ghzQr%EP;XmlsbEDD`Judu zxbSn`qUhH|Qti3JR@Acp!&M-RlT8*%u=V7#})R zFGDMdxCL7nQ2TlVK7uEwA4xcU6?>69-xzj)2bKv6&LOrdS;~XCDaZfVl;a_``P|d< zlzKnGGGw7pq5e~Oc{V;i!*BlY&}kTR=B=pDBj`053;Lh8^ILF&gZWRY0vZXF;ya#KP%Q0aaS4KIW_Fr;AvEsWH@z&h7y{si=E41=VD zY3L*dnvR~leY*sp?VAki)L&mZ{`ib5Kqca&_&_o&Sbh-zY0IS9X= zm)T!Jw&LFwnDVQ+xx@S-cGAu0mF0=MaQv@@_I;tb40Ikh4=R&gIv5s{5;AeXLH-oF zJGA&)Jkb9Lw{2R2FJ4^V5^j;jtf~J33Gg($e=l3Cz|3$gxpBkepbCVp@e1(b= ztEEnY_Qh2=^1(Lu`!yUoB~uT&YF&Y(3ekG10Yp%n5BnGu$4PPY z4dItC6XQisMRSVPkGBJ;7nFC1gtgP0;Nb-laz8f1D`-7{4r_r&ox;KbA!(B7TVysX z)~}znJU}i7#q}*u&nb)~qn7DdMW12IH=aRW%9-eC$}nm!+=UXC)Ssm3XZm8$c?8+t zx`DRYW04R4WM2R{Vk)`i{bQWAnRgxwhT0MiRgqwY0Y6%y3r%2iH6djs!N6)M->GgA z_V}H8h>!Pz1UqnEdu-3pKkb!oP3KcNa%4He5q4>#XsY9dA@1+kt*lQik)7 zo8fWrmd{Zp^f|VHSOD7n@oqs={@;I%-Hrc?J7K>Qz7=GS`dyj^GWm*=(=(`04*F{B zC4;>{s8;}t*gbqGh@ZI(Yu=TsR(X9bqes!Y>gTabeGBB2LYh3t`RQ;0g7Y3A{2sXV ziuZu~oevP7T*_f+aJfiyXewc>IB>}R{mPZ(L$36W>a!Ims*R&ZAD~nV`;hfOZ+kyf z&Vhut^u7lyT%cKcOm=UaoCYq#y73FJP($THrh^3;$9gTK=#CR4(qOE}FC3ON7m!Bx zc=OTfPG|e_jCthCj8lQ*=H6dGTa=405i;fi>GHYO?5!7{E#N-5PmhNLLtpyvQy9%K z>RVeqhg|6h3{V<>Z>((M(FBYSIS09g8<%v#4<5>Gbep&~fy~MzY8ovyqO^f4C!NVf<1KxBe!JODP_PCOwo;!;u$M3YOOV z6Xy6c?RF|j;BwLjWlA?kW*me)o9D4j@)b3CP`M@|E3RZ12r?H+rb$!h*#Wm7&p@bg zpwthgAzgssgC1mSWFB~_8r0!g;BSixxSVh%b)k*w%9Q_r&@M*~yhJy6>W_O+h}sGG z!RDm3t5%7nI&*i!AdHqCJzOvlfwj?~;af{XWym^aT5B*Ca*DB<3?6gs)s-@%;1Q;yiY^IP=|8RJ=0^NgK1P z1s&PxhA8FGsJ#OZX2P~f?vSSbC=5&z`qqBN+KA=H9YfObKOQ$eJK%cCpy?OsmZwT^u)#)W zY#}p3o-mJ79!=VUsGir^ezT%oM*Rt@s*9Rm`dkB1kAV|<1Nsn0L8P^8d2;^SU`Y=0 zY$G`xSn~@IVPaK8YdAy7v;t`$eBYmEy299TdBF0k-z&J?ONE3Mfav-H{xBB)EihKn zM3n$J{zZGSmpe9Y+?)xtlpKM8FlgaUP}d0_8}`ZXq#6UKaATX)_t5aOia>)CqSW4F z$BrcoL1Wy=y@LTppBi*Lw;7!<2$!IE_>4J2f`STOzx{Z^WB)bl`}6)^{}->^+?WT~6!i{a5lq-*ERLVh?9PhI%-n&jnXM@qP1|7+yT<;6{&_;XB&!=`{3|pYW)DK= zKnA`j=APaz|07jjF1SvI5)wY8L2N`LA7&jkcA?!KV!M>az$)f%IL>t~ianhNg7zG` zfe71P9*}5_w%I`$qI|x;HjH~X$^8r*JfR|>{tCv_Np1=}3Q=QK25Laev7in-bX|{! z&WavzU|^tl&$TI}fW(_OK*dc4yzu^jvFv4}-cPc#S1GkHvq$E)lP~k!6eCP-w3QDh zBRY$r2zb}re4?Z~7i~y5O=wIBJ@@ZRl%$fNi=nHtxj$poAV3W#*rNs@V0v!WUx2S2 zi@-~!bF^UL?emM9#PO1zKP7EWg$tAsO16;cixphmRtpS)E%)4VhLeh7i1c(y3(%kk zCV}j-(Ez$d+y)F5+GHTRos71};z{27o#u^$RQSP>couS{MR)r1BIa^s-rLZa^&Ri6 z!G46oAmwmAa=-s{@T7eQj6E*;r~0H5)A-EbjSYBgqh#fOdHf^+fP!BD zC=0`XKYqp@E6=k+A@&?J^j|=9KZ?o@^G-G*kvth^aV1(Kb2_QYtE$@ddoYNBe9GM2 zC!xx6ZPdW=3eSJ)?*^TU---8$xD{<@Z`5zlIoIKt!#%-6AiihLV|@%3I12q82^ka=bTMi?1?X3j&mUZ9qDe9!*mu)xCzN`acjNWo z^uLrldG|0?14*wR@D$~TC$5`jVgQLB&Mi+N6CMa(uOq58Du4RPHDbj+`~2=H(9`i| zW&?`~)_Gt80;&tVdDp&8Gk$W6f7ClI0vhZLabfH!m6`s6JqeO!ACgZ$?=)&Y!Q0f+~y!-gSK_t z`r)kWp7{~r0erP}nVB$yS5tR0j9>}Ukoi6U$q0|a|6z9nG- z(CK*g4*#)FS$Gj7L973YB#!zqA#>UX-2{3F!yY=rz7z?Qp@Pl-Oge7<&nM!csP7=e zZJ$w>(yoSAyfFp?{F+CdMKzY}u4YK}M#)XeUbNN0+(4(w)Rpv$D8@jBGD3)bWV>$8sWvzqS4uUA@UQpz0^DySus!o#Tl96Yqus!de1daiS* zLHVpBZ$Ce&za8IF-Dh!HsYG+hGC4^JnWLqT_kP%B=Aj{9ESZh%1?b4z?E9lztami3 zKHbFjWU`2uPgw0shDX<%sM~Y;XKjeTcSG%|lg9g-;d6bAZDe){OnAs&2Nf;7@`pXV zZ-m`FG(Y}R*^7|R8ipxedcB64sj`RO?9VHEoum4AXkk(7M7K>FHRK;$Pj)HVv|_`w zA87!~ExdF2(62U%vL%ACAKxelRj9c*mW&7XkV9BhplF@Sf_AF!9AVnX1 zz)_6$A-`3q^~lH0i1S9?F+QHn_6kpJzMPfxJ|xt#({f_v)~q;fx!%FAGcz@j;sp$A z9gRL~JGrN6uAtT8cq%>E;*2|ME=?8S?$j-I zzpSA_7jk5w(L}i|J|#oXtKLZtXmRwfDzlB^@w+2q^E_(CsSIg|V-gd_`8tj-(4QdT z6}wqrMgnis@7BK`2;;Jr*>zh5VXFvOA^ObMwJf`GKtgb`?4e!9?eM(|eKn3f_7s?x zl=q}ySI_a??md2ok33CjTN?Fdcy>V;y?I>dn~nD{h9vq>rB+ctTs(>uC;NnjxA6OZc-e!7<{zoOjGnaPsVQtE2_J z5xJi1cQL(e@9!F`(}He4sM0TA=iTEq`_^H0qnOQKIcCeKMFe_^+%puMRPAQ?M0EH4 zX97H}3YxFO)=|dN*m#VTM^H-uySW5`*h^91QF8YWtUtNw$|J)gon^(CfVv@CCjS_?%t3>9Mr%c_wuO{mBz;O+( zBe$*mgW}?{8)la$U)(&)%k4z?+C8N`45>>qWz>@EF3Fx&%Kvp}ikD;aHI^4_j+O5? z*yZKDHpN)M9Q^>=B!Dg|!&vIE^*rVKyH|ByeKO+uLT(0+vg*|lx1;aogqn{+S1SLA zYb4uoPw?fe_!TF@r_FjL=ig^dPu)hylwELc^n}xkiXYi5el(5CUR_msV}2_}~p2-;lT6Mm#Oq30S4PdjowP7K9CSJ8FMBk}`u4T?yx z<=qv-+of_xC8w+AjM{8FYHU&J(Ep^K{Zn)N5qU;j2P>|v+z^r}W8c&zA<=8*`-JSP zz5FKbV)S6a-nQ`C&4;4CI7ls@BO}Vyta*Lei+ru_(NPn&+(q~Obt>?m?8XYAH|dhv z)N&X02}j0mdG~9-cWN#kTd+3bFjzw}(gY+|8>-myocPZKD1r?^4Yrxl(j3|saFXjbpG+L@N zw=I7wqDvJ@xX#-tjv(#i&T5jf(4F1pzrp~+`F3N$lJ+nwtN#d zluR>7e3+0GlCEnwJ;>utPsFYs*2_wlVl`G@nUZ8Sx4Li5% zb*B7rpAFZa$UZUaf<*9pk6#72S4Egcj1F`Drj+!Cca{ui(<8+x=_!9!F5#4ofQ=>~ zM(W~0u3+RogwX(=4;SoBc6BezUZ8#si{`WGCrxF=Qx}`9_jR#e_V8|&yomE*8P1f< zJ^L1v;1m3pjNUHV6gcc6x1`w0zqj9UM|Z03*l}Wi15pSTZ7s%Kez`fInkzNkywEknb3v_;QL4Tove_f6NY7 zzM=6VMlfw#xq9_n<4vJ6>rI$v)RnwvPz68l)3#o&oge;(JnEL0r`3%nuyl)2YCC>& zgTrFQIHt06?3w3lDBkDe!>^?VQLP-1pY#3b#=kajxK48SSgUd(|2^{p)#nop^-l%j z^PY%uU8+vV1{_VKu!6%lDjrfCQorlAL$~Nn)R(WGHYQn>`AGenL{uW7BaLcO5>rV4 zscJ41VOkhl#Ahnejf#WJlu2`jyW#+i?#@WX2Ke=;hBEmkr|*QH%((5=UpIo*aR8X1 z4OMYfFWgGCz+WSOp9UiIp!=xp=+F`ZD9}mghmPUqZQS%_EyKL5#<<<(vL^u7%$x-u zb;UK-7@C72m|1WZJsCGlLREOAQS%%*@h&GJ@11se`bw?rkAFaK|3Llo0Z|*Gc1-eH zAWK~2{`GLFoxMte++l$4ZLYmKR-jz_N$mck`g3*BAhK6h21sP`)fXMSpF(m33Uv4h^kblq2v`sJ}(cOtcPpz{m zva+*3zRkR+|LqA6E(6c|;+3qbsuaHnx%xjV6MpI{10Ssg5ZUdonE!otuzVJv-Lse* zdZ`+{K#w3)$OQb%@B^<>(@1Qe?32^9Zb@HkKa^qfAXbpD@T8 z@v|yR@7|KesBWezL|1Y7)qv$OMKeP}2uUnj;qL`l}&qABtB za*dL?_UmV!anI^TVNb)>w4nNj#shqQCvpNHztu1pp(SfeV;HPi!{;xL7n7 zg8upi32DXs?{Fj;79F;EG2T|-rSq`W5wJf!#xy@*Z*TX*b;q?(SMLWFUJTb%Z{X16 z7ln2*3PD)>lOV{>!Ag$BJRBG$Od*s65>G0kVab(sTY(8$@oa$C=l@*m5flUJfD{y4 zO+R?jjNJ(+1ZnUcT0c(yK-FEu?U&AdQ&me699AsBuQmJly z)pxv%XExBY3s@6c--ZWCErhD>_wQi^%obpuijUQ3fk5bNALfx3w_y-MMEo_zD`?VG zSj^F}`SNZu-8GyTgmCqA){wp@^wb8J>;VIWQ;mCoFufayJ9Ij8kHLNNLAaWy80(Az zb5p|U%JINkh@FE422ia*hszS(Q0|bR=NSaevtW_}0W$zNT>W-EHQ z_UP({Z=3NajB&6KMaQ@alW3*~K|<&qHh8&*g3Z`VMs$6f{5*lL;22||_aRX}47`AT zExkN_u_Fu^pvUp-DaMCHF9Ko)|EuMg!-|7oqS|5+{0nDTF4|g8mRZFfn^)a6A98r$ zOi$1}uBdoo@oOd>`HHpBatjL4i)aQ#huk=tx`Cy|983#y09?oyguxdU=CqkEqlp=L zz$FCO

tb!5EhW%7$s`>tp*V6f1C-ytyHDbqcW)h^UxgwF8@Bq8bxY4^WcG5U!$u z$CRYv31;IYp|0cyYN>Of=UU98_}~ga8%?17T!-V1*~mMP;v~>Qwh}ZpghLk7h2T2{ zy8`5ia`H2>O4sNMZrA`J%3%Huox2>Tb5HL51y*zW5zsI?;3_(E*^J%j!Cg_GKWMXs zoE(WdBh&At%SjwT7D?Da+CX(7o073n!eR=|lRS9PULtsdxU^SjsCh<_(c@FhKr-xh z#`Tp5X17A#^$ZBZ+4eGS+|2h7HRr-mkX+P}?7oYL&UgZ#J{DnUI2>k5uOvyOgED?9 zPbu$yFz`>F1W3Q+WkNZ<8wAg5TEO;(=5Emt5peDD(dvsqwza;^+<`G$@k7>_=Sc&f z0PDV>`xxLc`%->yxb|Bj0G8+5A`T7>4wAn((6~JqyS5IA>>oZfvRQI<%QhlJ((Xb4&xxO!L^O_}xTV?#@e z1I;b~_-_y63{ZUbXo%6#a0RELjmAB)J44I)%STH=DS2+xnT>gM(-1Q$Lt>CQ3>jNt z#0*RU#bAzf7SOctJzi$W@*<=IaVQ5{f(j+-cA~itpf=Y*^NLo$*)XYiO7mDcn0$j- zoyBC_E{gTIfuv7O>#x!SqPbD$8)h^iFir#ku^%Ci=yTG?qFVuqxn$AFSpSyoKrcuJ zg{V#ejBFLji7UQ35x)T_J*>dGw*}b1QL)WP1Uix*jrH@^I_IJ@7-Dt;x{TBDR3d4n2nWRkRO;2h!=wqKMDm&B4`z-I?SP);J>KUTq61O*$Sub+ zXxI<}z&y}HN#`0S(KtA4N&PyIL!<|?9qyV8ruP!y3xC{BS}kb4a6nX_gLd{Q)2F{w z$fBJHWbL=YPz-ySJ#*$#__jw%V-NP}bke-jVTq7V%HOeb7IhJC)792&-DzxE7sIy$g+WQ zzd|b^V<-0cjpKUcUXV9TMSl zngRL6FI|qwY!~{IK=^@(zF)Wg-kB9UM@J!>X4hvY$Q6g0mUCgNnry5gid=pdvruj(9Ob0}|U`M_z+t+Cp>yW#&W>~OhubIybAo#)riI_In_@ONFN4a3{7{Aw>^36SQdOVhJ?KCG%rH# zW`9lB@?5CV}VBVB7 ztdje$E)2$0j(=qsAF8RTihsi~Ii3*^7^np;J`rz#zjGlzS_kQcYO3kM?H-zkqW-FU z5i>ksu_9a^nh$snBFkfaFkN86AG{v+F$~SvCt_H-9?$ZCkpb=GJ4-+HwA~r47$$2r z$|Tp*tQb+)Y}#X_E*3fI053JolQT6PwT_Lt^9ackJQUc0mX{f9M_1WntIPRg+M`c( z)8dcle-3NH>GlqZItGcP^;==Y_Y1vm@3&=qdCQ^W+g=A9>f=~tc>7?=N;jN^KEi19 z^xF?r)m2psP>pD+sTU&jsqtYx56rLtBINZa3AnFVJ}Fnme$bj6^Y_Z%2AYZ)*5>rs zAh&o-j$Sh%#%?PTXaR@;4+Bk|;$H}b+CcAOve%*I?vLRoKw}h9|iw}3w zZEe4FzK_#$(2LSbjp{s?SzDv~sc2|qFikW%eW0tdVCbtA%ImnbeA@*@dty0o24ePG zs%}NpTJaR5wdD!=D(UdjT>x8XEZ)0%H7G$!ahnz!NJ>b^VsS?eo#luq567L&o9BAG zZ&vpW;gm@h#?!i67u4O{A5qQ&vpFAZx!@s_rf(Zq17u%Itvx<>l7NW%i3=MZ>Z1f} zhlIixG0Egpof>TWUO|@FTfApR*xswNg>1j2JsmkVcZE^aK$C`sP;>X9V;?i2jE7T1 z*}^kv{!ng>VANg0a--&O?foq_$tVTk)vwr&K!`EyVMxT=;jJkH;-w7g$%Al#hZguX z0{sJ=t>FkTRq5vwMF$(_r1zFc^1wpI-6(C?il2jxEoeWd#a=0qzT2eam@_93>sl8L z=9V8~bLAmjo;GjYPBhDAk~1=Ffp*g=k!JA_Dan1vBdVGPx{cC%tso%wM?#DvK#gVE zGzGYC=nrUmIHzOU)<)=d=qtl&M;e!Bj9MX5HCYzUQ%V4K7Wq1_-=`Q+{?1xnR@UX| zdInxmPAaSgaHGMvvxmKJjymgrRZ7rUetkU;Ms8q`R4{^Vlipjf$3iy4Nl*3RB zHE#(U+e&fWy0qlTQ138MurLzw0kGf^@}wI_DEq>NdKC$nNe!pp11Qy4SXm29mVDHX zKXN=)nQg&>_E`HlXLfR`T^f6=XYv~0ZE`GDxv`_8qhut-(AF#aWuaSwN1QU>;rB%! zTq|G0pyY+mb|rzd&ljO3eSrL558gL-ge~GCodZXXMU8){^VY-Gy3Mn|=w|Y$%9(Tp z;74V6>R?^DYGI#8?l}hOeTsu$z8u@8ZkmkbHwLJ#NQ*RMRQ zSMS69Dv^^C3C3ywlFS7&2befYpFJBx$EbHhQ%gf0jC3M_O}1u^jzlBzj6`!#yg_Z% z<`b|7`C4TAEs#UqoK((o80iDKRW#bd1te_*c2r0f4aDYhmLk830@T=nQv>LMZ)Ys0 zhjXNSm<#&Hw~KuH(g0152ae%jsD#6D-i2eJKthY_VA7oZ12V$9C*bLwopJ4X(Nk8R zUc6O1&YRc3RaU7fCzFqc8@Jr*w?j_XucN+*SQI5}fXNYLRYx{s{PQ8o|I`tc`mqG! zr6~CCz3EfKm=@%6K_`7J_TdC5s-Sx$v!&M96=tP=Q1dkEEr~+A$Ngu|_QI>?cxuVQ zT zIaYR`o0S~1U-ZK*=WBWPBHdcfR;rDs`Gvbg3-mLu(+CN~#8PNRepMawt(N11xt;bJ(0HZ1?FU-zIweju_nNQ)ABbZH4`^VK`SZhomUlqCEe!$8M(t-) z4ti}^B~`S@_{1;@|7cuTE2QNW_*u6*cfyh4>%uH1fus2QTkkwq|N2QcoG=v7nO|#I zuof8Dt*G?=BNHc2uI`If=EDkA&*=Zu(Gh`md{|Joy!nJQKxw0BsY!)e2$tuzg{!~6 z7aO<1T0VH)6!(wPG#f1OhrP)zPXJ+_bw(w}ZaRhegEN|6KY>5mECz)iDZFnCXrasmoxt|y$H_cTDmG)mco%Q?AoSl#ut!x9BL>{hNCauN0u!l=g zB&-q#{;ve>71G6qk24vJot!=rvAQ1m)nao4M%B+oFC|)8C zBRXxwJgf2B?r%rwuy4Yosazk{(MM%r2AH-5BJBcj5V&{uDZ-$pkJhgSlp2Wu-Hvqn z70TS2`I8uoyh}4?PuF=LQhoc5!RU?OKMkyE&t^~E7_2g(c0T*NgoaT_Mehiigkw-u z07iB2tX%m_KNrFk219jzVwAECa7sEPt>KJ_15(Ak6Kb>e1X zplFTSQ_kfPlIkY(b26gkyr6r>AE_tP?X0`Sxw- zOyuE*`%ofzk+sbTz()R}S@ey}XD5mtrS#$mSa43uR)4-~QuhKx<$-X{XBob;s#SV} z)S98u;9Zg7sT(f?JPHLF;1Yy0LBhrN{Ksw6mWZk%Lw8$=NFfOym=eJv;XwE*Rpwxn zusRY9cOYJok*M|QxulqOhU3|V;%{JH**!V|A$Zh7_2_u@YHZ&UV9091KyIZm;2u8m z{`kX_=O5skwjtwrg>^2Ej8wb?F3tM9%5(zllxcd+UI2{7kAMr2u4i)+*mpq7^%dtz z8A@*4>sqZ>^BD{_@6xcgLw!;bCL@9C;ufvo76;;|h}GCx1zjli?y~Bm{42w7w@t;4 zi{olu;rLr64zoNNRIo@2ADwuy07;+vTdSVygztAyfz^Iv`$uWC!m^>M7WbwVF0w*r z>+NcB%D$$n#R;M!b2vrX3Ko+6>@9TD8Nyyjd2k4n0Fmh{6fAmU^RE3(Y#=@^1WQvH zD&ruWgN-jKZ177=78)3L|K?8HEn+S^Zg57Fqd5Ul_aKFEtRGmn1Y+2N4sIX@_Z8zv z(YJ2`CLa&4Jn&?9wwpy|jD9<>Uzc_mJ)4Ksi*r|`(KXbMPZVX@P0#rj8@xv?<6x$`xbTD9~ajx3AzgNMcJ7S z2uX)h0>#al1RA5(arSa4l(v8X1*@l*uDO3iGAlzg2WzdEVAC&eS=9>#( zqNr92SGlNWKKnB9J73y1JMel;7_r{ilrSm5<|D~hQ3_=B$7gH5LPfU``NQd4m>2O^ z>!T^441`x_m9P2xUunBxpc_m!0qVr)rS{HRv~4l(h7Gcqa^j{|jf%uuUzL`dG+R=9 zjJKy}C=L-_pc5N%5YzMKtq@Nb4MB64-N__Dp53v1yt>be9O89zH6$eBc~%x{bap5d zcODE6J+RK%jNXQ!N=ZV4^A4<4QysLO>6YHVPIE@#mJ|-;$g({w3wMz$-pZ zr8A3O_1Z`$$XvkD(b2{DUFQdv+js8hRwFc-AxA4@eh~8qRSP5Sn}8sKXO?sU*s&I6 zBC@r-gxa@nHK46EJ~@_n;D%0EV}l8|*qQV`cVs~C zCgEZD& zs!L2o1$aA7pEcqea!!Fe!uzyD!LP{4)m-;*gRC&2=rcmXAWUx>u4dRg?3rBDi~q;8R|^%lC84$AP6gtBpRe#}SWYA3ik^X`a5 zQ^DaP36(n6J8wU^OWS-KJUMaFq;|{`DMiK5mo;Gq7eD_Ytep}xOQ7OeNTnysufM*+ z8*YM=0_sT0@H~wDLl)il4L7#0RO6yaS?cJIlSS^`hn%7ffnOe?epn@Inx*A( zo<9KcwH&5lE5+e_cI{)vkO3+Wz0dWJ2~gs+`J$EB$X=%fGU5)pm8mE~ZGw|a4v)E5 zAaj^nKc45VY1y=Fa4`iCJ@9TgXAE<7a6kR780u(klY`Z7Uzwa#TUdy13BcI+qq)(+l9H;x1xH3irAWBIhUGjLwsQ-@eUWgF6P{KGXx&Qo?vLpM z>dfP#Ph_Ab5PPc4o2kl&--AbQ9+xAgChhc%vDuD-J@v3fu{6D zZ$f8x*U6XOuO`e8_CtTR9yp085pKi*HXVLzw#zG&G_2#X?*k!|V&-7bu!|i>ZYAE% zJXVG%2U+=2I~6Q@Pvtkiyx`dk>6 ziui0=@C7_qD->dum<bBMBo`5$~oyHYR7;x-oYnQ83ErHJ;c=o$e&9E zp0NJ!i_Bpa@%wg4{EJ z3^0>~STCf*hD~djK_h#u^AlXVdXVYKwyZ>pPZ}ZDs&@|};k=D>0EeYGhiIKGC^GoI zX`m-u1{g@E=u6wd0$Y58aPrHmAE=9fE~ynm&I^?a5!_3#COfO(Nx~x@cO>%A zXrOaQ%#lb5DJiorr~BVuu~Oa3|KZk*^NkJ54FNB`S+Rs;+g(DpAjxS2=Teg5)RHAv03xFu0257y$r7o{D8J3^2kN^>2jwY*6%HA=f zz*KC(J9_}_i{{z~vqszy96F|?Y-&0T2q+Y`d6b$yH!J*CXgy;<&GUg`1pE?K9CbpK z&br-u_kL0S1{!LbYGtW2H&^FYs@%#~G&?^#c`C}6%_t{Jk!{M`MAx#QyHyHZ$3JHL2u)D2O&mcR2H z2PT^$>%r4`9wh>QOF)E*2wSR~zadl#VJ<73xuD=Gzme$%Bo3fv;x}RpWQETn1Tx2< z@rPr*y}chG?zJO4mC!N+3eWaTYaGMw;l4zkx4?sN2k?N@o0a?Bc zOY1}y8nA|_k-4@Y(hJ{*#7+ya05@s zKxQcfp0M_)ocDa&g0+cTowU&Uj2^(-R0$0DaM9PBiY))=ccc+U`VmObt7?bwtjhsX zG_a>%U4WnB;<+z%FUmvRQk+h40M30dx+d!8E5N0u2G=3|A-5Y;hkCyQI)h>>fZmGG z4VA284f+ufm--D}vQy+YWE6Qm-vL5@zIqW^C#;`cZqvV4bL~gtbK7Fvk zX~NiFlVVKM)BX)X+!+g{Qy?CzV*!_`)B60pJWVlv`~~T(8q3Ka0UARfaXN8{h^0J^ zIA)2&#tMbThMZP3JF|JnoK_WTLGK>Rc6!8q%I7))*CBxlLS5PJ@4%TKw|2#fJ!nSs z4+scgf}q*Ekuti0jDSYVN7VWVwh8CJDaKK!&RY|G=ic42%$eg@l>AFR9wjJEV)hWW z>c~#eD&$aoh@I^ex-oJoh;?EUS*s&pmH{_jJl6+{$Ox;PvJN+XwIf*foQKbD%*oS@ zj|=$9ax0e=s{nCR0sWQ~ifn8Q)CB0()Ne3IpFlY~4yAD7P~d3sU}YFmyacz+h=CzI_Jr9_EW{C#rEs;*J2% zqN)#zqz?iVjBBXN?_QG${Bk3LERxmz*h_F(lEy-UKUuA421pgGFQkOKG8$-Y((6oP zg2#Y(36$5TN8w+#(ZL$G7pbgA^oTPKJ>>Dy(0Nl$gbk&O3iB(zB}QxT41Q@;-BR@F z3p7Qla2A%4mX^+V59powI)hrh8|Q0n!Y5>vdLRKPqXIyt--$%6V$TebAR03cv771O z7qHB&7bbHSV;2GT-B97ntd#LS=Hn9Qx&xcUyP(oh%9$t9owvfYla4mXvH(@QU&LB4 zxOfL9(-1BRUBX66FPN`v7nOUlHNXmS?h93SeyKSA3k_oQW=x$bkI>?F z^JaZ_7ZqUOVCm-B|NeU;ufg|s`fO^T7Mq4X=2}cPt2EdF78Vu@_oA(7GvV-%gV@hq z9~`15ja(oj#&YEqB?*$5meMCGql}LH~WS~nFhC?-6`t+?; z*V{lcb!WqIM^4+}bAUcqXmp)eAZ#83K%SW^h65Jo#_$cST%|{&yjnl#=zM{2CA*&r zgT_~znD?ON^A{Z7j4TmVb!Ak<&icHDYv*38GjEud-j%a(`VbVpN3cHgD$XH-2cev! z4CJsP=MtP+crFE|@o^6^*Svv!?%eyFB92+zz6cNXmRaY_w?pjk<5I>RN6(G-+i^klocscOGeFPa1jWrHt`tyiIT+tm z(Ye>eaNf%T+&=B>b5;?PV=}(B^AeW`1T!H% z9ZbxSzWd#zv=~)j8>piXx;(;&WSnKd7fZ?=;0c{hs40@Y+NgEHQ1S%_1%;xuJs|(AKH?|2=tnd;MyS zhBIIT9o9n~e}qcO#b!0{5_*2bFR6ytPvTYr2kh`Mt9ylv$po!VsqdqV-^I?u4JfF{ zKodd~vdI^|A}VA|2oNfSkPQ^8kOG_=GN)_9i|Ym$*SbeKu0)*B{Jm;XC0S+YA}1B4vJb9pev6AjrvX!sd zP9?Z0Qm_Z~;d?obyy-?0wmmNxK2;oXRvXkvip=Tcz9wrIV>}zuB&D?Yo-e~z8o@AB zmmqZhwBuYWfn`C>;_)|a@4kIusa1HR@4=%ck%62`yU(5MG;SV;&pG?pC%%;Zc7^So zlbnptd%f;uJpcIVQK6LDa9XHl!|r!S4VJF4@LnRSQpf)$NjfRXfK3lk-5VaJIF3(2 z3`lAl)LX_Ff+Q{rxd;ZwMLAS309zf<84%Nf+cFTBKRQA?x~h z@XFMQFT9eA6ZXV-r>K7Xx?9)A_S^V97YaI5x8kU%x-7`y7?<|a-b~1w9rvKZuz!xl zrlYkBX9!Qmoj9j$^CfE08p*eJSK23US5%sv~fL$V^o?t zN5;b0Jk8@6O6U6Lm{@e)>)h0t*krNb-t&_KENypu4_&{Yw{FYLIa2qQjGL?|xuC6V zUedk`FQ=_(nRu_8-x5UsAaYXdOqqo0OE_RaV`JBJ;gW)U@!v7+}~llfLur_9oEV$?$Bm((yRSJ;-}eFj1xWPL8kx1$a; zqza@x-YBr7^YaXs1+|^uJf-VJzr2u=Gw46gX_w3p=V*SMwD5t&`3p;Wz(IPC>{U}A zI$2ODCG$D#+U}<{I}Zt4+Z?;iwRX)C&Pg2H#mSFN0^b;QI5t77v{pH0Cj>2r6A1l= zQJ@mE{k8MQpFNJs=%qkY#wJ#ySY~8lwrC3f9_e^s(=HftJ2N zZ>Xt*Zdvkb>)`4W;x-pM$9K5Q=(o4Maw64*l+RumN5Lp=)(8{)h`tZ`^?moE1d$IE$j{yPW+8R3U|*O z3q+iB+!6uR3fbAUOE_4TjO_|iLH=!KUS0w~|I^1k6QbDJc;e0LpDbb1_!;RwAM(0)Us<%O>->b_OZ;q%JH5dS z=^CsRSK*h=PLtAu_I`Wi|K@CS%7x{*s5lQm>MM=*IhOXb6sYY!d=6t}#`?zNHlLlR zXV0j(u3dn5PnaWWP)i{0iw%}+G4Mtsh93Z?6v3(V_p+M4lueAI#p1c-?C%ybd=%)A zE~n8v!p?p``W=WTW`E_xEC4Zp{jf-=#$@(=`jv6nxSxZDAKt7-g7YBesoi8v{-veR ztQZ3uc`|(hr(FlL%1BC%u>y!leZke8Wd8`dlPb%xSCS{PVZaW@Nmp5EFp8)qkD-BB zBlJ>tmXi1$+An-qx22v1p|J*RYC6&s-~bBl`hccigDeh1L7gi2RYLU6aFjWw*o$b7}9@X5|vQCFWh{(7$AQ%I09KT8hfk+Y(=yYa0pr0 zX#VBOSqof<-XY)$9AX(#x0T`*sz%o_+^*L8)Yunf*ey&-kMZWlh$`T86HT`M`0cw% zxassC-K$Z-5(-7LbTL>4ZAqXqcp=LTZh#eH7&sg;aFqnM5TAtUZ_^eF$ph&$%nd_D z=r<(*JS}PxJDf!nn`oFXQDnFrDvMHZcDdcU^)9Y!$MEM%sL3E@>V+vE$l1f~HOUXr zDD(jJfi8l%xFe_4)k9{N0x`-mzy#;pG1pNtrvlX+ zQt-QYH=RUh5}tvHyR-FN0NAJZ@9Bx`lzHk%wU6IT68501zqOw;R&NPL$00{2R@dXY zvy)SyEW~>%2FYT`zyV08WE4?Vne9-(T7V)Y1YWRh?X|R?#0b#_K4tuL+Lkx*lP66I zhn4^Wi?bc(;@}@|AG#+>%FC^`^ut`Hs^7SrAn5sRbm;BqMu3neomJ)Ksg#*GdjAjD za-x$5Q|*Yt1iPU~?%#oB;2xOrKv7T2;r*<{)A0$Jxu=m z%o`;O6>6m7Md!F2BvXC)ywRZ$1V@R|t(HCp6RE9yERr0Gi%<4Hk6LFH+yFnS0K##w zy~;p__ptT+OX=hpGGe~qhZuT=t?lTO=^@2`QcnH^x=$LxEM1-uVm7K8QmLv|}5& zo6(rWyLt2LD1E#cm>W-YM6k`B>ppf(Jc#_o#l_mAtkHW#`NjSnaldx+=FRa}^0%R2 zk(5Pe3N2W$W@-p-vRK+1ja#KRovEwQ8ii9+@(Q{|S-cn|C{82|aId%r!lD>MgWTMs z;V57Cb{k+mM2kZ>fMzz0ZGEf|!T+gtQkO#M)Y8{dFYzbb{}$`{#;2TvNqJtX=Gx2Z z)*`;59(sj}J_I0d8vv7HV9{EMwewb7%igU8r4l3i$p2J0iPB}X*QKXodRZU}BW(II zlQq}_cFT(pmBB|5#6k2peLu*!pZGY?GEbBJ?@A^CaZ+y(nTe-G$}IJvH{iP&Y@CT~ z?kFN7NDUL6-hYS6PSpL#vHL%6CD5Y2kH|G*>#*zTMuC%!$9#bop6mCZ?}+wt|lnAk{T z4i+creo!HoLVNuH^73TM0p`0w{rxZ6QX7~KE?B3zQ#6o*K7XEd+a*`JF*tc;qZwY9&C_XDF_ z4@u-5G!kJz2Az5;=dnEc(AUt0SS<%sTWv!I%5Ku$$ph0%wMOU>0|HioX2W>sRygi0 z8@oJ)`RZu3*kL%F=g$j7Pr|b(YZ#}3w|@-rphh0c;3#qpb6hnieKSa8p zH>;~CANuXLn|Qifqr(r#CXvNRyC zh8DvcDPr8|2ux2UVL4hSAt-&awqaEp`0QJVw1}HMiw4lcWDIZZ)C3?G5-y;uKnQlH z2`D{e(tIA}x@#ZJa~%I~Ac@Z&rdWF5bJY_=2?@ui0Wro}<>yWSrFyC)WCISBRIK19 z_fK22bzRXpIO9p7$}>$`3*`rpSc^K32q0rV5TWPH8yp(qII^hjkLHsfcuWf%kH=KZ zJ8w0{+c(wMYof#k0D(?3l_h3Q8t7*N)gK7?cstSzk8JJdMm3PNDx%Y0ts076Jl}Od z`MBW6Su|w02^}rWP%3RBlRo~(hy29ggx0KF85F}zrW{vBujNzv?<3?$0XnMx6e0f` zbV+@vJL4Y&_T61*E|!LNM?S#}~wvO7+$)hK;{D42|Vm_}hL- zpWC0rL9T_YiXFo>j%K1|QT5dq3X5cl>hvS(IL?i>&iN5u{LV`MZ=w#%PvqE6j*E|u z3wn8vok#S`dN$%=$6g`-@2!8{5WC6nvnYn-K^{vIx+Wep>U4E=NgO%^1qU1fI_}>! zkj1tq6qa&j1eYk`Rk)9AT4V8_=^p_Fu>?YCDfBSq5G~VznOJ8TfiS2l^bM+0S@c?& zU_5d9m-p8e2%d1t7;Iih8gb}pOe(k$qy45(e8$V}y}^Hs(RI4a5NL;^WlEr_C~8#t!je^spwM} zuE5W;o^-T~oy)f4QwSxsv$Nx?CPf`~DHkBlRT8(8-gkKwNuvQX?rDaLabw;rXi0YD zdZH(sMD^A2u#8qHvCq}Ve66Ofb#lZ!goHfQ9UA@nH1^%qZGp`5m@N)UY6mf`wkP2Y zg`$=0?mqMHxwtGBpFX5fwPg#cynksL{Ji%s6vN^}{?L8=$p5aWLIu)~-2G)Utj5C_ z0fy6=d(Q7BM1UPY(5EK`-@jk=HA&m5*%mSkkLaVv|A9P6{`hnbz68RY{pHJZhcwJr zb)Vz@p)>jMd3lwpXz69!DgSaTxnPS!dxQ?^FCjPwxVt?g%^W&);?yEPeE5Pts;oka zeK$C|@yq)89vEHqe?hyW`Vtcp<3^zgGAT|^>fph0^xhC#i0TX4*_CLdSzlm7f^-}{ zt&qRI`2LXnzTa=ip`WAK180|MCy9Y@g5uXl^{P|Nq9Jp4`_zRzk_84hq}+mstjZ9e zfJdl#klU25P?SNz;>@8|!FylehfspOx7dRj(|_pMw0Xm1EZhQQZl=8m`RLr-LYALsG`UjR24Su6$d&Dh#`m4e{!%#V3ptNQZvIB| z&zgX~Ahtw^#rLF6Q%HIYYaLRRe*~!RYwhfRrgN!_8dNLVa(_^*7A9KBIE1QVk2+wJ zdK_QXg^CTuT=Hop-tF5nsO_|$T20z`x7i%XX(EyGtXc2Q-o^Fb=$iY>jvYIkuRN{3 z^;gz-TDPYe+gsYS1E1SH2L7)C6FpbE3Z!3#D4iiBMsuEVYCm+LT+q;w=qVn$FePXs z@&~>^GX^?U4^0U|M$Jx<)&Mf8DH%4X&mHLmqgVDtr5!BP97Sz>BsK?SX7aJ-Fi_kw zsEpa*D~)wu{Wa&=vsQ?aHsmzwrDbMjQYRU*14&dOuc5BSNV*`}$53rmX59Qy7CA0J z#rL0*%skT*Rw$*}(Fq=M&*liQDaD;T?ar1xZnEE%hiA?QjW@6S{ccZj|FmhX3EC(NFKIW*J1Q*sW}MgGqY?gl zv{gsEM`a@9HhadUSaBZvHXN}x=C-@+n|HJdcKRcBZ>={vC^=aBX1|neY-!ZUko8r| zbJ_d{+g|T9+b6~*Ys|&9WaT2+tG)uy-z)8Y`$_UqyxHkj z?{!%VO!`07eS0|7`TDk*_UxHaJHIAHGf7PRiE=9dxC-6q9-UbHrS?An=Q{-xx?J!nc(Ch4 zTkkHL$X~2DvBe?${Q1O72C-Au@lp%C_9`0Utsg^6xo?TII=6MK`poH#rBi{eXMQQ> z&zk1U;(lm!YDiY!Spm_g#@qT2Dw_6Zey_8cTh8rSd$DrIS+&kz6ogw<4|R{}XmLfq z3-_m+T(^%tDXSH^8Z(iqCdD&=D)Y+9q7w!`etOZ^uG3B);`PmC0eJ>xwR;!72eTSbWrR3wAF917Ap+zb2;#= zm*IT9=G2DaIf67L$l0#`yC&AW()?(wvCmPZL!o`^KfJ{PZRbS=KVtJ;=yUwTH0t7d zRjJTbrGwOP*5*O(I^6ZxF(+ss;K0$7OYGmJ>l$vhyc6ZGa!Atk<7ZqU&H2Un#iJ{s zpH2qu4XeuayjT|L;c1)lpNqY|V!f0^LCrPk^p(N4*O~=E0-I`~v}=<=`(&k3LdS4| zW_7~Wfr5pXt|FCVB%9nQOZ9bMdZwK1?(ILLD*LT-$%1WM(RW{oF3J`TixV&KC+>`u zbUo<(_#8cQX4I*Bi~dxn3V$n>*F2Gva6vukpzx7RrCj5;(i-%I`4`OReXV)m%6pH# zN|p;7$7w9B0rjM%kl-6tH(u|*58MSG;`)ho8x$~Y7EjiMoHrO(wy1JiEl1wX3y%zAuX zE<{P`$#v)=bF-|s=8H}~-P2Zk^$VRqgKs9D78_*@eqF1rxN;yYbjQKTP+RPHL0w3< zgkzAXC1l@dP?nbCbGv8Dk|j2u|D3BgJ^$T{Z&OmFS-(kyW`DJH`=}JlZ1J{AgrBr` zt5SyEieA@FXtpszq|F#-b~K>&&5q_I|B-Rs-D_Ev<#)@BjUP>1NFN?}2&K}Ok~_FR zhZH4E16A5)!dM|MpSaX2fuSp=O3P;>fub5g@WQwANL3yyCv-rE3KrvV)GrJd^#EA8&JR6Bcf6qWwE)sNwfa4 zcT1whnb+a&^FqJ=P}jfQ`rvP3_n-3r?%$&Y72xXmmgAAkH@@xCwTD=>zpi&Z`#F92 zH(~qke3Gd8D>|5apJP%z?s`eBIY%hsOWwuSVP83&9t2B z+}&Nv=VC_S@wrkCp?J@)KECHKe|bQri8+?5=l#wxH*F|&_^i3*O6Y^X<*;jZ#|nL) zM3lwE9I!IHsHeFtFHYU-bK!ze$&W8_i9WVhde70~&T94*zs@(;POmw~t;ge$VJW`( ziod8}mPm5jCw2cvFyz&TEc@lF2)VYV2VP*oHBxYJx2nEB#WH%kjUe+&?>1Lqf*5G>bOm%wL25TSV!kDO4%LXzgD| zF?vES)cQZ@dGowf{^zw!PCh&PjB156YlfRd?=$$vOgE5z`G19H>D+-#EgiVcbtE?+ zkA{YY`94ztX`G5vj0BK|?FA|OAah1TCm*iN+>~kbxPOP@G9A(a^2Z^0ZgTQ4x?o_X z36=6df$xGa$I93D7W|yaL4c$$7$f%x!)vmK9YIF|9qOJQY1;2O7ZWLH>Rm6TY~=L< zp7j&(*)F7wud|97l3lcj%Ul9l;K@0Q*qu`}F*R`yzrD&|8{8X`YJiKs9nDVEdkv)i zLp#kK`QKa~JjE;l-B>{0K(9<9Eyw|UpN*lgrp7ATyP3z1Ns!F|c%{CHZAF&m58B{7 zw887;cSNoQjoK3Rm@jDLZsyn%TNibE{An-z1ex77^U;?Z#;|;e5 z7Klr86+7=X1=J-cMpg$+bj0Qba3OtFHRCCVPE-!&-n!b6tmtT<(%B=nGYH+O%kW-X zL%XBEH`41oR1-EfanOw?li~{{2*mo|ae|S-??lHGWQH|FSj9$!fsJ?=0EH4x4`I&e zKrkl>^d?}AnF51DEPkZvI5XXGf&9}+rW9r9I7izP_>By z3Dt!qvBuAyy$FHW^LKD4*#RiG0oacfTn&nXNFj&zJ&TDKug4U$kM%$=NV79`KIDyu z{{*hq-kbb3C@_=JHs9M+2Saejpy|o6cVIUh7BKUho~x_ug{4TPS}JT4N!4U5xx$~d zBm_cwa61g)p1cEmOKCiKd=+=N3_<@oS`znA!1~RC zGom~A_$)DcJw;^MEl}O)@!~3eI~{g#wgp~Ji9R+BL$Jl%288D-h`vP8#<;dza5upe zHKxtP@#lTmE16Zx8+pZC23OC-fjB!FmZ8+m-1`PU)aIe z95A@MLF9c6T30%Mq)R|m9D|30!?2(zkWrO;OR*#Q>W8f)NKTZpW*Cd^Uwk+t1)4_X zz6txq=h+~V7geZ3jX8ib^5emNjsuSRJa>99WLh1|U-P&OtWQDlOd)E6 z-OoGuTxSI?^rA(hpUqw}xW1G;IVhQ3R zBSe)Wk2IS)9?i>)90hN1FC^|v2lP9ZSE0|h!Z_(}vF@j}Z`zZzTn7i$oEL%lTm^9a z@N|T9O5hy1Ee(ch(Awm=PlK)>t{{S#uk)yhS*-z!;oaI}X#U_t&R8P-QAQ8Q8>;9m z=S|8bpo5)$*AobbvN%&CD zt3sK)SAh#LLeZ%4>{VcP3lZCz29}FfibyMvoq>>^iuUhBM4dp?RS8F(eF|I%2VI&t zyEoso%@D%;VWs1|0GhD~-z|-<{uP(kg4c#*#z!hiH*Q};jTeq6R%KEmRtM^XPqob2V%iHV8*;i+j1`A=Mr4AnKKWEYiv zgC01oHg@9r3Rzq*m`o)roa0%?v-fMpM}t;O09%jJWlqOjnJ2V ztFp`|@JbGbY6wVdxvAQ~4`lQq$B!TDG8jsPI%^5aA(^Ua^z_-R7&U<1RMYp3FF9fbvlazCY<52x^8PFm}*ccrNYzq_FRBlbrSn znM_@mV1HX(*g9%J7dE4Dc;Ri382M^-Voc=SQqIwx>q4s!H^`0U54={I@ETF|X=e{@ zV64=e{&uK!daBV-%}o34kC{OT3{2d#9#|=7u}tZEn7yY(#q8TZNgE3qXeLZk+<|br z5CY@_o2jpjT+pCl1>wRvpuoirR~Fa3|Kc9NQBKx)u});H0M36f%R&U&I5*#K;6;-* z#N{sNKMAqW8b~tLOG`^r0Z$5H;(?uSAsR<6vdAb;Z^3HI^`N6=+b7{RePpNxK9d{g zEgS&#*@d7~F`2mS)yNFtUK_A9E-E~2o}Z>?tut$*XUjn-mQ0Y2<{Wg^-BOFFp3vc^ zdnl;53#p0*3}A$?l*`Gj`a|;XX16Iu+)vUnSlrx7-&3Fc%V@6ic(i-P@+yR|4Q7)T78a)94~3CicUeeT zMhQf^`fN=YA97UjRDX?Xf5X-AH*XliOaPnZRm!%v6zjyedGo^+d?0NXYU!KRv|(A8 zB;}*nECS_)i&zm4I?9WI44C?GRMya?#^ERUCdr&14=-)TTOLQG?Y#1JnBhIMF|Y(YD{+$MK zoxqiHVGAcscu#IDqwuYev6-C|P-pBPpbPemGdP2M#7idgv)wkMR{{&(+4kU>STiYirQ*l4|4MpG3Sgg`(dSsAdiLLOy)~$ffGhwOg(^{W#6IhO5rA zQC?UN5@s~!y>xmbb=tt@6Hm%r6tloTF_8ygoNhp50`a}&-G?x>`?I{qdFzMby0Ni^ z*V8kz#VO^{yV|i-Dt-`OP}H3-D;wAQlel_8=S19mq^cb&D0aMqnmnw0k19m>dG4;| zYN215i3^Ju+sFLKd?-UU8-_APOhED#jg6ZGNjZI3sxVa-tR75!)OPQZ2}Tk+_l$nw z+u^{yhsuN8J&HADVvE=TbtWx|kzgB^Hz2DT=)8JSvO19?W$kEOqA61$J=uFzYtU14 zH(uneiOgbVE4CkgJ%t=@B^X_n>APw$j$$dV6r9{*G!JgHxFJHsih0;3u_SD846 zxpuxi1Xhq5fYW4#O8x1!(`I!4?DH&dqE(4RBeExVCzj`-D#C?FG6nD*4)U=5P0AszV&R5z^T_@$#x$Ou^H>mJ-2x^n@#9 z>t@dXt)wF34pvBPRFo5fak#>%#z7P}abVkO9yal;CEGZP061EQHZljyy#Vr z`?XD-$(tDl&Hf?rGA#*_#)Un$=j9};m#kEozsct@$cj%!!&3SfTlV8e-v;u|`TIPB z2qVq<#3CQ3^5Y8Xh@{F)c=GJxOd%y+PYxP>#oLr3^P5?ArFx?d$TcnSzck*yp$6?O zY;Kxy>u>P_JEzuwK^;3g@K2Cm;dnnAnS`JrOrgkCR3H|W=6m(Q18N;3Qv=wm8`*qxN&2575QT#RwI8;c z7Q7K*;NFE2L`~gi=kn?*+CJOm8ZAVxD$ljdWC%`S?gKjN{yw_6892SIgY3l%qqEy; zCe~NvHtzJS-R4~oaa%OCQKeW%zM^3C76WjIhe^ZydGnk`2b$yCNUt7>DFi_O& z#3(CF5uXcRo&3fGS!srj&u7nJbb8L6-}H7=+l7*mw<3|c>G=&+d1fpr|0L80QE5NHye(Yx?xP7Cl zsAZN{1tJm`RwT%I);U9dyB~r9CG+H8Cs1_wXfF||im`)zO_!*92i>Y`LyP2q?)x|Q zltUD_Shw@A`*@fsF{m^XOGkZN!+gg4xYKUqAiy={*2SqVuU=(6LgI3_I# zQqGKkwIf1?^r?^GLO|PjKTEkncDeL*R6FGF!M=jZWi_}ESbp{u)C1R$CVucSM7>D1 z!sLz!?+=$IYp7XC`DgSPXM6-C^>ir3(4Y3@l)~=85?(k59rIzS2rvCcbbw*&sbqwS zNL*q)!hN9R&E45Bu*nt}07`0r0hXW<#qv%-88fyjS*5nHe0m7elEYx^mw?bo=0nZn z@b249y(Z8Cd@*yN7a{s6f+qPTq0@?2xNajnn*_xD7ML@qx9X3GKG_2iWIcc~=@W$qC@l2pCKm&4EBLg7f+?br*S zNFW+9or@RdYHxIZY682+Xo4otz|*W&^}OA^l3<{(M5<|j*+b4OgzDSA-N|^x7nvpN zXR^|X&e|q%5ia<2e8zX90#70%z`55JxBU}ba+h$9yO0{yjXZaV8B#^$B~~lQM3jrB zVYS@AJ)sXw88!hKh>j!A|Aa=k0d=p>N*TaE1kfgJLh102C3=0 z!yB|RZFiXhl zcTrq7;1mr=Gc-$UMu1o(6$05ZMQ;D)oH-jH#ZLt-K?qmkoB#_q^U5|nN~TBIB+CNQ z2YBf3{V7J#iBjdi&yoEZ8WZ80z`63e2Z?7lbJB1Z(VgTfo)-sB#(7xh#Niw?jp|^u zO`3z{ZfQ}RT_9kD8zcL4U6K%EGsT4)PxiKTh$|yvCabk2dnw9BhYSq)g!qA2B7)r) z7q74g7H$^TcCJu|5mj_toQ<-G=J3pylVgI3PG9~@4B-X=x@M&9e#_a9&_esd)|9ct z@@4W7o55vF=0vV32uUgr0Ifzst^H~u8uNgR>+e@3-C zw&yBT@dpqtold=4h^=%D>wF2IP^Uj8HZalh#^3EAwO4dy^1;k@JyQ>Q4vxzSIj|1D zyVTb5bjetrgd{Od9Gu>dzXe7Lo_{chSs_c#taz@Zvsm_HX95^A?yv5iIKg@kJG1R1 zOojPm_xET1xIpSVXhVAN@O=(IrPD!vRwRb>2<>~&e6%;i_Lf@|GFsbbmjLIkxl@wS z>*d)iOYeKUT-I4q>kElWQ)CRV*uGBYOn_L(29+rQE00X_$W;f{H3ygJb2lMRL?ltZ z6%rK*<11W6Bd8ChAH1D?7xjM%BAz?_G;AF+C-LK#k#}`r3lB)uen#eepywRG;ed0T zsS|PP6YkG^miPrTYXGj?5gM%41qUK>5uw&xKLhL}6d7oP9SO(DCb1im z^(u+sj+wHL##Y$9kO3EYL}9qm zYKzn`*m-XejAQqt^1>uc)c1Y1Ubav$M##0UtqGFhjxxsR8*Eyc5n6 zq(@p*e3Q9j=NfHr&e7pa#;tEhY7NX_q-#q~H_(Rd3(GDz<5{C(-2wIj`9I=8u3537 zJTH>8`M@E_;=Z{QcinY3wWkB*Ve!_Y@CP9y1Hg5=l8RL z_cr6KteY$c58}we0cK%-N6W%v`JxwT1+$LLq9PGBeJWZ6WZ8u^j3y8b;ICGlxCFT zdl>Q1kr;&Z8W>p7i}PiHyS4$BjnQMUm^OgV1$<>Ykm4KRke3zrPXoe|0xNnu5x}r< zGl9t(vT;mT(QlId6bwko>KbtTHj)yMiUm8Oy0ul0=R-!Y^j^?pZmiD3K3WYRnVJlE zQv!C#Kp)AWZ4oO24}#enpJW3M;4EO_Gj1q2G3flH;M(nhJH=eTuzi5xYk-;|DK2^a3~ny2uUj|)b_2{$j%du1aB#s7R=G))nf-uf zQV}gvksz_eSf=yMQm~zxIuD*frbS>xBTy z?9s8<9F{FRAjp#3}gDrg##IJ*vZ|VQ_N89nKV2qV|Hm#mB=v z*pImws%$>Ma3&K28#l2{fEjASJUL?$9TA|pcK`v1kfI9u%fNG{MN7~P+G1>iYkWPM z4{?%@&L&V3n+kBeKCG~3`Q`NzM=n4iq)2wB2sEznX_?$PiX6+B?EvY8a@)GeqibYDlG zsu!R#OBG-9Cj^!Tgr9T0Xrzce@nZ!6DD;lC=^a7H@%b81)UoUm&|uv}u?ep%l2aB45LUP>whK(a~#FZX;M4|fkPfQ+cxea4NM=*$HbtcJ>1q4TKPt)bEBxMy z?z^VXVPVD|bl&9l@H;X$M~UB8OTs@Q_6zHhk~%Jj!n&)#ZjTZ!34ECh92VCm7Q7;$ z8Z`mbljkmv%^M6M4CM+(U!9O41GsLGooj~<%{L-Pw=1(dp|5;;yENMY`NJ8-f}E%2DgVxDb<4`13d-ZcU{Y8(qV`iBY5a?2*|gb zW)rZ|<$%7W;6Sa0Wjd;On%{HFU*v)t)Q|*GNDu9ooKY~nu*C|=Mec3C!Avr&BPBYi zQA%)h$=Bxzsz#&7DB~OO3p7ZLvEf15?tg9t^yCKhF~Aq@L8h49KZbKh)vpG;D;ds6 zbQRq`#-d~2{vmPflmL8SLIF{IZ-Q^pT(QZ8El&*9vWka($`os1iV}5>=^`UINeSuQ zew;OetnKwM!<*i!9$^bMiwA)*B2!~HQ%eQ@A^UstS^c(vjBIW!t-cBOch7ZY4O56o zE7z^7{q^L@ui=2Lz=b56E=#)TA|h%xj-nQb19kV$st}V(U8o3#Q;Iv;GcoF#Wu1{P z?j>u86r%qUlpeFz?rF?ws|OgX3)SgF#VgIt^Nm?;>Nr7lBy~+iV5XZ5!%Uko0I^m9 zBdVR(1(wl-HRQGcNgVqsK(?-B;25vOoQ^*H;L}pfUtC^wu=%!znI=BmMruSCtSuV% zc~|N<74!maiKxAZwpH5l0mx4E*}IW)SqKpMT-#bkza3zP-2A4xg!z;yp&ef?o0u?H zqPz-V%3+GEI6kGxot6IC2q=W3Aa^iNd`FNFNJRBGc*@`%_8VJB3K0Au9{8P%%*fjZ z_wf^eWOr#c%EgCXo*#=M5U-ByNyUytS2-4r88*)@6_iuY9?(ZO`9BQzNTpyrN>?4(thSoURCO9ImA|ps3x^M-C2T?r>$cioM>#x#?4?+z3?f?JvZZXrdgl2C$MPs8g Q@?)cI+kedb{^#TW4I>px Date: Fri, 30 Oct 2026 06:44:25 +0100 Subject: [PATCH 45/52] fix(aerotech): PandA mimics the DDG1 shutter delay, shutter_delay reaches DDG1 - PandA line trigger: shutter_delay (default 8 ms, as DDG1) between the line trigger and the first gate, get/set_shutter_to_open_delay like DDG1; the scan now reads it for trigger_source="panda" (was 0, so the test series ran with the first gate at the trigger, unlike the beamline) - a shutter_delay scan argument with "ddg" only moved the trigger positions, DDG1 kept its own delay; it is now set on DDG1 before staging and restored in post_scan / on_exception - docs: DDG1/DDG2 timing chain (exp_time = gate period, readout subtracted), note that the 2026-10-29 series ran without shutter delay Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../device_configs/sastt_aerotech_panda.yaml | 2 ++ csaxs_bec/devices/aerotech/README.md | 3 +- .../devices/panda_box/panda_line_trigger.py | 25 ++++++++++++++++ .../scans/scans_v4/aerotech_cont_grid.py | 27 +++++++++++++++-- docs/developer/aerotech_stages.md | 10 +++++++ .../saxs/aerotech_stages/aerotech_stages.md | 8 +++++ .../tests_devices/test_panda_line_trigger.py | 10 +++++++ .../test_aerotech_cont_grid_scan.py | 30 +++++++++++++++++++ 8 files changed, 112 insertions(+), 3 deletions(-) diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index c87a3910..af25697c 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -131,6 +131,8 @@ pandatrig: trigger_edge: rising # gap between the detector gates, as the DDG2 default at the beamline readout_time: 0.0002 + # line trigger -> first gate, as the DDG1 shutter-to-open delay at the beamline + shutter_delay: 0.008 enabled: true onFailure: buffer readOnly: false diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index f53944e6..ac20b489 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -81,7 +81,8 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 gates per trigger; DDG1/DDG2 take exp_time and frames_per_trigger from the scan info at stage; after each line the scan checks that the MCS reached the line's last channel), `"panda"` (test setup: PandA configured and armed once, every line checked with - `wait_lines_done`; `shutter_delay` argument) or `None` (no trigger hardware, motion + `wait_lines_done`; first gate one shutter-to-open delay after the edge, device config + `shutter_delay`, default 8 ms as DDG1) or `None` (no trigger hardware, motion tests) - readout time (gap between the detector gates) from the trigger hardware, no scan argument: DDG2 `effective_readout_times()["ab"]` (default 0.2 ms) or the PandA device diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 6bd4a9e4..49b50fc8 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -113,6 +113,9 @@ class PandaLineTrigger(Device): "falling" (active low open collector / differential output). readout_time: detector readout time in s, gap between the gates (gate width = exp_time - readout_time). Default 0.2 ms, as the DDG2 default at the beamline. + shutter_delay: time from the line trigger edge to the first gate, in s, for the + shutter to open. Default 8 ms, as DDG1 at the beamline (it opens the shutter at + the trigger and starts DDG2 this much later). """ USER_ACCESS = [ @@ -124,9 +127,14 @@ class PandaLineTrigger(Device): "line_status", "wait_lines_done", "get_readout_time", + "get_shutter_to_open_delay", + "set_shutter_to_open_delay", "panda", ] + # same guard rail as DDG1 + MAX_SHUTTER_TO_OPEN_DELAY = 50e-3 + TRIGGER_INPUT = "TTLIN3" SOFT_TRIGGER_BIT = "BITS.A" RAW_TRIGGER_LUT = "LUT8" @@ -152,11 +160,14 @@ class PandaLineTrigger(Device): input_termination: str = "50-Ohm", trigger_edge: str = "rising", readout_time: float = 2e-4, + shutter_delay: float = 8e-3, device_manager=None, **kwargs, ): self.panda = PandaControl(host, port) self.readout_time = float(readout_time) + self._shutter_to_open_delay = 0.0 + self.set_shutter_to_open_delay(shutter_delay) self.input_termination = input_termination if trigger_edge not in ("rising", "falling"): raise ValueError(f"trigger_edge must be 'rising' or 'falling', got {trigger_edge!r}.") @@ -242,6 +253,20 @@ class PandaLineTrigger(Device): """Readout time (gap between the detector gates) used by the scans, in s.""" return self.readout_time + def get_shutter_to_open_delay(self) -> float: + """Time from the line trigger to the first gate used by the scans, in s (as DDG1).""" + return self._shutter_to_open_delay + + def set_shutter_to_open_delay(self, delay: float) -> None: + """Set the time from the line trigger to the first gate, in s (as DDG1).""" + delay = float(delay) + if not 0 <= delay <= self.MAX_SHUTTER_TO_OPEN_DELAY: + raise ValueError( + f"Shutter-to-open delay must be between 0 and {self.MAX_SHUTTER_TO_OPEN_DELAY}" + f" s, got {delay}." + ) + self._shutter_to_open_delay = delay + def setup_layout(self) -> None: """Write the wiring of the line trigger blocks (other blocks are not touched).""" written = [field for field, value in self._layout() if self._set_if_changed(field, value)] diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 71626551..fda9378f 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -40,7 +40,8 @@ Trigger sources (trigger_source): outputs are active low according to the manual: "falling". "panda": PSO output -> PandABox TTLIN3 (device pandatrig, test setup). The PandA is configured and armed once and sends the shutter pulse and the detector gates - of each line. + of each line, like DDG1 / DDG2: shutter at the trigger, the first gate one + shutter-to-open delay later (8 ms, device config shutter_delay). None: no trigger hardware (motion tests); the PSO window is still armed. Only for sessions without hardware triggered detectors / MCS, they would wait for frames that never come. @@ -207,7 +208,7 @@ class AerotechContGrid(ScanBase): trigger_source (str | None): "ddg" (beamline, DDG1/MCS), "panda" (PandABox line trigger) or None (no trigger hardware). Default is "ddg". trigger_edge (str): DDG1 external trigger edge, "rising" (opto output, J7-6/7) or "falling" (open collector / differential output). Default is "rising". trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". - shutter_delay (float | None): Delay from the line trigger to the first exposure in s. Default: DDG1 shutter delay for "ddg", 0 otherwise. + shutter_delay (float | None): Delay from the line trigger to the first exposure (time for the shutter to open) in s. Default: the value of the trigger hardware (DDG1 / PandA shutter-to-open delay, 8 ms), 0 without trigger hardware. With "ddg" a given value is set on DDG1 for this scan and restored afterwards. settle_time (float): Run-up at constant velocity before the trigger position, in s. Default is 0.15. minimal_run_up (bool): Run-up / run-out only the ramp distance v^2 / (2 * acceleration), at least 0.01, without the 2x margin and settle_time: faster lines, but the exposures start right after the ramp while the fast axis may still oscillate, so the captured positions are less evenly spaced. Default is False. capture_device (str): Name of the A3200PositionCapture device (always used). Default is "aerocapture". @@ -264,6 +265,7 @@ class AerotechContGrid(ScanBase): self.exp_time = exp_time self.trigger_source = trigger_source self.shutter_delay = shutter_delay + self._ddg1_shutter_delay = None # DDG1 value to restore after the scan self.settle_time = settle_time self.minimal_run_up = minimal_run_up @@ -328,6 +330,7 @@ class AerotechContGrid(ScanBase): self._compute_line_geometry() self._check_line_limits() + self._apply_shutter_delay() # One readout per line: the last point of each line, as in cont_grid self.positions = positions[(self.frames_per_line - 1) :: self.frames_per_line, :].copy() @@ -618,10 +621,14 @@ class AerotechContGrid(ScanBase): def _compute_line_geometry(self): """Velocity, PSO window and run-up / run-out of the fast axis.""" line = self._line + # the trigger hardware opens the shutter at the trigger and starts the gates this + # much later (DDG1 -> DDG2 at the beamline, the PandA in the test setup) if self.shutter_delay is not None: line["shutter_open_delay"] = self.shutter_delay elif self.trigger_source == "ddg": line["shutter_open_delay"] = self.ddg1.get_shutter_to_open_delay() + elif self.trigger_source == "panda": + line["shutter_open_delay"] = self.panda.get_shutter_to_open_delay() else: line["shutter_open_delay"] = 0.0 line["readout_time"] = self._readout_time() @@ -701,9 +708,25 @@ class AerotechContGrid(ScanBase): f" is outside the limits ({low}, {high})." ) + def _apply_shutter_delay(self): + """ + A shutter_delay argument must also reach DDG1, which times the gates itself (the + trigger positions alone would not match it). Set before DDG1 stages, restored in + _restore. The PandA gets the delay with configure_line. + """ + if self.trigger_source != "ddg" or self.shutter_delay is None: + return + current = self.ddg1.get_shutter_to_open_delay() + if current != self.shutter_delay: + self._ddg1_shutter_delay = current + self.ddg1.set_shutter_to_open_delay(self.shutter_delay) + def _restore(self): """Undo the scan settings; every step is tried even if an earlier one fails.""" steps = [] + if self._ddg1_shutter_delay is not None: + delay = self._ddg1_shutter_delay + steps.append(lambda: self.ddg1.set_shutter_to_open_delay(delay)) if self.trigger_source == "ddg": steps.append(lambda: self.ddg1.set_trigger(DDG_SINGLE_SHOT)) elif self.trigger_source == "panda": diff --git a/docs/developer/aerotech_stages.md b/docs/developer/aerotech_stages.md index 9d7799d7..a9bd2951 100644 --- a/docs/developer/aerotech_stages.md +++ b/docs/developer/aerotech_stages.md @@ -80,6 +80,16 @@ trigger_high = exposure_end + v * shutter_delay - v * readout_time / 2 (backwa (typically one extra edge per line, logged at info level). - **Armed once** in `pre_scan` (one position reference); before each line only the range is switched (`PSOWINDOW RANGE`), while the axis is outside both windows. +- **Shutter delay:** at the beamline DDG1 opens the shutter at the line trigger and + triggers DDG2 `shutter_to_open_delay` later (8 ms, measured from the first-point + deficit; `ddg1.set_shutter_to_open_delay()`). DDG2 then bursts `frames_per_line` + gates with period `exp_time` and width `exp_time - readout_time`, so `exp_time` is the + cell period and the readout is subtracted from the exposure. The trigger positions + lead the first cell by `v * shutter_delay`, so the first gate starts at the first cell + in both directions for any delay (no need for multiples of `exp_time`). A + `shutter_delay` scan argument is set on DDG1 for the scan and restored afterwards. + The PandA mimics this chain: shutter output at the edge, first gate + `shutter_delay` later (device config, default 8 ms), same gate period and width. - **Readout gap centred:** each gate (`exp_time - readout_time`) is centred in its cell, so forward and backward lines expose the same intervals. Without it the directions differ by `fast_step_size * readout_time / exp_time`. The readout time diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index 4b664527..a1fd9446 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -72,6 +72,7 @@ scans.aerotech_cont_grid( | `relative` | `True`: positions are offsets from the current X / Y position, and both axes return there after the scan | | `trigger_source` | `"ddg"` (default, beamline timing) | | `settle_time` | constant velocity time before the first exposure of a line, default 0.15 s (leave it) | +| `shutter_delay` | time from the line trigger to the first exposure, for the shutter to open. Default: the value of DDG1 (8 ms); a given value is set on DDG1 for this scan only | | `minimal_run_up` | `True`: run-up only the acceleration distance (at least 10 um), no settling. Faster lines, but the exposures are less evenly spaced; use the captured positions. Default `False` | The number of points per line is `ceil(|fast_end - fast_start| / fast_step_size) + 1`, @@ -161,6 +162,13 @@ centres from the nominal cells, backward minus forward offset, and the line time Measured 2026-10-29 with the PandABox as trigger (readout 0.2 ms), X acceleration 500 mm/s^2, X centred at 123.25, and the same nine scans in both run-up modes: scans 8486-8494 with the default run-up, 8501-8509 with `minimal_run_up=True`. + +!!! note "Without shutter delay" + In these scans the first exposure of a line started at the line trigger. At the + beamline DDG1 opens the shutter at the trigger and starts the exposures 8 ms later, + and the trigger comes correspondingly earlier on the line. These 8 ms give X more + time to settle after the ramp, so the minimal run-up results at the beamline should + be better than shown here. The target resolution is about 20 um; the positioning accuracy should be well below that. diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 95feadef..6125a386 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -297,3 +297,13 @@ def test_ignored_edges_logged_once_per_line_as_info(trigger): assert len(messages) == 2 assert "1 trigger edge(s) ignored in line 1" in messages[0] assert "1 trigger edge(s) ignored in line 2" in messages[1] and "2 since arm" in messages[1] + + +def test_shutter_to_open_delay_as_ddg1(trigger): + assert trigger.get_shutter_to_open_delay() == pytest.approx(8e-3) # DDG1 default + trigger.set_shutter_to_open_delay(0.012) + assert trigger.get_shutter_to_open_delay() == pytest.approx(0.012) + with pytest.raises(ValueError, match="Shutter-to-open delay"): + trigger.set_shutter_to_open_delay(0.2) + configured = PandaLineTrigger(name="p2", host="fake-panda", shutter_delay=0.0) + assert configured.get_shutter_to_open_delay() == 0.0 diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 7eec820f..a36ddc78 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -58,6 +58,7 @@ def _add_devices( signal_read_values={"ddg1": 0.0}, ) ddg1.get_shutter_to_open_delay = mock.MagicMock(return_value=2e-3) + ddg1.set_shutter_to_open_delay = mock.MagicMock() ddg1.set_trigger = mock.MagicMock() ddg2 = MockCustomDevice( "ddg2", @@ -80,6 +81,7 @@ def _add_devices( for method in ("configure_line", "arm", "disarm", "wait_lines_done"): setattr(pandatrig, method, mock.MagicMock()) pandatrig.get_readout_time = mock.MagicMock(return_value=0.02) + pandatrig.get_shutter_to_open_delay = mock.MagicMock(return_value=8e-3) devices.append(pandatrig) if capture: aerocapture = MockCustomDevice( @@ -563,3 +565,31 @@ def test_minimal_run_up_limit_message(v4_scan_assembler, device_manager): ) with pytest.raises(LimitError, match="minimal run-up"): scan.prepare_scan() + + +def test_panda_uses_its_shutter_to_open_delay(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, trigger_source="panda") + scan.prepare_scan() + line = scan._line + velocity = 0.5 / 0.1 + assert line["shutter_open_delay"] == pytest.approx(8e-3) + # trigger one shutter delay of travel before the first cell (gap: readout 0.02 s) + assert line["trigger_low"] == pytest.approx(-1.0 - velocity * 8e-3 + velocity * 0.02 / 2) + + +def test_ddg_shutter_delay_argument_is_set_on_ddg1_and_restored(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager, shutter_delay=0.012) + scan.prepare_scan() + assert scan._line["shutter_open_delay"] == pytest.approx(0.012) + scan.ddg1.set_shutter_to_open_delay.assert_called_once_with(0.012) + scan.fast_axis = mock.MagicMock() + scan.on_exception(RuntimeError("aborted")) + assert scan.ddg1.set_shutter_to_open_delay.call_args_list[-1] == mock.call(2e-3) + + +def test_ddg_without_shutter_delay_argument_leaves_ddg1_alone(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + scan.fast_axis = mock.MagicMock() + scan.on_exception(RuntimeError("aborted")) + scan.ddg1.set_shutter_to_open_delay.assert_not_called() -- 2.54.0 From 92be4440699700ecc06ebd64ab781d765a7b7b54 Mon Sep 17 00:00:00 2001 From: x01dc Date: Fri, 30 Oct 2026 09:42:40 +0100 Subject: [PATCH 46/52] config(sastt): SmarGon goniometer in sastt_aerotech_panda.yaml Coordinator + SHX/SHY/SHZ/CHI/PHI motors as in smargon.yaml (smargopolo REST API, host tt-smargopolo, port 3000). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- .../device_configs/sastt_aerotech_panda.yaml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml index af25697c..293719a7 100644 --- a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech_panda.yaml @@ -139,3 +139,73 @@ pandatrig: readoutPriority: baseline deviceTags: - sastt_aerotech + +############################################################ +# SmarGon goniometer (smargopolo REST API, host tt-smargopolo, port 3000), as smargon.yaml. +# All entries share one controller (same host/port). Referencing is a deliberate operator +# action after loading: dev.smargon.reference() +############################################################ + +# --- Coordinator (whole-goniometer ops: reference/mode/move_scs/diagnostics) ------ +smargon: + description: SmarGon coordinator (referencing, mode, coordinated moves) + deviceClass: csaxs_bec.devices.smargon.smargon.SmarGon + deviceConfig: + host: tt-smargopolo # cSAXS beamline smargopolo REST API + port: 3000 # REST API port (per maintainer). NB: :8080 is the web GUI, not the API + timeout: 5.0 + deviceTags: [goniometer, smargon] + enabled: true + onFailure: retry + readOnly: false + readoutPriority: baseline + softwareTrigger: false + +# --- SCS axis motors -------------------------------------------------------------- +# Limits from the MX-lab reference client (resources_2/Smargon.py) -- VERIFY per build. +# NOTE: SHZ is not centred on 0 (home ~18); PHI is continuous (client wraps to +# [-180,180) -- open question whether smargopolo wraps server-side). +sgx: + description: SmarGon SHX (sample X) + deviceClass: csaxs_bec.devices.smargon.positioner.SmargopoloMotor + deviceConfig: {scs_axis: SHX, host: tt-smargopolo, port: 3000, limits: [-2, 2], tolerance: 0.001} + deviceTags: [goniometer, smargon] + enabled: true + onFailure: retry + readoutPriority: baseline + +sgy: + description: SmarGon SHY (sample Y) + deviceClass: csaxs_bec.devices.smargon.positioner.SmargopoloMotor + deviceConfig: {scs_axis: SHY, host: tt-smargopolo, port: 3000, limits: [-2, 2], tolerance: 0.001} + deviceTags: [goniometer, smargon] + enabled: true + onFailure: retry + readoutPriority: baseline + +sgz: + description: SmarGon SHZ (sample Z) + deviceClass: csaxs_bec.devices.smargon.positioner.SmargopoloMotor + deviceConfig: {scs_axis: SHZ, host: tt-smargopolo, port: 3000, limits: [10, 22], tolerance: 0.001} + deviceTags: [goniometer, smargon] + enabled: true + onFailure: retry + readoutPriority: baseline + +sgchi: + description: SmarGon CHI (goniometer tilt) + deviceClass: csaxs_bec.devices.smargon.positioner.SmargopoloMotor + deviceConfig: {scs_axis: CHI, host: tt-smargopolo, port: 3000, limits: [0, 65], tolerance: 0.01} + deviceTags: [goniometer, smargon] + enabled: true + onFailure: retry + readoutPriority: baseline + +sgphi: + description: SmarGon PHI (goniometer rotation) + deviceClass: csaxs_bec.devices.smargon.positioner.SmargopoloMotor + deviceConfig: {scs_axis: PHI, host: tt-smargopolo, port: 3000, limits: [-180, 180], tolerance: 0.01} + deviceTags: [goniometer, smargon] + enabled: true + onFailure: retry + readoutPriority: baseline -- 2.54.0 From 94d7890aeec5bd5fce79effce8c9b83950bf3ceb Mon Sep 17 00:00:00 2001 From: x01dc Date: Fri, 30 Oct 2026 10:52:17 +0100 Subject: [PATCH 47/52] feat(smargon): readback monitor follows moves made outside BEC The motors only updated their readback at connect and during BEC moves, so a move in the smargopolo web interface never reached BEC (a read returned the stored value). The shared controller now polls /readbackSCS (all axes, one request) every monitor_interval (motor config, default 0.5 s, 0 = off) and publishes the axes that changed; it skips while BEC moves an axis, logs a failure once, and stops when the last axis is destroyed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/smargon/README.md | 9 +++ csaxs_bec/devices/smargon/controller.py | 72 ++++++++++++++++++++-- csaxs_bec/devices/smargon/positioner.py | 13 ++++ tests/tests_devices/test_smargon_device.py | 70 +++++++++++++++++++++ 4 files changed, 160 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/devices/smargon/README.md b/csaxs_bec/devices/smargon/README.md index 53a74a2b..10636115 100644 --- a/csaxs_bec/devices/smargon/README.md +++ b/csaxs_bec/devices/smargon/README.md @@ -79,6 +79,15 @@ share the controller. Motor `deviceConfig` keys: `scs_axis`, `host`, `port`, `li Simulation: `simulation: true` (+ optional `gradual_motion: true`) uses `FakeTransport`, no server required. +## Readback monitor (moves made outside BEC) + +The shared controller polls the SCS readback of all axes (`/readbackSCS`, one request) +every `monitor_interval` seconds (motor config, default 0.5 s, `0` disables) and +publishes the axes that changed. Moves made in the smargopolo web interface therefore +show up in BEC (`wm`, motor widgets, scan baselines) within about one interval. The +monitor skips a cycle while BEC itself moves an axis (that move publishes its own +readback), logs a connection failure once, and stops with the last destroyed axis. + ## Limits & coupled-axis faults SCS soft limits are **user-set** (per experiment), not hardware ranges: a SmarGon's diff --git a/csaxs_bec/devices/smargon/controller.py b/csaxs_bec/devices/smargon/controller.py index d5a7ca88..e2c00605 100644 --- a/csaxs_bec/devices/smargon/controller.py +++ b/csaxs_bec/devices/smargon/controller.py @@ -36,6 +36,11 @@ except Exception: # pragma: no cover logger = logging.getLogger(__name__) +# Readback monitor: polling interval (s) and the change below which an axis is not +# republished (SCS units, mm / deg) +DEFAULT_MONITOR_INTERVAL = 0.5 +MONITOR_DEADBAND = 1e-6 + class SmargopoloController(OphydObject): """One shared control plane for a smargopolo server (singleton per host/port). @@ -99,6 +104,10 @@ class SmargopoloController(OphydObject): self.transport = RestTransport(host=host, port=port, timeout=timeout) self._axes: dict[Axis, object] = {} # scs_axis -> SmargopoloMotor (registered by motors) self._connected = False + # readback monitor: follows moves made outside BEC (smargopolo web interface) + self.monitor_interval = DEFAULT_MONITOR_INTERVAL + self._monitor_thread: threading.Thread | None = None + self._monitor_stop = threading.Event() self._initialized = True # -- axis registry ----------------------------------------------------- @@ -107,16 +116,71 @@ class SmargopoloController(OphydObject): """Called by each :class:`SmargopoloMotor` so the controller can find it.""" self._axes[Axis(motor.scs_axis)] = motor + def unregister_axis(self, motor) -> None: + """Called when a motor is destroyed; the monitor stops with the last motor.""" + axis = Axis(motor.scs_axis) + if self._axes.get(axis) is motor: + del self._axes[axis] + if not self._axes: + self.stop_monitor() + + # -- readback monitor -------------------------------------------------- + + def start_monitor(self) -> None: + """ + Poll the SCS readback of all axes every monitor_interval seconds and publish the + axes that changed, so moves made outside BEC (smargopolo web interface) show up + in BEC. One REST call per cycle. monitor_interval 0 disables it. + """ + if self.monitor_interval <= 0: + return + if self._monitor_thread is not None and self._monitor_thread.is_alive(): + return + self._monitor_stop.clear() + self._monitor_thread = threading.Thread( + target=self._monitor_loop, name=f"{self.name}_monitor", daemon=True + ) + self._monitor_thread.start() + + def stop_monitor(self) -> None: + self._monitor_stop.set() + thread, self._monitor_thread = self._monitor_thread, None + if thread is not None and thread is not threading.current_thread(): + thread.join(timeout=2.0) + + def _monitor_loop(self) -> None: + failing = False + while not self._monitor_stop.wait(self.monitor_interval): + motors = dict(self._axes) + # a BEC move publishes its own readback while it polls to the target + if not motors or any(m.motor_is_moving.get() for m in motors.values()): + continue + try: + positions = self.get_positions() + except Exception as exc: # pylint: disable=broad-except + if not failing: + logger.warning(f"{self.name}: readback monitor failed: {exc}") + failing = True + continue + if failing: + logger.info(f"{self.name}: readback monitor recovered.") + failing = False + for axis, motor in motors.items(): + value = positions.get(axis) + if value is not None and abs(value - motor.readback.get()) > MONITOR_DEADBAND: + motor._update_readback(value) # pylint: disable=protected-access + # -- connection lifecycle ---------------------------------------------- def connect(self, timeout: float = 10.0) -> None: with self._lock: - if self._connected: - return - self.transport.connect(timeout=timeout) - self._connected = True + if not self._connected: + self.transport.connect(timeout=timeout) + self._connected = True + self.start_monitor() def close(self) -> None: + self.stop_monitor() with self._lock: try: self.transport.close() diff --git a/csaxs_bec/devices/smargon/positioner.py b/csaxs_bec/devices/smargon/positioner.py index 032e1079..c48361ea 100644 --- a/csaxs_bec/devices/smargon/positioner.py +++ b/csaxs_bec/devices/smargon/positioner.py @@ -46,6 +46,10 @@ class SmargopoloMotor(Device, PositionerBase): real coupled hardware limits. tolerance / poll_interval / move_timeout: Move-completion tolerance and polling cadence / timeout. + monitor_interval: + Interval (s) of the shared controller's readback monitor, which publishes + moves made outside BEC (smargopolo web interface). 0 disables it. All axes + share one monitor; the value of the last configured axis applies. egu: Engineering unit; defaults to the axis' natural unit (mm or deg). """ @@ -76,6 +80,7 @@ class SmargopoloMotor(Device, PositionerBase): tolerance: float = 0.001, poll_interval: float = 0.1, move_timeout: float = 60.0, + monitor_interval: float = 0.5, egu: str | None = None, device_manager=None, kind=None, @@ -111,8 +116,16 @@ class SmargopoloMotor(Device, PositionerBase): self.low_limit_travel.put(float(limits[0])) self.high_limit_travel.put(float(limits[1])) + self.controller.monitor_interval = float(monitor_interval) self.controller.register_axis(self) + def destroy(self) -> None: + """Unregister from the shared controller (its monitor stops with the last axis).""" + try: + self.controller.unregister_axis(self) + finally: + super().destroy() + # -- positioner interface ---------------------------------------------- @property diff --git a/tests/tests_devices/test_smargon_device.py b/tests/tests_devices/test_smargon_device.py index 8f10b5c5..d05f2fb5 100644 --- a/tests/tests_devices/test_smargon_device.py +++ b/tests/tests_devices/test_smargon_device.py @@ -16,6 +16,8 @@ def _reset_controller_singletons(): """Each test gets fresh controllers (the singleton registry is class-level).""" SmargopoloController._controller_instances.clear() yield + for controller in SmargopoloController._controller_instances.values(): + controller.stop_monitor() SmargopoloController._controller_instances.clear() @@ -196,3 +198,71 @@ def test_move_scs_aborts_on_controller_fault(): coord.controller.transport._mode = Mode.ERROR with pytest.raises(SmargopoloModeError): coord.move_scs(SHX=1e6, wait=True) + + +# -- readback monitor (moves made outside BEC) -------------------------------- + + +def _wait_for(condition, timeout=2.0): + import time + + deadline = time.time() + timeout + while time.time() < deadline: + if condition(): + return True + time.sleep(0.01) + return False + + +def test_monitor_publishes_moves_made_outside_bec(): + from csaxs_bec.devices.smargon.transport.base import Axis + + sgx = _motor("sgx", "SHX", monitor_interval=0.02) + published = [] + sgx.subscribe(lambda value, **_: published.append(value), event_type="readback", run=False) + # the smargopolo web interface moves the axis + sgx.controller.transport._positions[Axis.SHX] = 1.234 + assert _wait_for(lambda: sgx.readback.get() == pytest.approx(1.234)) + assert published and published[-1] == pytest.approx(1.234) + sgx.destroy() + + +def test_monitor_does_not_republish_unchanged_positions(): + sgx = _motor("sgx", "SHX", monitor_interval=0.02) + published = [] + sgx.subscribe(lambda value, **_: published.append(value), event_type="readback", run=False) + import time + + time.sleep(0.15) + assert published == [] + sgx.destroy() + + +def test_monitor_skips_while_bec_moves(): + from csaxs_bec.devices.smargon.transport.base import Axis + + sgx = _motor("sgx", "SHX", monitor_interval=0.02) + sgx.motor_is_moving.put(1) + sgx.controller.transport._positions[Axis.SHX] = 0.5 + import time + + time.sleep(0.15) + assert sgx.readback.get() != pytest.approx(0.5) + sgx.motor_is_moving.put(0) + assert _wait_for(lambda: sgx.readback.get() == pytest.approx(0.5)) + sgx.destroy() + + +def test_monitor_stops_with_the_last_axis_and_can_be_disabled(): + sgx = _motor("sgx", "SHX", monitor_interval=0.02) + sgy = _motor("sgy", "SHY", monitor_interval=0.02) + controller = sgx.controller + sgx.destroy() + assert controller._monitor_thread is not None and controller._monitor_thread.is_alive() + sgy.destroy() + assert controller._monitor_thread is None + + SmargopoloController._controller_instances.clear() + off = _motor("sgz", "SHZ", monitor_interval=0) + assert off.controller._monitor_thread is None + off.destroy() -- 2.54.0 From 6b49f9eaa3a2869494093ba560b971d86bd4335d Mon Sep 17 00:00:00 2001 From: x01dc Date: Fri, 30 Oct 2026 10:52:23 +0100 Subject: [PATCH 48/52] docs(smargon): monitor_interval in the config key list Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/smargon/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/csaxs_bec/devices/smargon/README.md b/csaxs_bec/devices/smargon/README.md index 10636115..ccac97e6 100644 --- a/csaxs_bec/devices/smargon/README.md +++ b/csaxs_bec/devices/smargon/README.md @@ -75,7 +75,8 @@ scans.grid_scan(dev.sgx, -1, 1, 11, dev.sgchi, 0, 60, 7, exp_time=0.1, relative= See `csaxs_bec/device_configs/smargon.yaml` (real) and `smargon_sim_session.yaml` (laptop simulation). All entries for one goniometer must share `host`/`port` so they share the controller. Motor `deviceConfig` keys: `scs_axis`, `host`, `port`, `limits` -`[low, high]` (**verify per build**), `tolerance`, `poll_interval`, `move_timeout`. +`[low, high]` (**verify per build**), `tolerance`, `poll_interval`, `move_timeout`, +`monitor_interval` (readback monitor, see below). Simulation: `simulation: true` (+ optional `gradual_motion: true`) uses `FakeTransport`, no server required. -- 2.54.0 From 6e2eec994200a949c8287167371f2f0dd3b7a45a Mon Sep 17 00:00:00 2001 From: x01dc Date: Fri, 30 Oct 2026 11:19:07 +0100 Subject: [PATCH 49/52] feat(aerotech): progress bar from the captured lines, statistics shown without MCS The IPython client prints client-info messages only from a scan's progress display. With trigger_source="panda" or None the scan registered none, so the end-of-scan statistics were sent but never shown (scan 8511). The position capture now publishes a ProgressSignal (lines read / num_points) and the scan uses it for the progress bar when there is no MCS card. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv --- csaxs_bec/devices/aerotech/a3200.py | 17 ++++++++++++++- .../scans/scans_v4/aerotech_cont_grid.py | 4 ++++ .../saxs/aerotech_stages/aerotech_stages.md | 4 +++- tests/tests_devices/test_a3200.py | 16 ++++++++++++++ .../test_aerotech_cont_grid_scan.py | 21 +++++++++++++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index 56d245fc..d7f4ff67 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -27,7 +27,7 @@ from ophyd import Component as Cpt from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait from ophyd.utils import LimitError, ReadOnlyError -from ophyd_devices import AsyncMultiSignal, PSIDeviceBase +from ophyd_devices import AsyncMultiSignal, ProgressSignal, PSIDeviceBase from ophyd_devices.utils.controller import Controller, retry_once, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected from prettytable import PrettyTable @@ -841,6 +841,7 @@ class A3200PositionCapture(PSIDeviceBase): kind="normal", doc="Positions latched at each trigger, in user units, one sub-signal per channel.", ) + progress = Cpt(ProgressSignal, doc="Lines captured, for the scan progress bar") def __init__( self, @@ -872,6 +873,8 @@ class A3200PositionCapture(PSIDeviceBase): self._signs = None # channel -> sign of the BEC motor, resolved in calibrate() self._n_triggers = 0 self._correlation_group = "monitored" + self._lines_total = 0 + self._lines_read = 0 super().__init__( prefix, name=name, scan_info=scan_info, device_manager=device_manager, **kwargs ) @@ -893,6 +896,11 @@ class A3200PositionCapture(PSIDeviceBase): frames = getattr(getattr(self, "scan_info", None), "msg", None) frames_per_trigger = getattr(frames, "frames_per_trigger", 1) if frames else 1 self._correlation_group = "monitored" if frames_per_trigger == 1 else "burst_group" + # one readout per line; the scan's num_points is the number of lines + self._lines_total = int(getattr(frames, "num_points", 0) or 0) if frames else 0 + self._lines_read = 0 + if self._lines_total: + self.progress.put(value=0, max_value=self._lines_total, done=False) def calibrate(self) -> dict[str, float]: """ @@ -983,4 +991,11 @@ class A3200PositionCapture(PSIDeviceBase): }, correlation_group=self._correlation_group, ) + self._lines_read += 1 + if self._lines_total: + self.progress.put( + value=self._lines_read, + max_value=self._lines_total, + done=self._lines_read >= self._lines_total, + ) return {key: value.tolist() for key, value in result.items()} diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index fda9378f..847c04aa 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -345,8 +345,12 @@ class AerotechContGrid(ScanBase): readout_time=self._line["readout_time"], ) + # progress bar in the client, which also prints the statistics at the end: the + # MCS frames at the beamline, otherwise the captured lines if self.mcs is not None: self.actions.add_scan_report_instruction_device_progress(self.mcs) + else: + self.actions.add_scan_report_instruction_device_progress(self.capture) self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) self._premove_motor_status = self.actions.set( self.motors, [self._line["line_start"], self.stepper_start], wait=False diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index a1fd9446..c6f8185b 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -80,7 +80,9 @@ the number of lines `ceil(|stepper_stop - stepper_start| / stepper_step_size) + The scan velocity can be up to 50 mm/s (`max_scan_velocity` of `aerox`). After the scan, also after an aborted one, X returns to its normal move velocity. -With `trigger_source="ddg"` the client shows a progress bar of the frames. +The client shows a progress bar (frames of the MCS card at the beamline, captured +lines otherwise) and, at the end of the scan, a short summary of the positioning +and the line times. To watch the measured positions while the scan runs, open the live plot of the exposure centres (Y versus X, forward and backward lines in two colours, updated diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index f09ed2e3..eacabc7b 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -497,3 +497,19 @@ def test_capture_without_motor_uses_controller_coordinates(): _, _, capture = _capture_setup() # DMMock without motors capture.calibrate() assert capture._signs == {6: 1, 2: 1} + + +def test_capture_progress_counts_lines(): + from types import SimpleNamespace + + x, _, capture = _capture_setup() + capture.scan_info = SimpleNamespace(msg=SimpleNamespace(frames_per_trigger=5, num_points=2)) + capture.on_stage() + assert capture.progress.get().value == 0 and capture.progress.get().max_value == 2 + capture.calibrate() + for target in (-1.0, 1.0): + capture.arm(5) + x.move(target, wait=True) + capture.read_line() + progress = capture.progress.get() + assert progress.value == 2 and progress.done diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index a36ddc78..09d6bfb4 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -593,3 +593,24 @@ def test_ddg_without_shutter_delay_argument_leaves_ddg1_alone(v4_scan_assembler, scan.fast_axis = mock.MagicMock() scan.on_exception(RuntimeError("aborted")) scan.ddg1.set_shutter_to_open_delay.assert_not_called() + + +def _progress_devices(scan): + return [ + instr["device_progress"][0] + for instr in scan.scan_info.scan_report_instructions + if "device_progress" in instr + ] + + +def test_progress_bar_from_the_mcs_at_the_beamline(v4_scan_assembler, device_manager): + scan = _assemble(v4_scan_assembler, device_manager) + scan.prepare_scan() + assert _progress_devices(scan) == ["mcs"] + + +def test_progress_bar_from_the_capture_without_mcs(v4_scan_assembler, device_manager): + # a progress display is what prints the statistics (client messages) at the end + scan = _assemble(v4_scan_assembler, device_manager, trigger_source="panda") + scan.prepare_scan() + assert _progress_devices(scan) == ["aerocapture"] -- 2.54.0 From b3bc8a27b2518a08c56bd6d186f6a88717c12622 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 3 Nov 2026 11:20:59 +0100 Subject: [PATCH 50/52] docs(aerotech): beamline values in the generated cont grid examples The scan server builds the Minimum/Full examples itself; ScanArgument(example=...) now gives aerox / aeroy, a 3.5 x 3 mm field at 40 um and 20 ms. trigger_device defaults to None (pandatrig with trigger_source="panda"), so the Full example no longer shows pandatrig next to the default "ddg". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NrH3poJsYBCTVq2Y9fuHZ --- .../scans/scans_v4/aerotech_cont_grid.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index 847c04aa..bcdc9da5 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -92,7 +92,7 @@ from typing import Annotated, TypedDict import numpy as np from bec_lib.device import DeviceBase from bec_lib.logger import bec_logger -from bec_lib.scan_args import DefaultArgType, ScanArgument +from bec_lib.scan_args import DefaultArgType, ScanArgument, Units # Checks raise LimitError / ValueError / RuntimeError, not ScanAbortion: the scan worker # only logs a ScanAbortion, the client then shows just "Scan ... was interrupted". Other # exceptions raise an alarm, so the message is shown in the client. @@ -104,6 +104,15 @@ from csaxs_bec.scans.scan_customization.scan_components import CsaxsBecScanCompo logger = bec_logger.logger + +class _DeviceExample(str): + """Device name for ScanArgument(example=...): rendered as dev. in the generated + doc examples, serialized as a plain string in the scan signature.""" + + def __repr__(self) -> str: + return f"dev.{self}" + + # DDG trigger sources (csaxs_bec.devices.epics.delay_generator_csaxs.TRIGGERSOURCE) DDG_EXT_RISING_EDGE = 1 DDG_EXT_FALLING_EDGE = 2 @@ -170,19 +179,19 @@ class AerotechContGrid(ScanBase): def __init__( # fmt: off self, - fast_axis: Annotated[DeviceBase, ScanArgument(display_name="Fast Axis", description="A3200 axis with continuous motion and PSO line trigger.")], - fast_start: Annotated[float, ScanArgument(display_name="Fast Start", description="Start position for measurement points of the fast axis.")], - fast_end: Annotated[float, ScanArgument(display_name="Fast End", description="End position for measurement points of the fast axis.")], - fast_step_size: Annotated[float, ScanArgument(display_name="Fast Step Size", description="Distance travelled by the fast axis during one exposure.")], - stepper_axis: Annotated[DeviceBase, ScanArgument(display_name="Step Axis", description="Step axis of the grid scan, stepping through the lines.")], - stepper_start: Annotated[float, ScanArgument(display_name="Step Start", description="Start position of the step axis.")], - stepper_stop: Annotated[float, ScanArgument(display_name="Step Stop", description="End position of the step axis.")], - stepper_step_size: Annotated[float, ScanArgument(display_name="Step Step Size", description="Step size of the step axis in units of the motor.")], - exp_time: DefaultArgType.ExposureTime, + fast_axis: Annotated[DeviceBase, ScanArgument(display_name="Fast Axis", description="A3200 axis with continuous motion and PSO line trigger.", example=_DeviceExample("aerox"))], + fast_start: Annotated[float, ScanArgument(display_name="Fast Start", description="Start position for measurement points of the fast axis.", example=-1.75)], + fast_end: Annotated[float, ScanArgument(display_name="Fast End", description="End position for measurement points of the fast axis.", example=1.75)], + fast_step_size: Annotated[float, ScanArgument(display_name="Fast Step Size", description="Distance travelled by the fast axis during one exposure.", example=0.04)], + stepper_axis: Annotated[DeviceBase, ScanArgument(display_name="Step Axis", description="Step axis of the grid scan, stepping through the lines.", example=_DeviceExample("aeroy"))], + stepper_start: Annotated[float, ScanArgument(display_name="Step Start", description="Start position of the step axis.", example=-1.5)], + stepper_stop: Annotated[float, ScanArgument(display_name="Step Stop", description="End position of the step axis.", example=1.5)], + stepper_step_size: Annotated[float, ScanArgument(display_name="Step Step Size", description="Step size of the step axis in units of the motor.", example=0.04)], + exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Exposure time in seconds", units=Units.s, ge=0, example=0.02)], relative: DefaultArgType.Relative = False, trigger_source: str | None = "ddg", trigger_edge: str = "rising", - trigger_device: str = "pandatrig", + trigger_device: str | None = None, shutter_delay: float | None = None, settle_time: float = 0.15, minimal_run_up: bool = False, @@ -207,7 +216,7 @@ class AerotechContGrid(ScanBase): relative (bool): Whether the positions are relative to the current position. Default is False. trigger_source (str | None): "ddg" (beamline, DDG1/MCS), "panda" (PandABox line trigger) or None (no trigger hardware). Default is "ddg". trigger_edge (str): DDG1 external trigger edge, "rising" (opto output, J7-6/7) or "falling" (open collector / differential output). Default is "rising". - trigger_device (str): Name of the PandaLineTrigger device for trigger_source="panda". Default is "pandatrig". + trigger_device (str | None): Name of the PandaLineTrigger device for trigger_source="panda"; ignored otherwise. Default is None ("pandatrig"). shutter_delay (float | None): Delay from the line trigger to the first exposure (time for the shutter to open) in s. Default: the value of the trigger hardware (DDG1 / PandA shutter-to-open delay, 8 ms), 0 without trigger hardware. With "ddg" a given value is set on DDG1 for this scan and restored afterwards. settle_time (float): Run-up at constant velocity before the trigger position, in s. Default is 0.15. minimal_run_up (bool): Run-up / run-out only the ramp distance v^2 / (2 * acceleration), at least 0.01, without the 2x margin and settle_time: faster lines, but the exposures start right after the ramp while the fast axis may still oscillate, so the captured positions are less evenly spaced. Default is False. @@ -276,6 +285,7 @@ class AerotechContGrid(ScanBase): self.ddg2 = devices["ddg2"] self.mcs = devices["mcs"] elif trigger_source == "panda": + trigger_device = trigger_device or "pandatrig" if trigger_device not in devices: raise ValueError(f"PandA line trigger device '{trigger_device}' not found.") self.panda = devices[trigger_device] -- 2.54.0 From 353d6bbb01d26bba3c2cbd391f38d17abc5c5f8e Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 3 Nov 2026 11:21:00 +0100 Subject: [PATCH 51/52] fix(aerotech): park the PSO window once the line is triggered At the beamline (DDG1 trigger) every line started two bursts: the window was switched to the next line when the axis passed trigger_high, just after the DDG1 cycle (as long as the travel trigger_low -> trigger_high), and the edge started a second burst. MCS and detectors counted twice the frames, so their frames no longer matched the captured positions (progress bar done at half the scan). The window is now moved to a parking range beyond the line end as soon as the axis has entered it, while DDG1 / PandA run the line and ignore edges; the next line's window is set beyond the exposures as before, outside both windows. The per-line MCS check also fails on more frames than the line has. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NrH3poJsYBCTVq2Y9fuHZ --- .../scans/scans_v4/aerotech_cont_grid.py | 41 ++++++++-- .../test_aerotech_cont_grid_scan.py | 75 ++++++++++++++++++- 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py index bcdc9da5..6a8c089e 100644 --- a/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py +++ b/csaxs_bec/scans/scans_v4/aerotech_cont_grid.py @@ -23,8 +23,13 @@ line. The axis never leaves the window during a line: with a closed window the e coincides with the end of the exposures, and encoder noise at the exit edge (seen 2026-10-29: 2 extra edges in 11 lines) could start an extra line in the trigger receiver. Noise at the entry edge falls into the running line, which DDG and PandA ignore. The window -is armed once per scan (one position reference) and only its range is switched before each -line, while the axis is outside both windows; the trigger receiver stays armed. +is armed once per scan (one position reference) and only its range is switched, the trigger +receiver stays armed. As soon as the line is triggered, the window is moved to a parking +range the axis never reaches: the output goes low while DDG1 / PandA run the line and ignore +edges. Switching from the line's window to the next one beyond the exposures instead gave a +second trigger per line at the beamline (2026-11-03, DDG1: the switch came just after the +DDG1 cycle, which lasts as long as the axis needs from trigger_low to trigger_high). The next +line's window is set once the axis has left the exposure range, outside both windows. The scan is hardware triggered: the MCS card and the detectors are armed once at stage for all num_lines x frames_per_line frames (num_points = lines, frames_per_trigger = frames per @@ -429,14 +434,19 @@ class AerotechContGrid(ScanBase): ): """ Run one line: arm the position capture, move the fast axis through the PSO window, - start the step to the next line once the fast axis has left the exposure range, - then check that the line was triggered and read the captured positions. + park the window once the line is triggered, start the step to the next line once the + fast axis has left the exposure range, then check that the line was triggered and + read the captured positions. """ line_start_time = time.time() self._line_starts.append(line_start_time) self.capture.arm(self.frames_per_line) move_status = self.actions.set(motors, positions, wait=False) next_line = line_index + 1 if line_index + 1 < self._line["num_lines"] else None + # the PSO output goes low while the trigger receiver runs the line and ignores edges + while not move_status.done and not self._past_trigger(line_index): + time.sleep(STEP_POLL_INTERVAL) + self.fast_axis.set_line_trigger_window(*self._parking_window()) while next_line is not None and not move_status.done: if self._past_exposures(line_index): break @@ -498,6 +508,19 @@ class AerotechContGrid(ScanBase): def _is_forward(self, line_index: int) -> bool: return line_index % 2 == 0 + def _past_trigger(self, line_index: int) -> bool: + """The fast axis has entered the PSO window of the line (the line is triggered).""" + position = self.fast_axis.readback.get() + if self._is_forward(line_index): + return position > self._line["trigger_low"] + return position < self._line["trigger_high"] + + def _parking_window(self) -> tuple[float, float]: + """PSO window beyond the line end, never reached by the fast axis.""" + line = self._line + span = line["line_end"] - line["line_start"] + return line["line_end"] + span, line["line_end"] + 2 * span + def _past_exposures(self, line_index: int) -> bool: """The fast axis has left the exposure range of the line (beyond its trigger position).""" position = self.fast_axis.readback.get() @@ -603,7 +626,9 @@ class AerotechContGrid(ScanBase): """ After line line_index the MCS card must have advanced to the last channel of that line (the last channel of the scan is closed in its complete). Catches a missing - line trigger right after the line instead of at the end of the scan. + line trigger right after the line instead of at the end of the scan, and an extra + one: the detectors would count it as well, their frames would no longer match the + captured positions. """ expected = (line_index + 1) * self.frames_per_line - 1 deadline = time.time() + timeout @@ -615,6 +640,12 @@ class AerotechContGrid(ScanBase): " DDG1 EXT wiring and trigger_edge." ) time.sleep(poll) + if channel > expected: + raise RuntimeError( + f"MCS card at channel {channel} after line {line_index}, expected" + f" {expected}: more frames than the line has, the trigger receiver was" + " triggered more than once. The detector frames do not match the positions." + ) def _line_duration(self) -> float: line = self._line diff --git a/tests/tests_scans/test_aerotech_cont_grid_scan.py b/tests/tests_scans/test_aerotech_cont_grid_scan.py index 09d6bfb4..6e59cdd0 100644 --- a/tests/tests_scans/test_aerotech_cont_grid_scan.py +++ b/tests/tests_scans/test_aerotech_cont_grid_scan.py @@ -348,13 +348,21 @@ def test_snake_scan_core(v4_scan_assembler, device_manager, nth_done_status_mock ([stepper], [2.0]), ([fast], [line["line_end"]]), ] - # the PSO window is armed once in pre_scan, not per line; only its range is switched + # the PSO window is armed once in pre_scan, not per line; only its range is switched: + # parked once each line is triggered, then the window of the next line scan.fast_axis.arm_line_trigger.assert_not_called() scan.fast_axis.disarm_line_trigger.assert_not_called() span = line["line_end"] - line["line_start"] + park = mock.call(line["line_end"] + span, line["line_end"] + 2 * span) backward = mock.call(line["line_start"] - span, line["trigger_high"]) forward = mock.call(line["trigger_low"], line["line_end"] + span) - assert scan.fast_axis.set_line_trigger_window.call_args_list == [backward, forward] + assert scan.fast_axis.set_line_trigger_window.call_args_list == [ + park, + backward, + park, + forward, + park, + ] def test_panda_lines_are_checked(v4_scan_assembler, device_manager, nth_done_status_mock): @@ -489,8 +497,15 @@ def test_step_starts_when_fast_axis_left_the_exposures( return nth_done_status_mock(resolve_after=1) scan.actions.set = mock.MagicMock(side_effect=set_side_effect) - # beyond the high trigger position after forward lines, below the low one after backward - scan.fast_axis.readback.get.side_effect = [line["line_end"], line["line_start"]] + # per line: inside the window (triggered), then beyond the exposures (forward: beyond + # trigger_high, backward: below trigger_low); the last line only parks the window + scan.fast_axis.readback.get.side_effect = [ + line["line_end"], + line["line_end"], + line["line_start"], + line["line_start"], + line["line_end"], + ] scan.scan_core() @@ -502,12 +517,64 @@ def test_step_starts_when_fast_axis_left_the_exposures( ("fast", line["line_end"]), ] span = line["line_end"] - line["line_start"] + park = mock.call(line["line_end"] + span, line["line_end"] + 2 * span) assert scan.fast_axis.set_line_trigger_window.call_args_list == [ + park, mock.call(line["line_start"] - span, line["trigger_high"]), + park, mock.call(line["trigger_low"], line["line_end"] + span), + park, ] +def test_window_parked_once_line_is_triggered( + v4_scan_assembler, device_manager, nth_done_status_mock +): + # the window must be parked while the trigger receiver runs the line (it ignores the + # edge then), not at the end of the exposures: that gave a second trigger per line + scan = _ready_for_core( + _assemble(v4_scan_assembler, device_manager, trigger_source=None), nth_done_status_mock + ) + line = scan._line + status = mock.MagicMock() + status.done = False + scan.actions.set = mock.MagicMock(return_value=status) + windows = [] + scan.fast_axis.set_line_trigger_window.side_effect = lambda *args: windows.append(args) + # forward line: run-up (before the window), entered, then beyond the exposures + positions = [line["line_start"], line["trigger_low"] + 1e-3, line["trigger_high"] + 1e-3] + readbacks = iter(positions) + + def readback(): + value = next(readbacks) + readback.seen.append(value) + return value + + readback.seen = [] + scan.fast_axis.readback.get.side_effect = readback + status.wait.side_effect = lambda *a, **k: setattr(status, "done", True) + scan._line["num_lines"] = 2 + + scan.at_each_point( + motors=[scan.fast_axis], positions=np.array([line["line_end"]]), line_index=0 + ) + + span = line["line_end"] - line["line_start"] + assert windows == [ + (line["line_end"] + span, line["line_end"] + 2 * span), + (line["line_start"] - span, line["trigger_high"]), + ] + assert readback.seen == positions + + +def test_ddg_extra_line_trigger_aborts(v4_scan_assembler, device_manager, nth_done_status_mock): + scan = _ready_for_core(_assemble(v4_scan_assembler, device_manager), nth_done_status_mock) + scan.mcs = mock.MagicMock() + scan.mcs.current_channel.get.return_value = 9 # two bursts of 5 frames in line 0 + with pytest.raises(RuntimeError, match="triggered more than once"): + scan._wait_mcs_frames(0, timeout=0.1, poll=0.02) + + @pytest.mark.parametrize( ("attribute", "device_cls"), [ -- 2.54.0 From cb1dbdb0033620f2b83ff18d85b9bd9e12b3c271 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 3 Nov 2026 11:37:12 +0100 Subject: [PATCH 52/52] config(sastt): aerotech beamline setup in main.yaml, PandA config to legacy main.yaml loads sastt_aerotech.yaml instead of ptycho_lamni.yaml. aeroz and aerorot are commented out in sastt_aerotech.yaml (not used at the beamline now). sastt_aerotech_panda.yaml (PandA test setup without beamline timing) moved to legacy/. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NrH3poJsYBCTVq2Y9fuHZ --- .../{ => legacy}/sastt_aerotech_panda.yaml | 0 csaxs_bec/device_configs/main.yaml | 7 +- csaxs_bec/device_configs/sastt_aerotech.yaml | 92 +++++++++---------- docs/developer/aerotech_stages.md | 4 +- 4 files changed, 53 insertions(+), 50 deletions(-) rename csaxs_bec/device_configs/{ => legacy}/sastt_aerotech_panda.yaml (100%) diff --git a/csaxs_bec/device_configs/sastt_aerotech_panda.yaml b/csaxs_bec/device_configs/legacy/sastt_aerotech_panda.yaml similarity index 100% rename from csaxs_bec/device_configs/sastt_aerotech_panda.yaml rename to csaxs_bec/device_configs/legacy/sastt_aerotech_panda.yaml diff --git a/csaxs_bec/device_configs/main.yaml b/csaxs_bec/device_configs/main.yaml index 7a45e202..944a0c1f 100644 --- a/csaxs_bec/device_configs/main.yaml +++ b/csaxs_bec/device_configs/main.yaml @@ -25,14 +25,17 @@ detectors: #sastt: # - !include ./sastt.yaml +sastt: + - !include ./sastt_aerotech.yaml + # flomni: # - !include ./ptycho_flomni.yaml #omny: # - !include ./ptycho_omny.yaml -lamni: - - !include ./ptycho_lamni.yaml +# lamni: +# - !include ./ptycho_lamni.yaml # user setup: # - !include ./user_setup.yaml diff --git a/csaxs_bec/device_configs/sastt_aerotech.yaml b/csaxs_bec/device_configs/sastt_aerotech.yaml index 9bf4f75e..861a99de 100644 --- a/csaxs_bec/device_configs/sastt_aerotech.yaml +++ b/csaxs_bec/device_configs/sastt_aerotech.yaml @@ -67,52 +67,52 @@ aeroy: readoutPriority: baseline deviceTags: - sastt_aerotech -aeroz: - description: Aerotech Z, ATS20010 linear stage (no PSO input) - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor - deviceConfig: - axis_Id: Z - axis_nr: 1 - acceleration: 10.0 - host: pc14643.psi.ch - port: 8000 - velocity: 5.0 - egu: mm - tolerance: 0.005 - limits: - - -100 - - 100 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech -aerorot: - description: Aerotech U, ABRT-200 rotation stage (UU follows as gantry slave) - deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor - deviceConfig: - axis_Id: U - axis_nr: 3 - pso_input: 7 - pso_counts_divider: 40 - acceleration: 2000.0 - host: pc14643.psi.ch - port: 8000 - velocity: 30.0 - egu: deg - tolerance: 0.001 - limits: - - 0 - - 0 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline - deviceTags: - - sastt_aerotech +# aeroz: +# description: Aerotech Z, ATS20010 linear stage (no PSO input) +# deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor +# deviceConfig: +# axis_Id: Z +# axis_nr: 1 +# acceleration: 10.0 +# host: pc14643.psi.ch +# port: 8000 +# velocity: 5.0 +# egu: mm +# tolerance: 0.005 +# limits: +# - -100 +# - 100 +# sign: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: baseline +# deviceTags: +# - sastt_aerotech +# aerorot: +# description: Aerotech U, ABRT-200 rotation stage (UU follows as gantry slave) +# deviceClass: csaxs_bec.devices.aerotech.a3200.A3200Motor +# deviceConfig: +# axis_Id: U +# axis_nr: 3 +# pso_input: 7 +# pso_counts_divider: 40 +# acceleration: 2000.0 +# host: pc14643.psi.ch +# port: 8000 +# velocity: 30.0 +# egu: deg +# tolerance: 0.001 +# limits: +# - 0 +# - 0 +# sign: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: baseline +# deviceTags: +# - sastt_aerotech aerocapture: # always used by aerotech_cont_grid (positions at the start and centre of each exposure) description: Aerotech position capture (Npaq DATAACQ, latched on the detector trigger at UINT1) diff --git a/docs/developer/aerotech_stages.md b/docs/developer/aerotech_stages.md index a9bd2951..b12d22d4 100644 --- a/docs/developer/aerotech_stages.md +++ b/docs/developer/aerotech_stages.md @@ -10,7 +10,7 @@ Developer notes for the Aerotech A3200 integration and the continuous grid scan | Scan | `csaxs_bec/scans/scans_v4/aerotech_cont_grid.py` | | PandABox line trigger (test setup) | `csaxs_bec/devices/panda_box/panda_line_trigger.py` | | Simulation | `csaxs_bec/devices/sim/sim_a3200.py` | -| Configs | `device_configs/sastt_aerotech.yaml`, `sastt_aerotech_panda.yaml`, `simulated_omny/simulated_sastt_aerotech.yaml` | +| Configs | `device_configs/sastt_aerotech.yaml`, `legacy/sastt_aerotech_panda.yaml`, `simulated_omny/simulated_sastt_aerotech.yaml` | | Hardware notes, wiring, channel table | `csaxs_bec/devices/aerotech/README.md` | ## Hardware and interface @@ -160,7 +160,7 @@ along the line, and line period with the motion and readout parts. ## PandABox as line trigger (test setup) -Without beamline timing, `sastt_aerotech_panda.yaml` adds `pandatrig` +Without beamline timing, `legacy/sastt_aerotech_panda.yaml` adds `pandatrig` (`omny-panda.psi.ch`) as a DDG stand-in. It stays armed like the DDG, and every accepted line trigger edge produces: -- 2.54.0

fIR#419 zmx+3n`B{*xCTzSkTWvnv7ParRpLhRRiRjoB`(D4mzuE=MfDojbY*@v1f4Z-6g7n$R1`CtufBvt;=r|dL_pz;P=blNVqv&7dc7P z&a7RCo#(l27#3TRVh)!aoAB?qxvx+5v5=>vW)SA-1F3VEU~{}WsUtbF$aT_TaXzyr zm?eChn~Z^r_zrr*&b=*j5>^{MeJXiem%}VaZlf$1oz-Po5Iem?er(mbJSBLL?F`;) zP$6?CAA!;P_E9VGeeTCkxSW5cPJHVh{^FQojCDHF#nF%t5-i>)(x}pKq_V`xU^2(r zmOR=NFkoV6N~huu6^9xv_og$CamrrJUYpD5-PL26y42Xc2tm#TjK_V>uC=2E4CI|& zegk`GPYs+0*BoQQFNgtBT{-BGIT#AHM3IyZ8}l zaUt|laIHD_zgrcdd4HK^E$6>`SH#fkGn{M{qAX0^hq|D>OxZhELT@I*Ktien9OFtv zaP2SZuDy75nr6S*JA@eQKo^J6_Q|{2Ltu)<@~SU|8ga_k7=Hf|(1+NMPfl`jakj z`PzFL2##vjZ;d+UyPc~Mp2fh?O{H%p=2VNFEDS6k2HIunJXou@i0@@V8a{Ivl$jFi zZs}#Z?|?A^YD!Cg&b2xbVBp4nhz6`-yKslkvtr@PQH#LU$tRp( zT;i`}nD;*WcPp;%UFai?-5$wF9gTp7L5;~SXu;%LXMV0P6C&ZueCxo#fO`pYlN+7a z=>cmKnkBFw#wAV@BcrpQN-&CWZ4!Ydvr(4~_YZnm5+hGT%ho7ILx8`&+w3=T$NFR% zMn1zrs7P2+g=(Y6`doYrI7O4=Y${J9Oj}8F(A1Rj*obHv6YPbw`MfRil{J!=e`obx zLKoZ8KDH<`=3g}q$vmz>52ZUSA-#uOp0_ji5H-7BlNr`VHD)9JDem(cpw1svF!9iq zFz?hD{FZOkChsWN#(e-27*Uu0bJZ;W&+LOICE&eP=PGL$z}J<0I->p4jCTAeceFeX8&YH69y}WRI? zpleFZ{rqfxLjwJ&AvZs}#UVH0l?CssT~CazCy_(D6Y#@!cq`x>o-D5cQxN<5Zzrm~ zZl05zyZ&InG>Q%h;Z-@gAui=gU%5s-KClHX!o z=;Zij8KhawPv;oW6fdGZALTqHa$mVOmdgc*-Iwbv(NfK`vBNnyIK=Z*`G1xov$L~z z0~Z6?9QUbrZad~$3-8e9D#Njj4BXs~b9ON=k4&r*JYu#j%Wwr@x5EA+Xad8I+Td+) z5Llvw4XcQbM8F)TJ2VV9LEB**vKuu6Xg zWcNa#2LOy*!dt8!l}pLY{@}whxbyz?I_vMS{(I=&Q|a=u9o5Oo;}Ln^ZNj9@qSmo# z#Qd-IM$OMGrm$-_8Y(f*)co@DL-QE#GMC9FO8}ns7p~G5=j3mbm6w{pXe4ywJZpjI zg__q5SIyr=!xY(YmL^p)b7v3Rcs0ELzWO4r@`l#U;L~GqnN+;>qK97D}L7% zQ;DzM;WIQR(8x-oC=OZHpi8Bg$Bk=Q>p^9~B~$O3P!8Qoam_8a(F}G4_RF35Ena7o zXU;2zS80Ug#m!Hx{~C1>`BS_^mp&Qf^h@e2hTtds)Qv zRg^?zH~SFFE>zD~K6(;?zqV%K+ex03X!?@02*ZGHN(u8pY0U~#j3c6G&`Ou^R3pcN ze0$#Ry{;Lr?Z|s+W;qbRi@0nKLFq~@fUd=ISk0g>+RK`PEVW|Nwc_>G9y+eogjv$r zfL#x<$pJCw8`IRq`es*P)x6Zo@?TiscI>D}S^K#2XMtp#NAXUX*K6R3_5Idsuxm?T zf3BZnYbp3R)nUW+6rD>mT9HyhruR$TtOa?lnYyEmZZWvUQLH^=_4)+EVXR)EMP!O~ z`%c*RyT@lARN-f(?%*C0N;2;o!5xqg&TGZ2k+DK!=0NQC?gOx~S7V0z=3%Ui0U=d}kU9;-+ZrJRr!X$ps$ zVlHugZ55X;-KrsQ-_DU;GQ@{yD!kL}n|9@SA(u6|oGO%+v_m6bB!|o0$><7%-XHFRm&dzbr)mMwheo&HbS6J&`|R)eB051&FFVqeC?}1SHO%I8nExwyz~g z&ER2J`=aZ7PD4eQ0#j{M@EfsVQE(3k6#Shcj$9`ry^UF6hGPz$(Jx@Ie9S9@kBa#f)jq`NksC` z4_=bAP{Af`kh1v*Pjm_D#*?chr!o{3*TtGj)Q*uYNG9HB(O<}jz3+y{j$6|=P;|avoJT2s3O0eNJrz4&qVG$7cy$jW>D*ut1GfvVE}L7pGrdo#_i*L z9Ys-iQmGMEoES=oQilmRl1bFBFG^WUe46IxF-waR3oNgFc)42&U5&fnQ25y_%2~oY z<}`sRsM%NSaEmNOZ6#oeMZC=XdcO99_&c)K!76hM3BD?(3!!SA3A=TWHjQ91oM{p7 z-{m8@d>7e&*D!44d0+h|I?8jH1NyoqtB$5X$G*4e~}A){-K?!e&$=d8~2S> z8`tG;Z@j8}Q@A!mvYonLd6D=vF4~$K@A+$&1iVK@whf>l9ypC3|5<6Q8ufl1iku6? z+ul=JPg3z&d~EKC2(D!RKKo$gxIRp_*_PAwIwIcrvZ9>MjP%4@OK50~#DPIp&DTvIu#Qdd~gc!o8m`nZD7U z__L^2U<(^UP^f;}*Dwg@WGZ(ylKB-~r=3BH8XWAk-FNW*uTiP0BDk>V;4Bq!a^K{-8sFCt(^j|7xGn$ri zR&OkgJXe!hsH;Q)!$fxLv_x~Vgp5VSN|vKv6soPXcou8Y%QDI3U_|#Y1=q_E>Zn-+ zo`Av&iopn1oczC4y<)M z^RHu@&`Go&dUL)%I1p=!EL7$PHzMzSn(QHq)^)v1EGG%D+VKtW&a10sV>6w}*UygE5DbBap(DxlT-#HDa z=D0C4Z~ExwN&WC8G7I~%{sJ~5GC_YVerc(jI_TkhYbW~2>#8$hopzuuX4Y91;%aH z9oS+6rbw#o7XhMCv9&kSj@wD$Ek8~t*>sJ;pJIhRx67BWrL45#@Gi9}Sk}bq-1zQF z+xVU=_`WbZB&r0#!02cCs_)Zb$uqE6_O56LuVW?~bG75q4q)c{P^E^>zw77RxlY5L zR&TLE6VJm^c8V*FiYXuNS5b@IdF%=L{M>S6Ct%W5U4f})KvT}pkIU&S*(0!bI53U< zddT*3d$H@6W1013askfk&8qJMS+k?il&$AodQ~Pc2cHlLsf5U0%v%A+=2mM-_-D&M zivL5?S%y`?b=_J7m5}bPO-grnOLrsP-7Vd*=|&JxLb^euyAc6Fx>G_r&fVsDmR)%*N!bQChUJ(bFfbot#kPV zp0c-0EBgHCH)QGba!V@O40Y-93T>oo{0a6!3voj-;An)qj4_KmGOtAIr(~}VcA7Bg zguwn?zUsi0O-gh4frjgI-tinJv_c>&4{c_Q92iegJ*7xdo3p4*3j{A=r=1IU+ z+r$-p2a5-0$Mwk9y!ZX*%ABz3201Ci>q+TmkE*KICwdSMC2st0^BH+S1soSmY{jGS z>RcuFL=Z|PbBQtbZ{`objHM@PPZ*^9Zf0|Xua;q>nMA{NZFAWr<69;NgQsl0kbQ`V zY;$Q!ScRNUS|SyK*WIq z3az&mU}`pn71g@sV%E_jFTXsUP2EOD-DHuc%#cT#}e{h&VEkCt>Jy=r&I zKnwXJgjK64!*RZdf?&2$yk%=Ll?`vO5B?>1rGgz(6n0S{rf)=#o%<~ti|{fAC#;+? zVlOL-K~#&c+iCM9I#r0ssKu`@eG!@aR`;02O>cirBw&BItLoZr zLiDVi30^SxkQVyMwCIyT(9);KA2M#y#0$Li-&D+^9_?a*N@y=@$gjtw;wRXLCG7YG ziD-%{8c^?vc=F}O@A=F^ez8A(JPoR9Uanj4@vVTrw_RF8uZ6nh)k7ty&zJF?tnj_q zI5`yS?a9YVXSP$GbU1l?U#vu>6Mf#pUwxltt$dxH*Uf3#lJ#HYVg8dB%12< zB@c%h7?3BgiN(&@?HA40^771m0?&}2^*5_K`C!7h(Emu*fFy z#4Mw#iv3!KvWd7>lH8*4ihS<)PZO%}&>v*9q}h&jmBK@5%h$4n#alk|8uB3Q$&qhZ z`SE6eaqb*Vxin?@)0eC(weoh^z}T~d@*jra5Mut_fNIxaUhp( z6x5A>Uu@mRg!tNT%wug;_yQBQS25^t zFw)xuqrdEY*w8`66yRYMuRjWEsc`9dEJ-zW*>%qoFN{V0fV1Xe?mlUU#bK}FxUJg@ zOA`S9ys-RicZeXl_1~O7<|En15aFN}|H5^qDZ25oAAfOFzBkD*FiUJ5vK-!1T%3Fy z{n#9;f03hMzW#6$D4Ya29rx_OG?+_w5H<=F0oh<)#`vqx#H5~{p8JQDC&87w&}Sh504L}x%S)b-0IxIn0P|AX43EP6fYtFVtt?!k54>5rTEhg z_f((MAWY~ryCChJ1M8&3w57k)@Rek#YmW=Sj)D1)++u;bFMzmGj1*DOFEo#rjq@yg zRTL;J92S!&q2tP|Y&;cMLo>umHb(&J!JGXJ)1~i@X}PAUGknP~Zj#%#4c^Z~I_?TO zmX-RQ&za2nO2tuX((qE z27g8W+tFjpDqBb>yeXGNs7RS!TVd@dnOU+xb^zs2HbW3s zyY7rD_nzYnxgVL3x*o_UE}rMfBR{%@d1IMP^sd3%rJ|d&M`zhdmO1^c**K3D|0b0f z2U3WZ_>R;evSZQdh36DXK)Ut@p4=(}BQ<)Wz?mQy@eN7WZ@t9IYtGm`bDUou_~e3$B0g1A#5p>^j)^qpkEr zljVrcb`v|D@*(Q#=4whdbnm=7j9CphhosDYxl0Vuqz$xzdAC_{+|kWN(dW0)N{qC3 z0V2gm%GL|@#Hbo+vQp}`&QN@R#WGgK9HL|m<f@)k3%punGQb-|!-hLtCc~oFf-bjJzx5;oZLyQ~^WA7TB z6_d!T_r>0%9}8*ycEw58#Yt}`ZwyoOQ763D%mZMs{H6mk>vdkA!>b3)WGMLS^dw;a zWh-~8vpqdK_*-jxT3ddq21^$6HFk%U19s9yKiZv~NnQTUBDVROiUK+D6y9jxAQhl6 z#(gaz8^MU*nW+{0-8e8R79!->mDyfOqrg?N<^=;3wmSp97T0@^Etmuvxp~X-q1+A! zTY__MRb>c3$sM1n2W}TyP@4ABEk7kd;!z46%zUid|`{c;p>p@;4&&Dtw`5gKfe4vj-1eEnxdO@QpHW zvcw$PWoA^KwQ*N%ediCqZo5M{4~s}pu_avrxxjUvt>ZPSF*E?*tDU=}kbYTPL*u_@ zW*$+&W4~zHPS0wOJVW9|y3jpV!MLKd`S)tFTw1w(D-~RXU-`mlgk z#=xp+Lq39G%b&2*G$$W%s_H9ib*Uery60tBx90`kasb!tNG*1Tm5QB5s_d|klFb#5 zn-~?fqHyu5Fm;@-l2KyS10fa&R%6~sw5)8ZRqw2HRpe|BVZ1eNV-sFr zAKMqpN$?9~MkO&cd|=IpIYm8L9G7ST87C|m2`&}$i2wfriK!Vd-VD%w@-Zfeu~>Dj zLWknTSg~FgC-nS9{@W`Yb^dQWuHfrE#zqC=$b|i`D^=^|t0eNK@K3zbt64H1vEW3= zhCOks@?p(ZL>=S{xJp?1N%b!?nCa@{P_m{-2xS0BrY?E+Q$8O^p;Ix$q5ydesBzqpCU6y*6;k)) z2{zaH(hfkDlQC0DAnNu6gHpW(->ABzTGz4qW^8`bIvh;B9L2cUFA0$!6;Z@v%O{aZ znV+u^Yce-5U%!`OtLVV5eEdWU>!|hNkQK`KZb-}wN(JT4BE|`JFT||h<$V;NAhK8a zaOqb<50>)Atq@=$_PRYwLnBKPV{Ptt^tjJ#=TOc|eNj#uX!@-e2F~5|8V4$e_&}r) zaQ?a;?F%@zRHQc;;7*!}4eCh4*8?2`;o`q3>`Vo5!Rr9ryPY|VBR}k(zB`$CKhhR2 ztG%WM8HYz!m-7VU;}tpAqioqrqan|LOFUqVeLt0uh{&ccQ+;hqG(2#f%y@%{wy9S& zyk&G>j-tSd7fXT&xh!%4jd@z_VvIsI>ixln--EEe=ahV+rY={61tD#1@3!F-zv+s4 zF5w(DEvG#RAzgCRzTfJ30&`5$%Vran9-3uuUehg{bA{|RzZ9qmKUS)^82DgfD#;5I z7e*_rCBfW|4D_bNLM1kmVqI+@4MKJd9T0wV^KY_7W%ngHGhKJ$cskM$({%`qbKWiD z(U4$oAuER@ZvX}%S>;_&B#`k;EnsTph}VyXwk%73Yo}ASh^;}AGSy(tzjOC#p($zUgKY^ zTjul7hBb6@+Dj-1S~B7bhMFrU#6(4eKfClv=6_Lya#UFyUqPp2cD&QyNajtAio#ZdM0~ycm<7W zWsHKTLz?F$G>j*AG$!$B$wr2u`G$#tLlXFodUVh-x};O7;<5E;!8Gu5FLLU*dZ=*V zh~{!&E?UNqyNjQ-e*LX`XFs)5!M+}dPi=8?c3W$ z?Gf|lw`c>s$z~~D@kah*8cPBHe9UyZ&ta)Y7zxtecPp1R5(DSTW31IEH}6adGp0E$ z=^F{}YhdU(x_!F|-ZSSE_KO=<{oepB%ArGe`^V43|KpwKYY_V(ukjlw&)stoE+>9wmg_9F*M#`1xmz4iv(It` zUZm!}w|I><<9^cS5&3j`s{rVgEPCwtOuf8(q7o?n_7uAsV^y^8g>si1bUt_C8 z{bxsgBGi8*K@Wr)yvil)mfuRJ-I36K1Xj52u8=o$I59L^jPH|r^Y4f9b7E%UVdh;3 zkMgo3w_QeB2l5fv65p)H4}gwIgylZRZ~M*89(Z{ROUulh%pwl%IEU0d^LU{adC{`O zB1t(Lj2`M}1@+pf_c*^}&^}R3h?T{Qxw|d!^+CeOreSLwVy(CvtVr3Rv!gNw@@=ft zztQ`3>FX3Cz|^RRPhqK>juzrpNi_cKYzIdGctIyp0aDHOK4Y0iOrBV5WZ7iBAYXDJ zK53+R{J5Sr5;}1-!xWTdkmju!!Z2gR)U*rvX_%7$u;0lRZzTeO)QMb$pJ^F;Um23< zLPKw?xd~>h;LEG0RJ7#sjfuMpvLEplPwys~eIp`gN@Kgb_`}qcr^wTA#y7_m5=ok^ zmi#6L3OnuEI;`;U;fmhIm6k>Ayt%OKyE1W?4t{!CWZDAvI6!xjY=g=z%{`U5i3l88 z>})HWHBh#>%JI|pdTq^4-xtZ5L$`cJe=&eGdVvXPD3zOJ3A-t1Gg9{9h@!EiB+Mko zGsMBVzhB>z%hMd-Z#h(`qT#*7{O#KjqsjNthIxW%4w9htj^DeXHdz;Kx zi7EfuU}+-tcp&7kl&D+kmYTojBPR>V@QcA7*D;+!f|13@97R{)9ojm)Ln^F0Fm4h0 zRuTsi<}2||%ycf;w6SuK72qdZG5GSBsR7qU8B)HE}YW z`x?9hH0?kT4)FVfu3thzN+P7$*dQn(9t%o!eL15B#Nnmp6AA?MPY3pkUVqX^@(8}M zsBqzr5Ad?B0A2{v|1W_eeq;3qIbzy*A@3p>Q{3&}NkaN5lHSfe&@^9Oeb{+IuW=wI zBx~Al|K>3`TwBc?RharhxG zs6lU1Bdo?H+mfd2-m7-FV4;s;>?17>9l@s6BptR+ABXK0!s}ukz9M@3EXXDos~y(f zXd&-rC*=YO&4Lh&3WeUPfPt*{W)p*VbML7v!|u5dDEOG%{%)`{h|ke&~bn$D4He757Zds56KY@ zoDcQ6my}V9qD`h_uT%kYkl(sN`uX4#Mt<89{MB*39?l2$x#~>Y6dqN(v%x6}JwD2% zfYh#AlB4@TBeC(w);$&-)=Wq3!H)$)GG%f?1)qo501dek7i@;9>?o$FD*Huq5Ls(7 zV*wyJkMi*?Qg3``4G}}COfQ0}$cS&zLmqjERY6kRhOL~ixle@iKfE^<`;gd;xSL?I4-N%%=34Z%5Dw-u{jL_~Vpd*THVsZ|KI$wA zXxsrfE%JWQ;;vvGZ*J!dayIHw)LLFxiYa^i22S8(`|2Am^c6K?%w(G_)9!yc$C&29 znK0Mq&ma2A8JtCy$DMnvp;iJ&VaR{~2!}qb=yfcml77s7A{02+a6VjYK{&G= zAEA%PA=FgYemq)vwD0flzrG$29*^T*mf`Ys_V7pHc%kA?F)nc^9A*>T8^mwP(!yH7L|NBF*H=q%8sL`@=sR}o zI(CeFtvQiCM?BV*rPbAM6kPdj{a(mrkct#?TcpWFMZe0Zei7_+zaZH?|b@~Z_0BK=^ zL)e4L%+}P0?tt05pJ*@9^U0Y39{riXIp*~~DM`-$i#gEd?3s&Ns<%UP# zG2z26$@8S;qsbOSo}>+La_aJa-kUnZvcXyj2~C`(TpA^|*GS3V^GH|Uzyy%h=J^YE zBK1Bgt}-vZ32?d(V1|c(=|mKf*uHf0??96MhocZx);-WOpRAl>t=xV;(EbAz zs5{4Rng>ngj1BHv?#z-js;WDzITNgtokaLANu}?k&dA z$&z4|o%!&MqhHfHRO9UJt5QW(8%$^puD+JWnGT~uUDn=)=WDbS3a>|-QC(8adL+6$ z8jK^6IQI{7=)qRuRwW_v@-$F=MD-Ai9$IY&kv#01r)0S$lNV@dKR+m)++ev!RH0b*4fFc8 z@ZIUq*x4XSByOYlHN2()Qn?2aGj<@p$yY?sdIXAOpka9d1be1^IM4npbd>Em$0Or` zG6Bz%?6q@L;*Gu2E$=Ed_lQy-fXm{VP}Ia9?DKwTB&jq=l}2WNKfw=(C2F?7H@5K# zNDMYNUJJprG%GH8EC{QKC!7Cr>jgHabfWL^J9UAr{WT^r-~e)1$SK(J0zx^=S#u91 z&6}Ts5qR$y`RvW9_tuSqxs%Sa?`>8Dek=$hq<&Wx5}V8r;|qJo&G!Ro+Y?>jjLzpx zrSzX`zQGmwHjXa&n&`69ijzZ$7>jiGRRI+z!_)+DgJpcn2H%P;|N2iiFs~K9Dy%&T z(ZI&k)g%Tpz!m;$SZL7UkIn!{n_T3OtwXp3>L}?)$}2Hb5_Js@0)unkszT6z zRT13&`WMnR*ac5E=t0iV0j=N5=|E*1 zFfVa{PGTb>grpSI7QBmM7#d9Cd4bz)Ei!XS%V`&loc*kIm?>GR2j_3JLiSL&WQ+CL zryp^6IHjM)lz*1+uh+q4MerK5oS=85< z@wmx`OtL0u+Gk8UxAxuplXQA;-Pv0PHy!~BaKRwPUSz*aO;iCrU`v2mVUy9+gu5-` zQiEsOgs?z&Q07)~^M862Sw|qLyP-Dnw@b<9_Bd-D`y(;&Nylt(q1dezDeGTL#2Dp`lB{^;(i}{N8Zoi&>h%CI3#167Mi~b zlxH%57Ko}N|5AHL$U~ib{5WUJ#*|7P(KiO)+%Emti4ITxo*N!ilDyB;F8G5|*8F*R zP2dFo7y5q_Y{}jN-%kQBZH>eecd5=ce-tcV;f|!^cc44mRqRTu3L(PsOTdK_X>PX@ zX-|F5^6WUz^*hj8S-%Q6xzaqd7h_^EX&t^j#!?tDubq}^GOJaw;w4c_<*H?vN{t2G z6*Jjx)R(%ARzE4pp)oP9J@xLUce7%tM`&y7)dkm@g=QKgMf=Ih-qFGhIv_1rlkqF) zmn#-)kbjjgNVzPSIO(!dEYQZT$yr>Nhv*G)8hfCQ9*e@5Af@LNZWL)55BNsT-ojh3 zamY+7&)_WtXj-0IMF})pvrrbo{Ir()WLwhXHVgOZ9kwwosW2gFPmHOGpY?c*g_`SK z?~@vTSl#taA!8c3=!!=TK=>aRsgf0f{*HJ z&~$qy)0_n{IJVX1kN+H@blCI*8nxoIF)!I(gI>4PM}4f3(-%tcXlkE zk~Kmz(Xb_(&tN#Z|Y4OVVP304B=S2+R zgT_C^{(J~*^~>xy`r8;7-U2HojMqWZ$`yifd}7E{u}kxi9(a@9eq`^s(YWZaf%>?L zibp~srJ^XrLqtTRKQs%^tJDi*zHhU?)~lFhx=DOK@&cp+xsm=eKP*{l3))Q+0u=+v zG&G(5_=uz1dV7^D2FCl(GHfkjXEFmw)q3mBI@8c1fh0o_wwE%mXv1)7&k-TzL5|$e zQA9agobO!WD!<VTG8c8q` zQ{p;AqC%aEUsw#GT}0GSq9lo7&xpy-j$aielSsc!LG!asrZB2!Wt40E;u*EA!5luC zIH)j;B069}-kg%qRnR6m8dfOg1F!jq)`PEfxtimC2jcJ*;G$I7iK&O}OSKlc<{w&xlt|t^Pf@lAHfQR#_7fZk-(5>vty}=?B+=%ZHLKV0ZBR zy}FH@yIVF2SWFJQ;1)k!@J(=XYIGiUZ}ubWdG>Tx{hn`d%ir??{R0^c3HQvjBlg6Y zLfl3+U0vAn<-U(CsQ(6YS^jZ2+~A3tz8)*fxpYh&rAI%PrC?Vg`JwjfQ#A@7R}|A^ zCi(8t+Zsj4z$<2i`(RTL2sl)AIv~k(c4$ZrX^LikY~bUH-jA;QMnT!ag{-C|TFyY- zR~u4&icE1Q_~)*6C;%0K7gxWjq%+X}K}zWC0c;!p$2FDL=g!OS|5kN`rF5}UveRNJ zY2jc%^Mx28#H45Y*uU-?1`OGzQaSLLn{L+f=se=)uD|3H_X!(!C_o{PBOvvXaIOlp zvU4ffkr8Ey#nX8?7K^wsb@r>!xgy^_qkmegzQ)D~$=MnrND}h zre3Lp1mp9U&v+Dm;>t)9(0wYb3G^E6);^Oy(5^gQHYv5`?0k1p7ai<-zoe|QlB!St z6hDH|(j0qq<7HUADuH69%~Xesw1}KYwdOb8=MKIBoutH%jT$+DJ^rDzY zmdrREfqrg$CuXp{{aFg?U2QnY!KZgqvW{hhbeb<1U)o?@;O$0*jUdd$^*yPr2QVT= z2ME9k!*`a6nriCr&rn=7bJ0m`^jDzirqw3>QL9`;D#Rgi$cF=n#D}6~*$OeH%s@*R7(M@IP>8zrcKH2s zpiT|B;*{bTP(zjr?@W<_X1KN$!O4f$bo)Ty63Q|OlE%NOEmu#51malY)$ zpq3RI-0D4usoI0>xc(vZs+iJb^ub#jtK+T`j2FMM{2-M&#x>Q2Ao-{FOd_v8{e8g; zF_XMm1)2=MqS1t_(HABH5A#YWlc^ldE<6#fPO)Ex*nW4{EXU-+H#~Q(fHxJqRsvo` zMGo(q;B~ACEpJ>o4(`)=cS%TU5yH;%<~%)BT}b}l%2LkU0{0>b?H_k?wcb@se*<+QPi(&e!5S*nn_Zj07i?th= zp~&b0IxwdD9pdMweAYc?ZrlLTFsWt?0QI|{;~-k@CWF8N%=Y)y zKe3mMT++d3q4oZ~rwvg4*o)?4@@|McJ7Yab*`A{t_TgBhI4+J3^K+Li-^!muP;cGGe>RZ=oTF-$Wd&E6A1xTV}hVIlHgtFt%35 zczJx7n;AvWO6O1N#_R@K*9$mTa|$Fdzww!r|vso&Hn638XRRv4~WKC^lQ zvz0dHHe0k*i|`U4W1v*b;>BC44!nS7s-m#nX-BL8U6{`;dWWq;9XmR!;Gy&j7%Afn zWGe~AH*R5ooyBI+;>j&H@OEKPHh+ym?@smp*1f0wW?HB|>~&n_4!Z{S*w<-vAyvLc>#t;8#izN{X@g=Kn`va+roPc;x8q=$B)yE(w?LMu<0Nw z>TkN|mK-Ru4bHK@ubDg6jNT5BKvQ${UhM_2rr{&7-f*2_9o}YXGd1BbdB3S>MZhpl zh)hg$je^MDkMT3_G?2EX*VIP3lw~pn{!50v-?G3DGe8ip`JV4C_Lt%v_cl&-k@4X1}0k##)vCk?p%1E^=J_p-Av6 z1L8N%nc}5-TsCQJJ`7+ND6#eh#=SC%ncQEK*!Vg)H^ZrER?tTrPdCMqdfg?{ za7@@m#Kh95ICR9)dZS|6tMx~ff#9D*r^KNJym5I)`qJB?F6|(^aI$^-eC`;=BYvkqm!%)W~Z8 zx|d@Vqr_M$_(^YlFyjV6Hf-tW^lIs|JQ0-tsjQuwP}joT z=OiRbT>@qKneJ}ZUzj1F^W5=~fx$}R%Wjo4opxEYs5FMg+Z4S`wH`484Q_e$jj{rw z%xmW>TQs=X^hHoY6b!Lt->Cu;;qUl?DU;;1qq9-3RAynm;mRmAq+vZDwf4Jx(ExAX zC)J_mlQ;5nq4T-XR$ptsV(HA_4|w=rPH@Ob&K64*h@vs@6x!divrgA+pzx2J^V)3l z5c$LA&UJuBLc~{I)V>gwK(sQ<51gIW2u)Q6xMOF(C7ti|3;HA)BrKF82cSfFD%H9s z{#lYB0Ck9m?gb;=(lCF3)+SYXK{*R#(+^`7`}lel#o=cDdqzBMgP_3NCh0N%@X1nr zR&xPxctK>lFYk*Hxnwa+^WIzsMI6b=)JDX@!FdIIguK5FvcluZh+);WIqmr~0~83{ z6^xS}0(k_C0CEtElMNc(X_iXtQCE7vR6zROg81%>B>QY$O3>CT`B{ZUoRJzFNy5fX)BA?= zyn40H7{!=OAhHz1vuVSydX4Wvd(KwRDq4c8GC9hoIRJEHK=cK}93{HSt|FmTG>-v~ z%94+Ru?HH%4;t;u46g|g$p%4?cBiK>flDzTAMLa7F%YX>_4(@`teLvKgykdu`459? zK4DL(B0B3%Brg?(p?D;|qB#7!@!<0Lre60p@9nsv0F?_ki8`dr30~)@?cUWYNzq?@ zeU(V3eykxns^m$*qc7eX!rnBPY_lct*|T>io`*nW!Qwk}&TR>otT9iHD}@v7JRTx2 zD24yN2cIh&A|U}JfJq_j$Q^(GvXTgE0y(~`drXxOt0+_DdAR+biUWBTy}IkJN;)&y z5^hX$4f0_TE=3`0&?01xv5){#D7*PlEyv3r{1ekkC;bJ2$ z3xYb5Pzb0T9hN6s7-)X}^Fx{7d`9|{&b3&$=K(10Zx{?dWy zF+sY3o9KY$0F5WrI^h-6jBk3N8LFy+t&j$e?t*qFv`9bmC}3E!Q?wHd zZ0rT&HGRc@QO>940-}JJP7YM^rASo)pGeQclsnb`Cc@Vj=hyv# zw*rXdtgml8?1MF#i))Hydws7;7j@0U0G>- zrpU!Ciu0!qcO>9PS~?wMB@t59<+@R|`R#YQrUFXWfqf!iNSXv*2fZdQi}rrC&l+q% zBVw20w?vlD0?G+hm7^t&&-%vl0|4L*?wV4UkkbW%1&+5^82T!M6zIw}eDbsMN#RoZ z>DG(YmSxclvcLqh)Z7@7Q6RMAZg8oJUqK zSuo@vUv~+t-E|;)1SIx1gH*ks69PBN$zWld7KyJ)#V}w=h6}U+)bH%&=ROO!Wqf`M zM3MDByOkw5226S<{r;GNP9}$-NqBes>`n1b4u)iyL`3t20r{~WR(6cEgopDMUVU_P zz{fo?h{-4PNY*h=TqYz_rw`Pj8bVi9oLYcXOUjMf02S($Wj;IvbV|+nKgGLc=t;GtCcR zs|P-?1fV(XcS=E}C zW%k*2`k$9Qz#(v9{<-1+BXfuB^o!$i_gc$qBOkNaHz*UXg$pbHV1Ws2Ge}&rRSg|b zy^_nr>%KO9ot0# zejRWwXwPM?A}{)!!b0-})Y!exw8I>QIJ@#n)zS?^29-O^)o>ku*nG@$!veV~HFRuw znBma{L)w)}M%3X2!}1ymz$s-!-HPS7#MbsR_)kIi|0;bHxPZb$efb^xOal30+^C4s8cu*6|qxZTqBRvIk zZ4b!3X#%I9A=`Zuw}Zuwm%FS?028}=Oy>8xr4~;dgxv|L79iDl_iJlmG8O#S%fK2? zjVak;NsMzoDih$R8TQ*cRRL%zB^w)GifD$@GUVL$hcXz$mIBz10Bk9>t#@HY0YKf+ zJ9VvSmI{{ZRICO61!tVG6$>EVb-1^9Ljva z@McHJWmIuazTD9Q7&LR+UBurU@FjbRemuAVSGOE;c1ESLgAHwTtcRHRcwfI9 zFislN;$;OF4DSNm3H?M=0q-3k&5d}JW>fflap0YgM@`CLk(1(leXg<|E~TD)>Sy(v zDOD;vsw}LE-)#LgBO$OZpTgn|O11>pKc<+t{Bh&qn=>n%mQs=EX}8tdsnO2|zgbfd zxpSEP>}3Yt+%MXuK>iB!R<1q2UvfJhnl+;t`BW^`XI3-nZK>Z4pZvAqLB z;-A3xOZ>wd`G@C#8nE0cgE?+~zXs6@kWK>rtSoYdmB_r^TDLn^s5N!Z15Jf{vk4AV zB6?7~Z`G^dzBp*ZXnz=nS@!WJ1#vb#F1%bT>z#JUAU}6V0&w=+iX~=dI^-{cb2yOy z!)5bQ{>Jv&em7ZXpw6O2v)UF|@0Y1j6{`36axb%Lo#yV7ZC&JNBn0EIuq{gIcrsax zkdTlUvGMWG-GuhHptJiWss~Er20qw!V!pEoE za0S0l7A{vEIstzVIn*><7Y=S<1pJD6d!*%n38CuGQXg0=7nY{G(N-ATYV1&ReDPF( zfn^mJ)xa{7EwpY?U*r;f5s!<8229Uo453!y$+CFe=oO{4G6ACZ4t=#TO|Z}`k&mPw zN#U-8yp~X3zU+llojJ2wV5($ZrYsu);%{I;bru&*rTX2dG~#TnOOmSTYwZ_#fXJ#K z1qED7*wXnDVa$zIPHzn8mZ=IF91!;dg%JSBh(qy5`&s}Z7+s;*f{!Spw9d7nJHcb( z@61D84)swm@RZojZ2~YL&0<8I8+Aguww4|WCiWUef)!iI0Z=C;aj07y;0#1FwyHvl z+cz-{#==6o!Ykoik)h=|QP$vlt8)k0HY_PI%-IspaUr}$R%P7)zf1@CBWIg6Y~WKt zL}ufLvss61jkis~!s$ztF-{(ixz$A_B`{@N-g~V_hONA0C$-%)vxeh;>LAL*9(h^kbujo-CLUSwupPU`Lz_hDNz*ZNi z-N8Mo0o|LBqJy{r*X=^aIPkh{HjelL;mHoalG5qm)Vi!<)lK*qaYFoQvGf4wyp1qs zw`$L#f8oZYB*sRDW8{8rs5{PI!$@#?i`HwFksyROz%RgWVbRtU_<{-2Z42<%v-9>WSH|_og`ZE z_OYQ8e%zdRQ%N-JGV2tthF|vu0r@sJOd4tkC3kiQ@VoSZSmZ9t2FZaKf<*&VF`%xI?EP)&IuC2sM zmkAvtGtS6J7uFLcxQrAePJ-wauxPsxn*V$5#t^PV{8@SN5}IlUSuP8WYvh|z9$g;QC>!4lQ zNR#D+la116f;orEf|Z%Uv~Ee!8caU|p*%~PZ-Gz?@0{?Qc5IBuj5z4UOMgG2kVTut zqG*k+l=JG4pY;GHzj}N05X?Dj&@%*{RRuxW!mQ{6z7#pCBo*>ApMXxc+09+QWS0?Y z%o%Ns$U1-82#WWBW3vFb)8O%GzN4L*f0$WdoXc_-__8!|IPQS2*@kDNp&Y4VFP6Da z6;4#7Ws60RTg4o~01C_yAUX=vf~D4VmSs|0C3P({4G<4-Ab_C0=7PjHYY+tk9G|(- zK#~iv1!yce2@wmQp6g?Q@G^h~9m*Pq+e+(ME~UTwVk@u3$mRZ}+1WlQvV=vg&I_P- zFb9Ue*nMm*L)#(NLUvj-|Gt~mJ?NJV{K9e8cfAydbb$CiQg4@EwudnTi-8`eFJ?)L zD{MZi|G#}JIZ)8{i3`To5P6Eo@1OtrCB+m&``YJ1d{2Kwz^FgerPUf=q>s?g$iXyj z(JXh~$@O&E97KPiS!FoJ*t8_ZmMMViNctc8k^t;qFo%@c#mW}@z(t(?FvB^mlG|aI zLIzBeghQZaSkP@OmXvGi=g;uk4q zROtbGpaOKTANiLj2%zF*nE3p&I@9qv5^~;wET^S4m?26^E(uZ~LpB1iTsqhJVFTDY zV$e`u9jKX^tgefgZ;*8H8UyE-n6JZPZ$rDv;&~9n{g-1c2hPFMII}_noNVCq1YW;f z*{SwAhnCpXti!yr(+lm?FTgfN?w3Fdw?FrzqE7@^FcT$s#-z`q11n8Knt87|UD zGbkmQ_(10ecMen;Zx$n5V$*dGGt1Zv9n|dk2Wxxvm=CsuS6YKyM%HVfh<7D6Z?`s< zJiJLmT4*D7vcP-*WezFQisH&h1ER0Q;i@3*Udx*KwGtX#2C4}a1%$ek|DNoI1o8sN z8wuUFb5Z(Rr2g8?Nw_MXozC+`>S6myg4YfTT-lqyRK7*@_O4sEOv%4n+PR*L5R&*j zMW(%5Pc4fPB@z7N@!-ccEa`QpT$R}Kav4MdcRi>e0=i)M4cGzVD3vbTOyV=h=73Ux z&5R+WVz7PI#jqGri$%z_66g^ffr`t^F(ctggPX#k4utSx2Rwv>JQ;aNUgUqIcudzx=M`TMo(B4}fU z-=F&XRp@dX2f76MNc5|_{j#sk&HI&=#O_)(2bJZI47ODjm9}pT-0iiYh11y=Ng`rC z`91-GIrZcS5SC;SFX0Jx)S>T|$AYddG`#KS%U8V^xW?jiv{nCV*_vWWxv^{fr14{# zu#%0Amk+eF6<%-nP_t0*$4~v)z)L|FkFBfm@M#zr7_Ze8=y-urj-S2Ixq+HUWd21j zI2Exep`nf5X<_t2G0M)1ajU_w5m!U);C#;9S5GG11I@Z=_1~Mpci#%yf4%pKO+JL1 zd(@{EgLhVT)^^8nO(>FyE*GqrsQO^5f=!i560oEmIm^OI#+bz(@ujY9x+g1lt72Xy z`j5$nCS4YMm{Drjm_CX2KT;_(VzDz)^)qsz3NMGm;K^G*t$(XNrrc#kutTrD_SU{~ z-Uvv>?>Bt4Al-edMDCkYS!c%vi=mA(7RFgE;CZZXF`6NoKqA<@ljn=a<3#qNT=Uy{ z6iwcT{#bkn4UOx5>6h?2Lhyxu53;<)pg8K4o~JFn+z)#!6U5SJjdItOO>j-ZIryNZ z!w9oAU|xqtYP?Zc!+Y ze3YzNQQ{Awos$)K;g$na+*F9Z1_+47SbZ8Ki1bT*n2UC3%7}HwiJDqvV$AniQL}cF zlyrs01yj+w%!G*arEYhTje0||u24Md0XsIQyL&svvclmb8peK6&33qyFf@aNp(0@- zh1hy_hIjEx(>7dK>Z;mnCDrZWjw3cTI>;3km?+tqv~6LuUs6Nii6!Elb))L_Ac+j2 zXoa@rOyQ}HqQ9N0r8~DBKXeI)EWQ;&4bMZh6$-6@CJJFOG^Qocz!{@3JRLa(ehz$k zWYBL;1Ql5L(Ezb0QM#cIo#{aQRzUhlSl>SoSC2rJOMvK410fWD~DtFK?8ycC{{nE%KX}h)v8N7k6lyin5f_x9dur z2;VoQ*R{Ev=nTE!s*5VnVSOtfL(ShM=~I#1Q98BBp>r{GfU2y#R#G$^^2emx?4yxr zPLI?T93f0GZN-JMx`u4%n3!U4c>#%wcU-!Lx4HVy{aT#wYJ~)HdOv+0z%Q!{Y*d^L z2#u&sFd{h$s$zDg;gHIOOl(p0@Yt_zo3jyHC+$&)ByVKG&#q0{Hc6aSOt3kx|NiW( zmHkmGPBlP2AZ&pZ|Izi9VO6fp`!LF!2CKtMpc5kYd%vgnp>7Tw+5 z@xO8JXaC+W?{T2(SRcfD=9)NX=A8SgLI+$2E4IUmfn`zpLwfuwGq#XI@y#biu5VZ@ zP4RT{k&dxOE8XO(QbGe0UhwA%(Lao=2!s@@3QxDC*J=sgSDEWG^}8jgLz2YKqT|Ti z5?&*v#^>$ROYd+~eo=Fa+oK-E8Z%A*Au}icWh!6r9ySC@{SgJF!54OZ5rzwkrz!45 zxRVZNipvg4mZ(@MRK+hOjaR6DMpvt&jGG^k&@Q5QoDQ^MX8s;U9W7Hzl+G)< zO0%7B1oB$x!3iYDDHXUsdIZTT<6(>DMclqa)%Q-!xc2gq1{s3SKfc0sL;r3^ofcVi zd<{o0GrN;D`P!n2!N3gr~$4B;oob!aL%7 ze-QtA7wx3FcX4z@$qs0$lXmzhn9x4ZWV9wuc=C90BRjPuK3)(Zx`o{DftvneT->6e zB$DBhtt4>cHnp$x+uY!1oYfIEBL+I!F=aqHJ9$h|Tedb+?(HNZ>7@ilz_~r{MRF;% z-inC!j$-9f5cxF2$g5%9{*dA_lW@oAFy6UT_nX)0jA*QELP|wvb^*KFDyf4UpBwB6>Ljj_Slt4)fMPf?f^!x4^&^WMX9s?C2{_E0XMKv{5VLb6+;e+x zT0?@-8~*Z_-Kf||;BMm>8O)fOtAid2?dQ#pr)`-yfGO&zp-l~h%yMsw;9@Kvd|EF3 zWP_gu|7sq4&hPWI&7Ze$1}7O)7LFfL_nzK{VNv$6lsC4&*9J*TNlA?~_w^|ldTtPg zVo{O}u>-#q1wL$2X?R@@@=6BWP?_l3z^t>%l<)3D;2W36SmLCig-~q{ zb->hDbSqp`A_~zNgnA>KNQv!8-^ArnN(q%{*8AG9 zl>M9{r{NltS(+#ez5O$%STwDt^G>=n<_DwDnpZdef_Y905RQfVt++^$QXJXS`A{{1) zTTrA(ubL*rmq6Q_lN@POnH){pvnJ*@iqHZ+-3dNT-S5PO?=={`5f*(Tug7vTO6K0T z5nG?#SXiS1X2ZNoG;?+he|M-EE;Y+WFFPY2UX8m3#<2_&*>QAHjCeseCI2Cvhw%^k zGWNFK&<3vv^Mc5IAPhstND3^8lo0Swb}A{Q-`lwnK$pR({{V{ZF^Y|v_3sm^hI_V! zgsB7;*pQHhlJ5SYN-~=d_%kQ$z1=_%d)5#_KL~D)SmFr3%_dmIE_A_~{A%unar;-C zV^}0}3Rtd`j46=gM>K0=&)tb%Z*SJ)fy=Df?sruDt$x4MVFGLjK}F<-z97rbOQ3-U z$XLO@fosIqJGox-gkm20j6L9qy*rEAIds ztFPWse$!BqHkdBL8Z1gRM_xRWJ+)WRcR<%NKM2IOQ-xxt!edp<4FDoZT9n2Bh$@1N zI^UJFy|hAK32>n=F<{)iO1!mdyd5x(MI8?Cw^<$DLG@~NN=52Bff5E*85qmfcUk)c z>CxSI1PV;d3xeAqwPI^|Ge%XU5BLJMM7fkixfxm%T0E0fpvv67H9D>_<*WobA6RL% z?_uu{ziWD)|6qSPI}{ML9jZ=pp-?Qm&syB|iFaI>3$8&Lh+2Qu87@p<7vP#DHY{&k z(dPaXFu25b$ggWMff3#tR0N6@#=`S*GxYDGW!aY|DTnMbv;SU(6M4FK=CfbK5Cg!L zz~lAWmoHyXuXaLkBwRSFW~7!TKHmP6(NI-@x}VYrsxxAv;f zgtq%r*8w^ZcWB#9Ow4^K7~;hZt<^stp`SF?#D^nS+MiSf*T%GEuGbmvDAzkYMtgD2HzQVh4 z;=#zy-fhEOcyi5De4=i+m*00F$;62fGGjglYZVsqF${|y87@L0Mg_Umx>0A4K|S|? zK5ZXJB05P%fzX0uWEand$V>Piul~-@lZ~iKJfTCu6%;#VL((ZT4L~&@eTo$=SbdOErof6sEf*F0 z^fhVK3i~a{slkLo4*ni=E0w&<@>RM|S0Kz4T4^+SP zIl<+N%R3O?V7-4ftF&ANY@&E^(;pd8It8p=EPn&m=Z_#;QdVz#yxU#GYpq5^(H4sN zpIhc6K3D`L0RvpI+$c(Mzh7L&u#$6p#(A+nzz_@sL3uT|x6J`qsahV#g)n@PsQzN# zwADLm1KL0F#uM$)wA=m6osw0{2QHe}F_W6zNyG{4|IiI%)db5tZ>Ui8mO%6OuZvq$ z1OlQe)PhMVpk}DL^T~nRXJ=7zUS*78w}^c z@+pOw%VoA9C-%Cm)7u&&KMDZ-9!EhH3qTE-FGg&*1zz>JfgOBRC58 z79de(Py3s+crE&zz034iMV%@)Qc+V=t9l4ESw6dIU%&*c2N^Z3OG+;)zMA9}kWE4e zsQ_F`FEtAW?tk{azfUz12M`A!qgP3=N7AD98oKJiCEx&U2b1VGlN^3fw$Z5m5470? zyoREacGMP=hE9ssFqHGPsqX-jPUj2$7>`&r(`v&F#CnGKz}~_qy*hqUua>2~{Az+1 zc^SXqWA*hUzxKq;m@ilF8apO*DAmY9zm{F3hJ#Zo^f5|4u)L=doum@wQpR7nEc`m} zf?=Aj#l2?ECGs4QQD~)vu6@C)ln<_^)YIpLK2IMV(k5s*FIB+ay!6;XQEHIKN*rVw z0OzzkJsb_Bzls0+8KFCxQW1Dvg@DQ{0QKOkRqFmx2EXo|a$UP%@p>hTYXYrFKI^m6 z%`%cN<%eMez|N`xAMj(EPK43%d2XwqdREc!6We1zyD6W8-XOiS&>#6IKtvSA5Ov6a zlo%77Yd#}f1muWNq{Zr5kJl!QtS+rR*J|~x76leL0|qO@F(N@tGYqa9sA|Se4Zy3K zo<0eh$UxC6CCF+nGaVGvW#}K!uE78Dz2UwVK47nvdqkWB{{~Gy4YVmg1l|xxbyqFo z@xhY#4f!1XWguO7U}dS-x8{8=~UXN5PDhx8HFj!X=YU zNIHL)06&-)$!7}ITo`}k9dIq`R>7$#2m}^2h7rYY_gTv=K+VK49I8nnCB)Z}j(Qb3 z827M+g%|1u8+HQ7Xaq%%C5$dGGv?bj$&ZTxkOQ=kz(6SVh_x=sGEi$WVRf_~oN4Ip z4;(Vh=PyG_Sv}u3w!e?fU#vaw@ zF1C4dugi5@d}H@T_|Qn?#hYBA(uQ1XR3I5c%Ruu!XM=c}L=q^YAH1i`*h^2q#f^z% zG1MIaC}|xF%w$aw&+1T17uw$X#h*-&&k%z zsnkGaX<#bYSpbxiT0{I-1BA(Jz8S~BX;Vn$OGFyi5 z?mnX6Dtq=D_g}^GBDs|Z9#KtEgC`<~0@Qd~^$~Hu2o~-~95p^hu)hMl!m!CpO;3+( zNCQYD+wC?)_`Paa;bXv!6A{cYG0b&YtIYgVS%woFfKX!}axCL8BXuqLpdPMZK-dmy z)j@2#?I?5q0LVkz4CA|a4Ie{}=71Nx?J_n$s!ZC1N5t2e>tdK=uwFnt9vJhjMP;Y= zxRY%Md6NGIFexET!d)?^-VcS$aBzYsHWhm`;NB8y-j{-M2B05V8t-8UZ&d5it@A)oB^<)F8Yl}yCoN{eDEwh6Cgaf-S0r)0)`~(vieY<4d9UZoEJ@6 z&AEY6?~9WTEHaFUe*mBaw7MkFM$I)GEMOwv+%nYCNj`}#XP5P?<@}!h+F>=N`-dvy z>;B~~9;tG13Q9xB`bQz<;x~-PKvM!Z=vgM5fL(}~(@rhKI0dewh?u+ziWo=9rCNbr zI~?}aXlGeZ_C#w>){}kx0k3h`G%WpO?#!yJ z%0M|!WNOchW_;sM!ysv-D0)wwu*G}m-raT_c^*cJ>ngsH95u98zFpZ)ro)#u2(+KZ z%Z)_Y8}_*&?nlux^FH}^ksQqe=?lQqjfUtBFqj$uKVR#}rl2D1W#{09@#Zh}>Vo<+ z&o92`Y~2H#Uj1DORKeyme$t?lObLEN7rnUobNCI1CqxmmXvP{#+}Lfk_Ps%iB(%X5 z6q+F|2sW1nu7KYHADR`i34@)}Yz4;!%EHQu4K&~gJyn}EEQO9#Vxe21UyXKq{L90t zb4Ir4NjyRgrEG_9@`mA?$sQV0xmjP^E)jHE{iprbBn@ zr8L-_ujC2i?RQ(zFq1Lfc)Gar7azX(y8Mdx(ToEHAs6W@5)=-nvkwPHWT-+Y`)c=K zJUlap63b?rt{w{m{CqtXm-moqnqRr3!N3m8diiA!P`I#mk7LUdeqB}y?EDRFBs zjd-X79e8#V6B3#DCnFMs0re;4XJKBL!}(3u;GHJ!b{(<0IdtzXf0E*s%HIw+?k+bT zb-pz+I#`VP6chNH!l82)u^-P%qT4}^vIu@+^8v*_SqUelgc`%TDWjmM?D33fnvZ(Q z7p?(XJ74RO=XZY^ft6|*{h6vqBF4;R^R=t{O3J{p+(e*#zDuH&MPOO6b#cF@Yo|iS zX2+K$0Jne(PsZP+m#wbbNSX(b~^1P_7o=G^OYs=A+zbm05(Unyg3t^A8>fGWN4 z*wYw__JD%mA~y3K1+~eQ6wRKuER^(mO2e+mDap{s_CQup zHJuMbW7dHcwS&g=?KGZ?P{&B1yJgAaBIp+}G3)STo0N9@Bl2)pjyRLf1?;{aCcH|R z^zg1a%LPU}dtKaww!iV{0b}^!e)VBQN7(%o?mtNyz$Mon3%70$_k@B(){YY96!aIN ze+YDLuzb!fr<=3ojq3>2e#K6!Bvs2%W6k8b`mWc0I7EUI4A^H!b`N21GjZI-!&mzx zsFBQ&&qZcu#&db%y8!j0_Fz80dQb^zZW$#VOluB(wy5q z!RkPY#EIQJCS<#Mx%N`-0S3g72O&X&?i^(}E=MIn20&lbZ#%+D&rGK6SGJN&F4C9z zgkMRj`)8CcJ>-{M=g_>{{E0+Nw*cFXOA`DRWG#Bf|6A z5p(QPrZir>XWKWJHT{AeUx()RK*Hyn-*0~Y@YOJYt=aMn2GUj5!!7u(WolL(lr(cR zDej>yvB&$PtBnIEV8dSWP=)in4!mlsb1UPJc8E`Ffz#<8X17Mm-r#)8jHL+l=Knj3 z?cXf4?x+NhMZu|;A2X!Wrtx#RjUS$tzdC1s^xf-`+lTLkcLfhnmF{(ahz?3VZ$egR z;uXZNuQf=T;?#HLt7H*JE)>dRsF=*5{w5E(biNc2*d=3}%h}^9zau!+4yt3m_H5R> zzQCWpMm$eO5Pr3jI=~dHHzdcW@4^%tE=8$RHULdojn<(wubmcJ--y6DnQE`(&vBr` zc%m$M6oXROGh4F)fVFdR#J7>1iB1anZx8y}j{zyL3~b-*U!z9w1(_gOMqzDtyN7Kn zfp%9*65g8o&Fhce90#iUNQ5_nIM%MaVlFO%(*`RZ-;syR z$J!3pA8)xr8Mi*vy_bJ%S!<5jBE-SCg1w zko8R;RI-QJV@}l=xLzAB7ciQ%LVYU~0UdtzSkS|1e@E*9E4cxk=}&78I+T?x$ERg4 zuE3B`#DF(Axm_up%ok9o+Y4|pk6^ua2csfnoAcP{!w@2J9&1jY3j6r_J2WJS3u?SE z`#q$YcuJfb|HmHZ9~A?7uFhIQ*7t91-mr`+)CpIHvxMv1*p6BmD?J{v5In1V`Kcr4 z>cqHGOf%w9q(XboHRk&m?`M`%*H~U|Y2<0ayhiEYBuI~)kJrQg=*}l(*QbV{n3*Fx zn>;wzNLWA2H)|I!0&(Rb(axtZ;A(C^tuQPxWH?n&p|z3zzSn5`Z5NF6)`_O%C=%l$ zM!rz?lD+YP|H@p;UY2K5XY(y&m`s;gb7-egB(lRW?D4X+sA%=D;pjtuA{(XW><1@j zXV2w8%Th_vlpIrNdrO$0-j zTK%?;Li5toCGnta1F|n|!0&tKFJ%fvW9GLYfw2BTv~SDL5(D~Vt2T>vdsoLhY~W*3 zkd^s?!SsDbnezf!JnD;hTGv=CX6^kT5KDHY7;=VmPw_NjSK9eqkulr(8(hB|VxKcr z2h}%a4Ufk(cXHbzITuQvpM2FE?Cc-zuTKaF2pk?Rpz$dwz2x?dZS`B3 z{fZn-o%uc_?9uQmiLJ`jb7V<+efAWY2whhR&hU z?3>>Y%n%de?UKXo*MCS*r`X?S1a;x5I59*orqe!1W#n%Go~%#$%q4(KF%PJDyX)XkQRwp7MzXzoxL%Ho=JbL)vdt#f`1*S8nzGquH z6r9Hxa#V%H^pWx*hdpANPG_~e`GO8RQInCwH3?m;sa1m6NAiLWjVtInCs;OV<-{A- zR{ic?cdb>et7GVw`IwWs19xci`jik*YkBbF4T}qrr)KJX`eo8Ey!76Mz)y^TuSR>> zPE#bHq(%pj!(BS+vO;Jk{&^VpmnOkHs6=&Q9(+r>CbdKvDK_s@YH9&hF4Kros2_GBn$@iI7DLX8FSF#N+MV(vI7ySJSnRNTZ>B zSVmQGTm=Rj>F`M0$={D}|9snxI>G3v$4aa(0Crnmu950-XWA}bwMB=J2qkUP=!}VJ z+SCrI8(vZmWG#hO$YvN>RN`#&Ao_30Ma?R33aLG)1pKDpR2n(IiYX^js153cs}Oc& zunSn`3(IjcN(&|QdP3rk`FxROruT&U)Ao#Ol7P1s_1dIMztEL>joOh3HXfhOos`*E zK#D)cwffIDluLv{rnc!CGK<io@O@kqzw1EvN^d`G|mJ?caAvUXMY{V1Y5B-1PqZ4t7er0iqtCp{ssS4=)OmO ze*W9bX){|p|3O|T4mI`CYP0|S8g(N;(H}_W3YL#&;I%0)D?@6vvo{>?U2#CD`7{XB zgMUEo3zMtOr>9O4k&$x~PpZdWO`2L-xe4u7wk9>8(GT2-|7!2~L-5~k8C;ng8*CFV zgvF5i=QYayr~4+JA~&<8Tf)!+G$R9*1Qyg5I`X{A&~5DDj@pC055vEP-jdmG3LH16 zP1ani_be-J!|UYysID++jj89x?$8er6-2j)4ycTX_WFd}1P|fkiRlO@4;8*qq(~F# zNPOa9FfwawKh`wWJw|vWv{!rMBKT;9z1)+=jj^Hh@y?U=7s(>ClF*d#b<;InoZ97d zaHUJH_=#b8!{&+iagR$dGxiiOwdtTy5XVpQ3hD>cyxQuVdTw3E0;)jh_b(hKB!|^s zmTSb3yQB4-RHH12$8^_7ck8!kL1=l^)zy>o&Wh}f$L&P${Tz+QaL*TNX#7-K>3x-j zF}vkd5983VFqo;WuTMVllXtv8Wvdfk1!K0>vX^0sko$L&6*ETBRA;hUydrs zy_%Uei+9wujeU#vovU&~X{u;}giuILo)qWXv^dA^h*K-{gU?&@i_pNHHKPj=tF@$L zH?}U6YqHej;?plF{-S~Ff!94CJv5kwLQPq^S-mi^l-(kVl>J#wG-B^tYILkOx2Y?Y3uep*fpUE4{%7Ux{R* zjJM%J;+N8LCjU`ro1^Ptf!zWmCqXdVtnqwI@!3T2l?4LbmwfAHuz7@qDL>ZFOFz^m z_L;x_{{6eOmX=oJU5OS8pMK92L!}ODhSOr1TsL>M`CCcFZl324*?+zj=k@SVJbe@R zxS8q4!DmmZ9D3J4GLk~i!+S6o0<;EVvk9bxSj;>|}`ttROG8{z1#EH0$C zegmk&;7dUWm&a5>gG5r3gVdf`Kb(U%VVpHjjVS%qDEdR`4Gy0le28^mH%Y`VW5S=< z>|uuSCi6AmHDrfw$xS)1;GD+`asfJ8oPeSQ)k?!8&u|4OZz?t%<)fjywuC1YLLw+|NE#%QDKca4R#+#rQ_7 z2~d-P-_3?n=GzqD*6ZvpCt?8dnVXw~joX4g-0jwVrFg@=?@RbMwXMdQk}O}D5R@UN zNg}7EJ0iblNW+1n6j;2a+t(=xHf9Q@5hOINH10?`Zu_qK9_OCI`!$BJ5p;`J$iWh$ zYsOb#|Hj`c3t6Eq2yGkjiFNR4n63Sf`4oC{K4!@IH}EGan@6O+;JxFj74+%*(#I66 z1{A*sf?OV2Te&&^4-EJ~$3gZTQ-aOZ99^6X7*gz?oSk4eQfc}s*%l{$^sTl3+S}>N zydm?)P4QHun=f)e&;HhzdsRYie>`V_*wml0HgH4R&@*J|>G4V__4SuG&YQ0%6zzI% zwzB@hnlNg=Kfekx(E^r^wgAEv4If`(yoyMD9)#_0a_^*Vr@=`Hiikmu4=R#i)X5{> zjgWi$77kN_V!tTTWD;3bKc`AF<9Q+LD9sy_)wA@*xe`dZ^2U*$f7@tIdBA-?T)u?_ z24I2T6YqrFQE*Y7SV>v$t8t!F{2_&BdN7%jn7)Odc>QZ;@2ZufynPpY`A;z|1M<|P zwh=ZHeA|5V@i!hVQJ+J|(nE2pP5IpdwY%z+>7spLz~gd$rI^tcYE#>o#N|$Cx$zua z;e38i{jWloKNN`azQz+rt`3IblAR``I+;T5>_8}aB`BDBHY5}SxCj47e{*wsjRIxc zYroBbl;`+v;LwI>;Qw#|*uwFV|5F-(2F}{iIdBLGC)>)!{-T2LV4ZhNQw2S^GY|C1 z4w&?A<1_kPq^^OX3$B4kY0V3Zq_4z3&{G*El>#T)KrG=Ng^`1yKy`$T9EaR`83Hz=^{^(J z11iDByOkvWHZC65U!lCtw7*)gd}_DK@FSZ}+*|5oo(?(Ya?108LOEPmvm86CEKAAu zR~btNci~2hS*BG6tRWZ+O{-%TJNT-{HMkl#Rs=)l9n?bVv%u$PNMz)96Cy?<_URK7 z&xe7JwW64HPs(ZMe`{;lmc~%E zOvUv|VzqvwK)+WV*L$>PQlM;F4bjFLRS*iI=hqpVXdP5;bX?^?7@G|2w3grrTEXq4 zX?!KE>wPz!ZCt{I&z`L$6Fz?~yT=1)xu%viBz80; zNsfjtUw%j{Ye?fJf%AJrm1N&@tb1YV$fw=Y%uwNPqfFvJW*$8ixO#5_ z-VkEcQyD=Fe5$<%8LZH~yAqluf<0NdfYxjX{xN9Owd9ngr%Z>c zzC%qon%GFs9ly%d-9OugOg3okE}so^h%a&geF&LtZm!^=&yO1G_89`^A+N#n>fotD zZ)e8*R9kHYSB3wakR?w z(vHcH+r&@$pABgSW^Yp&CBpfDrpboAgqZkMp6gF1b2Og^8lNX@N&!3O9R4VXfu+74 z<;a#4h&_+V9Z{xD5N--Z(}@FIqpGh(F`uhpvYsE?q7J=f)5F$p4vA=7b`v6M+q2xg zoyE5sqn21N|9p;nOLEtbTS~34Q;D}xe|r?pql}y)ql9MkOIzw>_qaLl zc2#!VbnYA%!txQ1A37iini>lXyRJyx2EXjUUe&`B56$TCSR>WY%dNQws z^n#r~Ip}wkR3$if@30O|w8ipJ4ZO43W6rP2Y?>_m;a6DM7wyCQVnT^Qjy$*gE8Efz z_Kn*U+MM4A^(k-73Cr|3@3|;p^exZ69$JG^EKu~(@B33@NFv8)I6@2*-+dV}#qxlK zAJC$l;GNrhteJeUUT8UE$`5W?Bsn6c;|U8I@9SPIwdgwaIuL_`>@S1^Osc?1VN{$L|g=gLYO^ ziHO3t3JdIMeR0RdC-nPkolHv-FmNcnr&5t^BEz-Q3M6)i+y@>DasNXv%`2k@t4?!gCjHI6HWl zj}pO-%m<6)Ykp#pui&;mbxEz@9^9W%oRryG7$-&MGm}C89P?tTumkkex(Zpzm|kP| zHb?(S|DMm=aP4}e_oa10Kz||OnLYcg9?*9-I2o0&Z%M7) zp1`2mm79KaN;V}3cWZC&0;bsxDLw+!e9`xhLFq3OP9z8SyTXz5cR&+;gM?Fk8+-sx3c&5eG zxExB~B=)ll%l?sQ%Aaa}iVE}91EvqJY6`Ni@`&!>yPLOyiZfKGOu_4RfgOP+Vm?@% z2HF0X6>nk%F`S4c4u>I!4#GxktR857o`li&bmyXoQ~p2u<>;|tER}lBp{DkaCgDB7 zO9mKfCpo3Ch9Ed=k)T1NJ>%QVmy%*F+P6x9MLyw5K_^f*pTUo`?X1A;bfqKgY&~AZ zsSS~Mq{?zQ+n1crW*NX?rk&xqEBitJ7{ID={5bR_O1;*lB!) z+SV41fJ)o80}Z6M&J@*ZwIy24_4Li7tfCi;ORy)x>rJi#-1;>2Swn6dI>o_*J9H*< zK20>%ZsK`q|*;4&2xLnBsXD5+3=FV<{l5t-*&P4J4)~n zQas{(KIgr}AF|065MeGIaJ2U$lGu*-Cp|nsQ}w5EhQd~R;g2n+>2y&b} zm%~=y-NL6Oh==)!sZx*>!*hpI?72I-8e{AsDc@MK8Bo(C=U#JvlR%!`uY$!x({6 zK3VHIyMP)wFStTaru6L*56FewSYzIvq5wpB%>R>EC=xRoLe>q+w`Y6YHDol9Ri)hQ z^SM5epS~u0e>1F{Dki%4AOJz`ux=%@KEQs(xu6)0<&}AQTv#B-yOU^h zn&zgC@WaHa)mm>Oc5hE|-@)bxz1On5x4TwHPK>{!CTq$1KK3%b`G(x)9&Vm+aP9|Y z7*#yY@+fJ)*;DpXn1qWXz&wYKBIOCaIoqo8zOv)9okbA4I$V%fR>r#e^*^!|UPl0I z!LIBf-t`e??3J@aTF~Ti7s!jYHPBM*>WiRr*{7fRGqXBj^;Z1r=K}`HPqya{~R|_DRhWa#Sz2*IjGF;seZFOJhYLMgCWWB0K+IcG`twu#Q)y3^#ve&B`|(Eq1@`SPG4=O6!+FvkcZ zHEJt2Y7O(B&65fp%jc@)2=FV|>FTcR-Zgd_e;PB zWG@02DP+pm1;Xz5jSry~%66<>Tk2Vl;vQnSew3u`y09s3xyQ{SP2NU|3}D(9IcI*d zEcr`9Ky?g>q4j^czqp1)22%yHWVDPy+!(h8FfWRhZn_GEVLO%1<=sQ$GZohQ-hW|Z6X66>f}=bw+LLp-nW z2@rUc9L@)q6@tdzIdn=3qJUJG7>CE(syjuHjiw3(u2V{HZzGkV7Vp)O#b-Gg4Y0e& zvp+U9dz-7g)L(80yMWHDxd02b5|q{N_Didd*lMFaF$r&Ok9r|0wEo}A@E1}iaz>t9 zIOk}%0T_mpGRE~luu224u$^38d0(YFz@2Rkzk2Z^Xm{62LE*gM=jiC@GtsS|y&(!< zxA6bvc{~eF(T4#aU60y*PbKo3Y|4MuC#Nt(rR_EoVrXM(|Az!|^jBLCSBk3`8#iH} zvn!E{($Aqd{8iPJv)0qPY#ZeB8_dohdtNp6hA?}g=yrSIAxtsbV+dM87kC+2}!d=plZ#O9OkadNr;>-FDXt;qQZ44YtjGI_a+NIZ=Uy z7!ESQ!g@yi(Ig=wo|4iPxSWWu(w zv#E+I1EUV=oPSW<*IO^PMFd#;V`pDpuvfI7RQ|3LJ|Hjpy!kkFtWL&qc-SAQrr$s4 zv_wmk!2){N=Kz}q+i1$CP_f&ic<$H~BjZa37oJzvk2~UcVE-&gcoha&AzbpAH(}rM zzz3AP%je@N1%-u$GmikD>m=*x{#82YUk`WTCGA-5J2KuOKi*gX`a6yOcQLUIIvh$` z8fEbPZ`M+mR_$wdvr~slt9F4?Z_WNBQ-FgUdFrEcPtkc1FcB!o<3EXfytH@DJ3ldk zo;@c83ih17f!GS?yKv5G{HkIbq^v!U6G$fs#kz&8UW9pkM}#0bccWymHQEN2bs1Dj zBd7G&@`}oIvFluRFAkfNiLCq4)X+_5f?liCBZW8}Bc#}5!}9*FehJ8bik7DXEO*Tps>Xy`zYiV-E4|z9=l+i@-ZTenEt6-?mS$mgNFrLt~$_1<(K9o&h^ME_`K8*J4)&>CJDAC1(1};D@3D)nhl_KTZJgj5RZCNjNalNOTN!}%r1wX za9-Om&SC((lKbzs?hYs2e2K-=*lM33T28yF5BzPUn1=k1^a1pfks0yrq+sOCW{?SS zKyPmO#(Y*zttPVfgWa(gko;RZ_TXC=Q@hKB-{0Q-thw4_)JKppHOg>J2deHw%+@31 zjY)Bfcv4em7n&K;S1o5DAA`ZoiieG7f9L0gYIy=r_h(!f!78E;3B#gK>6V$E&t)S} z6aBLs^z49G#2;8N1mV3X;Fm2eeQSpsyJMalh@|i59v9)n)vZ$DZ5#u6E9{V`#wwE6@ zK`FGxQlA-~uCuLmb8G{YFxFzva(2O+#U+944KH#$6;x2RA5yN>A`54jabxgAn~Szx zo4E_A{eErLSI8&5IzSdWNmv@(OJb~Ze%D5p<6qaX>KaY4c*1af{$bm{ zXeer~2@CSozUgfMi=k!t`~A-#78g$pOM`yf5CK)*4S*cr`}HET1m0%pEdqKbmYt<* z+j@h`gLXV>w*hYB@s9=xqrHkZ%TWh^v}rmYxiG@zMzFK=Ij*%eF#3$@mNb*(Z6>kb zC#96Y1r+s*Tn3jT`52;Hqj(5=u&_AF&<6~bWtl~e^f-3&8cn=M8D%#a-VIe5-T)`Uhs<55HS4?=yGsMcA_2C@JR- zR?~=XH|~hF`mycZhLEcm`!7))8_GMf&nSqIAph{X0x>avRg&Y~F2;w&?jy{WMf=*F zFm;j-*nOCNq}W_cUMqcxem&^4mx|MH{lGW`h=mEF4?8ONH^crPMzX~*$X+uV6#Tob z6uxQ0E^vyrErKO-$6{ktST(cz-EMsDF1^s&yX+B$yGP9CLw^sCzUvN#N{yjGpDC0y zM?F%5q7cMl(z^#qP&9}xBfYz?P|YNsXE!6D;KJ;4YYosim+-xS^8*pjuZ~#f zC@f^SLuD9>xVLMcci91nG1*clc|)nB1(Y#P(N@BAfa4~>zT4$^C9YY7a4L1jgh;;G z*3EI=?(H}OzR|zFW&kPH2Eqmp2cD}fmrYQA?)<@jc&WLQ?POD%V1=8TDKB!4{~}%K z&aUzaVEp$3eoF9{=ltxLQghRECigWCm&GHtL59`8ex1I(-Zi}5c<-RyF$+9OM7Llj zn`0N$B8nO0U;mT7ghSkgD-eeDE~X4#WZ5O-*L}xR+q@_u^7lbE5`W4cn253nQNK9_ z3Jt!M_nXpR#AgFNkZ>&gsTeydpJQ=`cd4BK7vuMOoFeD?xc5IWRLYV&g~pp8{f_MZ z`ZcRRe4q0kkw2U&cm*AlXgnR_^{pW8T`Z3w2jgU%@E(Wu{@6uQ=}@%S`&HHTvu){H zIM{7|X{hhRa^L1f0_Y3pAz6le)-<{o->(9SW64n&Q0_~MZ1VwtBKVD_2Vo5Wu%ep~Azy=!s6sRP+dxylTbzh} zo;Y1ZkwT47$hUTE#-iw493U^>?Qsy>BNfA&R^~reek_BKY>L8GEV#&juQa$mRc2fy zm}mb*jgj?}c)4Eyjysh9@Jw@WjF>A$(Af|$g>7S#j1`;NYSa$SKSM)EG@_!S>Hk@7 zEl*0$7MCFSsv{ok#-3ms7EoR>xLFQV0T@C?Nn<*~$Ph}zZ@s0|{DqIF%;9p0xBagcD_ol&9dhpwS`o-wK2uW_n9$0Pq#Okghm>#`yNZ|h3U*Mr%tWnzy@e=} z(o<_ai>~RUlK3w^OD-LGB9NoZ4gQn1DZZn1>`gXj^GJj?wYyx&hNm?gk`ClUCuK=feffZErK z#{6@jRt^tV1@M5d*ZH(1z`maT?Pw+6{PvaF_9>_;+0WP5npo^N47fhUIWeO=^b?8kPo&9ZDsJR zO@nlv{QaWA~Sk%0QMju8Nxx3si0+gCg5-8!rAJo^AJO13A9 ziAhOG|4UjD`&mCkI5&>K>#hLFCBX)3$Pl1#+fxTMYZxEDiM^__i|0jRt!<*7!^Qj+ zoLwZxfyIb^?<#y_y&NV>>nmU$9k}rX)ZB=5T)F@inZ&CZi^?)OXd5!de38$Q%R}=b zXYCGHJ(Jj%1atzpplk8nGcn7#1C#$4C{+GgY=5@Bil-7~5?&CrE8_`ay*zXk6X$q} zR~2{##UG;_>$RWQVXjY;q=k+%cOrgLL1jt$6&gg>d&~)Ar2EAp0kD>0=6F6%@k2iL zeYoiG!xI+oS&1G3Khx>j^Fmp-4kgaC-Rr?J)zX3T9LHG_B2Sul# z>q#@f_Bko{e<byZ>id!D zpC5&NT+4^-4tWsKro?uLC{?Rv5AP^0(C_P6T(U=rV9OGGMO9oH#bEl}#dffC?r`ZZ zLqBHMOMsj#O-fX=A zh6)`s&->EPia=@F)=YL0y1!+*KP5{z2xxZ|v?g`I+_ZR_ddU?E%C1TRNV@RdUwm`e zQ28zyGY0iQQEFha_#TW3t`%QU?m0#qx82k0e~N1PoawOI1ioj}xe(Dj3ca6vJu!9B zu(G8Mw5EXl&mY)fwr9Y4lzLde4CKg}1PNCA1v5LA&vyW1b6gT-jMi>N!siia67MLP z16CSRc`Mn(XjG2BT##hE(9+2~@(>QxJbotGSP036Q{HvdEjgHJ@0=WLpdP_d+ProL;v7 z>Ce9U0%3PIuY4r+wsuXdFJ=K%q_LF2)A_VOWV;_Pt=o3=ksJ z;0%4=0vmTghIj$&NBIE_!;>2_V)pBuf-H~ioUmk06S(J9QQhi~xIm*@?(d@hUv*Lr z+XPbKFaJQ!z8e*coSrv~{wPNZGrFS+!E`Gh z-{9+PM!bg{IzFO3`1TotAnoR%MK7*b?Lc^RWyn50!X-ZZ*Ei8RBp2Sv0ve*joHU-< zLpn=MSf*7Ml7AEUwnV$$|7-3$fTG%>EddpjtSCVQj3`P*qBJOAM1tguNX|J&MFAB- zC5dDaBnQc{$vG*pX`)D`$vMux>I;8ne$Ah%KUMRpcmlTF_nv#sK5MVN_BzD6)jnjA zwrzrB!)VZX)YR9nU<_k)axh?r0R#K`#zHC2-#njR+wmwuD$8eYw5z}Of>XgOjVpuw zeTgRgyPKc21}qya&)IW=j3HZ9=RzWK0N6hS;#*EJ+_*OdV`m<)~hG#?@f`#vuCD8XvT@2wW<*t70MEVDan#1s0kw_ z@dctuQCgb+hYufo7SOxPlFTyE{;dXw9{+53w+o7de;9#JwyJX-8xr@J2+_Neh8OQ9 zRJJ9Iq?(%*n3uCxa!#?rI_?UsPRz8=>SXNQU+LF)+eK8$D^#ITEEV?Cs&@9_K(%Ux zZ;>034?oS1uMtGSdm@`#r%(p(SQcxewhwtFK2(}=xb9zilKZZh;?s-y*d3@qzoY#%X~3E zrZc({OKKHXF`xIwTsQdhbUC@ z*tT`y;c$1xsBjHU|51CYtYXvyBY7w%Giw)Yx=+R~#X@wRYj<5D9&vMh5 zJUE-3*(SlSYR5v>D2Fty)*>!b#@{bSyUeis7#j=c2j*IbQ@<(LXK%JA{Q~^=H{s{! z?TaD6=t5_jzGc-+;Mr^B4w*jvU+WyUuGxp0q2(Xf)LscXrap9@b6f0zXqHgFr^Rcj zUhM$sOcHLI>DYW7M1Ea5udVzexhFFR2otxcrMG?b0R>yBU zONHY)Ouu@pqUZ+nV+n*(i>w%GCZ=jnA|SB(rivjMqEZ5KdU7Jdl5m8>bBkoK*#1z( zF4**_8ov08@H%R}XkAk=Mb`nxF7IAGOfZu9LbLfab`zB~gKF5L>wF7m34;xbM zpeox}x+*>y#<_)&&sb`A?DpN?Ly($N@qV%1Yj$Hf^%R~^c|rye%rHy|#2m`dz~HM% z_f>H9lY01&6obK#?WClpI?oqP(hAub36yj&KfURNgs>wd)bI#Sifm8C8<+#Q{2*w{ z9OJfp49UyS2)tFdD7s{1E%s->GU9=#Uyt6RPm3mz9QI1f1`6)#3OzQ&qIMT2E@60Z zw7ShV@|$cnRa3sW21QxJ@YBQBBM8Iyw34oxk#hHjH4A+326DVBc;@lfOeGGvsRv!W z0UIs%EbfoGsH~Q=C3tDW6-#EGO(W|;Y4cT!Zf$VNYP2Y?BksE6>8BfoWI1_sM@^p;j|@5|pM zIgWI{`O{P7=a4P*c&g|v4>{y~`*<~a;zJyzMrnncin*4If1u+@1QCngipz7rYAXz(AAOA_;Dk$P5bri_Vi|@>w7e(6gZsG1cQ0{h18+} z$#y8SJ*Ajb7T4T1xq0`}3|q}Ib>I;A78Rz{njE%7W09q!-UrNn`4;oWkSm92PnvPw zo8bitM}9T?wQl9k_`4JtW$XP)@RHIcLX_*e+0m=sZ8iJ;OgWv6IBvMPwr zeYYx5rZUm;>{YTnQx>0s#O$aNv0m+=>lE9OM|Cp@B1njpdXIc?omOi+H>(_U*0wCM zQd>J+tD$Yu;)E055u(W11Kx-BPoEUD39on0#AXl2=LFs&3TWlqI|D_zixIf(okPby z?zk<^*3(`Stq9*C3H{pIiq#8%<`}{#GOlT%4*}oz0v?8bMS`zH6a-(<&5=WhsSBXH z>Ql-Zl|1dj)kkPFz%FCVn?mGq!`pKY!?`a-YWAgqV$T=Ed~cyZtIJoQ5o9*m7WH#t zV!32^F9TDaty@o!<+SwV7%Kui;N8XGn4F;fPdNr?&y(8UAMbw1n&hZlETPACgE)m> zXO9LttOb?>g&vofzGy3!n!;}@TwML8N2vbXBlqHnC8^NPfDrAx?d(W)trm@d!*#+p zt@(#I#=o6Tc)Y@&wXAssURS7CP? zGEmXo#p>0C;g}fD{cv!Piguo$LmIIWJ_veL1ZR@s5$pVF%nK!$(AFT>x2ip*eEqPW z`ungjqMJ~&%yD1CLT}fGO9l>GkOO7+So3R2X?W0;{bR{V%|%VcsZ7gFgh?y2pt=pC z*r?(Y>ThV&=!E<+)$sc#`#L9|F^%8)e9HR_gkMD8crHmM@gCF$@tt^mydt=SHeQ5> zHny13!-Fnw9lc2uE%msI)c@m?uKXCzNNZ?Gh*Bu7BnZ8?*GK=NjhWmxN|Fm+Dqm^x z@JK2rfz~=H#ZFGZ#SUBUl=eo-kZ0NET!-C~3g;<961}Hv*b43q&HmLoa6y+&Es8ff zP6Vl5HHUFbBD(^)uQ${#W3%UK5nh=~mykkuvBHDnhq9VllWriYXJWlq!R4=Glrv6vA1(lU*BFivoV^q?N$4{@e-Z?fIEeGH+t7?d&XATU)y`=b8kAbM@M_ zM-O!J7!SX;I8&v^`j<)^#Nu{;eiG6ZZf0op*%#9 z^!fOQU?S}NDcY3m+}KBoUi#lemoi<#X7DE;4KA3qT-u^7DWI~dq4Ilp8}o_cb&Gnu z(S=8nF|}%0q^6iqYV78jV_mDlbe46r!r^0C!E+U*Ycy`(1UGeeyoQMr^ck>A;{K(* ze_a=%UKcVI7DsK6nm)!XYy`c1i|5sutUSqf&2%?@pukc)+GBfqPs9Tx->E?pp&Cx3 zFAz95=s3KzyVA}B@!*NB?#A|+u;wFPSe!+EW_~_jl9VYD{UD>G%{8xmbxOm!_LRik zyZnbsZan+HZbMKMjb1M=c)T234K|x*`^4H4DlJ0~{56}oWXW)?idzzF9+3^&!}a_V zR~3fkxta>%g>P`V9Z71?^Sei&4y!xEjwms6p)z;P^`YVsHoiv&H}agFI&=2_FAY#zM{vHhE|?Xx-E-|lC! zN<6R~^(?5W%dC+9WtXPvPZ=v^Pt7b6By9b~_Sf|F^;hQy zOEBf268%=*5c7Xk#Hh4UDGPgTzJ z!A-_^iokBc$>g!ABYt*ZVPge4a46~No1sHzK*#-0*Q?*zi1pN=cbt>3(GSf0Xo}nrHvB+tm!+PN-UP6jNnq zbtBK`IBZCuF#l|sp%oiu@#1#-^j!s^)d+$^?H!mh#S)zjAK$ zO4W6lAo6|O`zjPm$J}t5MBI}kK!8Cuppn2ap>4pSHvVhw!*}@|%Rc(Zlb=BK*;(l-5;QT-*KSWK? z0t;Y$4#Ut#{|C7X;JrkRe0DwO$;;|2&M`+5mEoPAr!S-nt=;>jDT9kE*S1Ifz01*0 zwd!ZLTe@EqzY#Me^d=wpm?l(c_5mbpW~n41{CmZX&u%@%+cc?APu&?y?Z%%QG>v;q zb~3GpuF?G1zR(k$4pkwj9uY-@A}smP2)?KU^K|TToB}^$Uny|-`5H$mbhtbtVIPTd z9Jys)Zi>35M6PR^IG^ z$Ac1Rwo_TU@m00WS0;OdE57ruR#V5(?7wa#j38l^j@mQLo^8=jl^11Y&$>Hx^Q+GH zORStTO-5apRt^)mR8~AwEY^lZ(!s_w@4fnopkv}~zCXc^(l?XPhS@95u0ohQk4yJg z|LHlzfJ&SKRa3H1-h3fYxa$7pD2appJ^FRQGvsXoW27Sco4S7kHVJ|7mbZK=q64R7 z3mF{_QfPKl`;wW)i)0m(XqHx#_(h?UP^sN`M=+f|>-IC$*ZEmszaH2bd@V?H&#XKR z-2&&5IX;v2tOb>w5^@^AB^A#Zh)N_(yL2EU@yjYj+ZD&lUcUJR(6bt_Pakm$uV zcdh@Scs{YX250a_a(!ZN*hy{MOi%v~ZvX0WMrA`v;S@tDdp80L)I?K=`wuj#)0!P> z48+|i?#&QAq7%A?ktd@D{Sz{GV&mD)dA@sXX(Nzqek|g;K6t4 z(xrb25Pm)2Xyi(o^i5XtGf%i~PTbC6hLh}A@~za^MdhT1ZcN+g%w0R7nu(h;XYVl* z1l)`2hSrDeh5!}62j9%0D?nUu%p@{qI&+{E6vbECX#C_Y>qp97mNs3RLv-j(g zL4t(PBe?0Y`oKx;gKZlAVK>_xcerb$zc146`~832{W@p(Ss_JHQ09}zJH-#tgrv}n z^MqGsd0`HFpvi@=i9YVgl8tF3WhQytLo$<$7rXk*gE^J_q-(03y3D1*6RXuXi4qQp z4AxO6H#@@fwnakXk60ww;tGb_HkZc2uN$8T)37;)2Sy3Hd)sTmnH7zGVs!{6M z_o<3()CsdkaznpXTf>VrjyD5DM&O?OynYep{m= z|9~>?arX4-`t)O)mDk}ihXQf@#@Nb#I z|xgizWmwAj($7-&|oi1NT}p8^3GFu49nSBgBFv)^3tR5HSO1V44I0^ha&$w zdO_aM@pn7epI_C)T>^P1Xm`93V&msey%q!89N*gWdAa}kXOur=PxK_sgRfFEgrNHm z9uSY!y}7{7&i;4Z_V>Ge!v|t@e^T_Ik*oepHE;~Cx=qL?lmGorVs>w{LaVNDa7+%^ zwud0K^1rSP@}_f;H&HN&L_)&jyYk2N?$2KfsldEK(&BdE6;jDi)?<^#|M6{OvwBpb z!2^S%eDgdcOZw0&kKn)@%$cq_PEswmrCKWo@qa!G;Kos))=I5L#S*0=e0pccAzF>W zmub%(0tAix{qEohY|7$qnps92(OMTNXba|^%#=s3yKe67|1*t#y;{QW1+T92*7A4@ z2o%9%fj9Dt;6+76Mf|KMwXeugnZ2_KrKOQmQ$}e@roPc7sejtk|Cgrqv>1|}cCzXO zo{4B1_rKD*0Dt{|Wp_%3Z;-A~7%ymC(aphBIlmI`fg8aq{PF6VP|>f+K=~=B8{Wp< zE!>qDxg9^*Ruox6FmdBMH7l5@2t7j0s^V39qK9_<++_}qz(A30KSq=bk zpBz!vus+f*vcu|4L@}TBp8;WL1c!Tsdj3m!X~7q(tE*X2U;YbM{$@RZNi-VyNXX%B zx)qhbmAN`1lh5HUle?WjiM8wl85u(`(T{T2ukoMsXgs~d&K}_Bca-_zhYDqF z_wFCo%}q_4z(YygxkCs!{nJB4%OFgR4<_D_oS}rA9uM4S$w3%U!AS~fT7K_32@Sg* zJPnE4w~sS0v;sh=`JN!DZWa+1Ru9?yLpiw<0RG=yxVyZLwJhlC!8zE1%&-Gy$ehCs zV7`71xh~Yst}%{Wn-jp%HA}J=eeO10huCD?Q(iWCev325j~|DG8*+}xEJ&ZSO6RRqVK~q`g9}ptD7d(|3N_fb*JkUXZ=$^|>V~=* zN3UUXKToKQxkFlpmHUgJmKz&^EjPp^C!*sn*tU1Pa|Xhu6k6BkF~paOOQHMh4*rZk zPn#?l-$xch+g@<(lSlQ8u4&6Vdr^aB-UP8Q;IRhn(G06jAms7xH}A{3Ra;-53Sry+ zU~eP!OSH=aGJ3v;s;ty&Q0*tmvo9Gm=}LOk^dviJHlsuuwJ~7(>jS)sp_TZ?V)qM* zSSTuV|7;9W)X<1*YHT#l!v%`&C(SvO%zTUYb%90K1qkkw%j#4dJpbQkqZ zA%{$b39#0S-d9BJ%;lK;sCh{&%Tcx}m!k{P>}g`zFinSW7pQV~gMKg(kJr3EmlFMB z(YRejI)YCMzcr&5lVg|)X*{3laP*Qhe9*~QX6eYJX!mv1U+)T@*-7|^2$PzcnkdVE zEEbK2sw?Q}eJ&n(M$$DFD4GFeh_OA;(^qej(Q!*CDl7Bnl1;i6r|!<1w>u3vPeu{T zLU1w4F|}Qr>4wJ`&E*0?FGPpdq%TVcGU8}CkdLDuNJ}dLpK_I-pMP$Q@~XDm(hDK5 z(C7v&X9`ksS)0pgPZ8Z;3cPy$ycqavB=7F-`sCZV&uhBY$z`+#bf3z|w0iG6x&P}~ z4gj+>4-Od%q-^1iKA|lrb>^qz>=xrqAx)g5_7mZTgyx>mOLQt$3nW?6GnmSR^q@+Z zPj|x~GUw?vzV|Udb?VgB8#mO~raSK*A$aP8lR>Qo6Cn4?Xy*?csk~~sQXUAGWbM(N zW&?%UqqDHo=IzCVOuDYXKTQBG{9BfJeE`GNt5+W|z-Qeuw*)7sBS}TJlYC|?TW-i~ zxGEx|rKq5A-RuB5#Z&R@!4HXuVQhOgQ`u`9%#_wQ#_f~V{q^^Lmb3Nqh(LKvi9Jm6 z&1v2TTW;k(INQlKMX%z8@}2%o6QSN8wXYRn{Bi(td6W=zQOO8@%VN`5n9f)&DL{>> ziazZxbzTt(#71*1I|*M!p)~ak4T;JQ!dO*5J!B5r20;v(DRXXQ8Cr~3ZlYE@#L2Kf zw+BSo-Xv&5FnmeX){Z;7(zbVDrLE(A5zv~*01sif^oHove0K)q37R!E_BcimExeJd zl++uWB8?e-^p@1+wYd>F(MrdX!CRJXiNFJf3|c4$F-w``FF^fC$xE%!N~by>0MCOW z4R%F&d8#f5GidX5$uHOXu+QxPE0U>_xE9ZSZ4QR{0)#UemYbVfp*`%f>pdB} zzY=qB-3(T~#}~Xjb8LQWJ~rQ6-m7@I>DtE1Df{Sr>G?xXz2K9vvtLSabTPn7XyMNw ztc61)Y_UoEdpiXOwPz$kIkYqI?1A~cl2El1Epog$R@*z$-kmF}kCNH^;-YX+_)w$bBy) z;ikUDbCHf^o!;5*?eFhnN!Wxagx^z5CzC6dwO!%dE>pd<^aw@WRG52GIvwc(-X3?-ojDLi@*~C|EY{;wcL35 zkd1qOeR9i?E6}8wPURf3IR;&fnul7*t_H6BZVB_sNs<4kcxn za({nPViFSYA&TnA@ot8j4(BzcMn)~OeETZQvmd}V=~X>HMivxaS}J@KrvFUA_{0Q( zEXXvZq@-S(;~Z{)eLa~u1rd&}WIZ?Ca##+#y{>5;2}S852>edl(_fq+q9bWNqEA4y z2@soZ%VO94+so^;Sh_nVWXCpkz|G1vONAA2f4J#1+^Mp{puEM2Miy>rpEe2+7vV#p{iF0Oq|WwdFR zTwHuQqP6E8oE$K>WxRv;;i7d_Z&--SJS!1G{l z6^ErcI%uytT#oA1caIQXLq{zNySgE$zjZcrf?HgL&Dn!{gOnlMkx}4xQ1`xH zXF~&KKj8JlHV?DQ+~~r`5;zJu3uCssfj&5Lx;{B!X~33sqbI+7Z}j?n-070g42-+x zm~XQHv=&C#wxhyemp(kW{XKfgc%hH!>i%{wdKr3ZxpnJEEy~tXv;&(OfgHdJO<^f>z4_Zf3x~SF?0`CKYkIhVscFFO3*nc4oHO z8;yw3Z*VICFtZV?QUA8caQ4ssME2LMJT7`IDI@bGD=Vw^H9I(p)zs98&jiH<=;f)= z*HNshh&30S>at8lubpLQC;!rkd7@r`mD17CNiFcRzx~(b?N4Wf1@7A|;0PJl*{RO& zzh>zWr@s%M?<)XP)^IY2&2&Q8G6)|J<4BNgdVhWp>ohNA+H0U*pHzmXA-^r|L#ng6 zIJygw_3+PaVM^D4@u7x1Iv35oZDJa@+<=xFHv(Ev5x?gjkK4aCfdQab0l%`U^$w^dKj<+`ac*9}NNagI~#gWQBoP!5!zy{_ttSWdNoY4>fk6*Zx1zWEH z`V2Lj+H<)X2wsok@8IVOIr!*U)f~Tz_ck;Xs&RL!Kg?sOkDe}pzkC-F(Y%hsdiPrl zmq(SBmR6fY&RU9_6(`+xtuxdX{Zj6}!#9v&dj*U)s2vCGX*v_7#$c&caQesYArH28 zxNPTidzmq=-)xo^H`@o4K7^xMIsamo!JBQMSS85R)ytNw_x$X zE*Jo?@$v83=o14-ayBd4_3F0-Htli^I-j^U;gyzH1iuv$bb-~!jbuZjz^bY$>VjdX zu{XrB6sT3%>U>~A)e*R1H~sw|xG_Jd{u^$HY<>O%HwJ#e4OZ$rfVv&!Z>9O50Xifo zsB>l?V0jAc4IvQ`*eo`Fe&yS@Zzqoh++a^K-|K6Sx>2AL(LO03f9H){c6N3O43EwZ zWOSeZBhG-$jC?V~zO?KOR3s8ol9P?|_n_J25nqJV+QP7?k*jOnm)O{wf>F}EE~`f4 zD_f`Y;jNYNXQEx_1-+EJvmr>bA4z)6UVLwBZ(j-fViq;n?Xi)sFI=XTd6kXLJl`EQ zWCZD4x_B{ZW@g3*hoid?SRW6C4h08?e7FIlzH+;1IeqT%uGQ&Iiq%UFAEItJ<`?dz zW|qq_2i=$Ycs*GsLLIQ_XG$AuE>=9y|3D~hu(j@#kp87 zy8q}8gcm(1Er!0-5PH0RU1)5I#W{R6m~$ zEzb64DqCAyUlkNoMZr@3g;PNQCU20V>JEFtt$IN8GkzBWWT{`@)`Sgi1~yn_!$@l% zf-dd-<*)#caGfQ{ev)11i|!6Wph4cKwyuuP?ErpzBySJ!U2v;`Jfva>I!yjXo=+4we$BJqYIA12&Am$?8?%qh8h?ufzfFM zKO{0T4WyD?040RXkPSp^n_;*i5!AWkwk7FNB@M12q}0Ht}Rwmq>8w2Ux{c=)*h6F*%)0R9YCIv0-S>k4#H&8f}j<>j3L;HxYsDA;6>^DCzV0!-am zI9j%RAj@P{WqzPYUK7g9DVD?KNJas9C5ROg5H3B>p!Fkl;NZ{_qSft&9X=Q!HWasJ zKrN)WwY4?sgwf%Gq)(~-Yl0~ck`XMI6r_)-s;j3$tji!IJN+4|)yV2dPEMXl%W0DX zO2BNX562J>6GZk69nT$D{HN&YQ`>}QS@S?QZdJCL1qQqO@!<6OiJON9!Sv+5;%hjp z@*Ye!1%No_Jz5#ThO1pjfw)ht3((^yfd~fTx8mgx7jp@LAN&s`ta%C{WA}Mx&bDL*5_8H zR+&)A4TR7q+nc_MCIQJW+S@t0ON)DBfd5xqMA5@~FxQ)BA?F1D#u>;y}<%B37)sXuteT}qo2IAd4SZeoe>F%U<# zATob5c29By-ULxf0Flyx(L63@M-c9s|E2{+;r}Z|iuPp=cjbFu^reqR5{Pm?aSV*& zCM@S72<3vK+(TFm-3`g8-Az+4ytGzF(oN38Um{^Sni&qctoAdOx~O>u5MHz_fZ zFb6Y$88rF^>7O)63c)h6>f8p)5^!C}Yso*R`6KMJO#;lTkMFDA=Qpc>&@54ASw8AlD1er;NBa%;>{i z8EJn%1qvzn`J$~%HU|)iHTZx~5uk_^LEN`jeQ=;49fJU12r+QLu2U+iPyCw_x$mLW z?tbK=Gc-%!PRu)N%0GSjbjZbSsz1+^23%WP^RZ@pa}GNqF&tyAMsH(+C_kk_cHVo= zw!PXLhLU30_Jl7F6!@&Y?)cMI0+rGP_f-dh+864W(EMUwQ;eDV9AM8sZ%eS$DhE^lmb^~PXAdcSAi(4Y%JGy zQX09$uQ@r{7&FLUc>uH%6BDne%0R}+3*ubMAz?6L?&ow^+e>S&>B}{yg4^eVfkT8h z!k$nch_tSwyLj>9h?M|h%;mX*9bVkoyEO@GSrR$r@Y8u%)+bI-QN4Wi>Iv*obNzWF zUXbYp0BbO2B{0>S$pkeljNt*)g=(SMnz#bouCZwcYXnd65b*;Lq;N~%8OWsNzSa%5 z1W6d|I+r0Yd|f??i0c3-^6bn>x=WRTP7;>H@bwaM^(GSxc9W0*pV4)W%ZHRs44kyV zPy!OHFxc_O)?D!2_sj;1d8gdW)2BB9z}d8L)gy1&JpKvx2MvfJPQuYXd=FR1j5 zyK%E~+i%B(>G`%hM!__XV0O4f%JOzP8l>XOlJn&|764JKgrX4F77*g%Z9C$i*oVvn zHnul_dnUbht~{S|(VG`E&2)#Q+=99_5zW03jC^0UHwJ z+0mk0t^=1S3Iv=m5UjuMH=M^8=m^22J$QG)@nHEvCVWBFx*TP90zQHQavZq27(^;_ImYchth{2WJ(JY@ zfX;k#tN^e%z+*g&5s8XMxqj8DCPW_IL~BGnG|fspeE6`WZ4mA037m5)EY!O?I_Cg3 zUcp1er$(TdYJ@fTOUeiV=rl8{=-w(7pphE5G{A2`A3ruB9xQM|dev9`W+&hoBcc{Q z2T7j?0T%#`2uOkvPc_>*IzuZUN0*@@2yw{{=p0zy0?49)Te>y~GNrr&fRfB$di@g^ zk1;SFL(cGR-KHqk(gU3kGE5F7Q2?kvLM7liIAK_KJ-W-Ai%bS2b*|{k)-3`80_F&q z28vF?(Vluh7`0k(naxba2O5CrLj&?O7+#wlT*0B%%xY8~UpX>{jsw<2K&oH$gty_0 zXf+!u4F@LmheJuWS~^=a1&}AknUjeBW)VChI#|nq^cVo80XO>&B=BKymnEZ)l>jMg zptZ!ZO=QQ69XRp<3x3%0gustn7&2o<0Db*h?VXIHGo6S00t)L zf|V6AykNOv3v6aeMutWH9wf4|UfY1fsB#%(VIG=pdf@u=5PloRoTUIen1zCzu(qZq zDKhd5ByP{5Eopt!vL4^+u!bWr_4Kn<6d+-&&WAjjrDQOJ&tjl2t95KC35;A=ZVm!( zzvJc(rL->uT`&vP2l+u~f9mMr)A<}~x%co*KyE~a_1NDsJdyD*3fd4QxQJL|Yx<}@5yX;5B_s7vOBDXIXNm(7Bx3wY}x zv*AMPv0#ju65uA&KI)@~yFer!kGBR<^SW6KpZQ0EE)b=ivhznuDEL4}N&*PxHPg;F zQ_YEyGS%|f(m#>}S#n;nC6pF!_2MACK$0x<9?p_JAJ#8&pKYQCy9V_|aAIn3qaJbb z9rd7~pm+fAeP*tbx}^n63?goYGsEZukC;qeK*9=MNEt#A$ij~Dn`b)~8J%IGoQ#Ex z2@}hwm8r~NU2{AIpxUj4FQFL=`RiU(P50cVGzzUU+hc%=e86G>AwG8Q{pzY6vJhaOY=W%uSu|4K<%W~gW6gm+t(sin z9bgZMiD`%Og5ch2=QOa?;x|&en}9&aPW1Lt_iDMC%Q6(e96C-%_lq3NF9U=I2s78X z^9m~~sS6^hQ&3P?gwlymuC_Vdm#<&Bps4&DU?lJY<=aJVGQ`roen-_KuZYdmbJtI zXR;JJT!vYh?7}QG_h>+VeXG}XZ^sOvivJnGuR0~I2D+;e<6UH29It0=qU*;mxSHP2uR~_eO*((kLG1wGAMqnJ{?*IDrA)}?x z>cYZ8hwjG%5j}NI09DC`GgWg4y&8iq7q~>q9?Zp!FC8g_xEl_89~JY%vnW&)B+6^Ftfbjjts(zG6=7DVGMX>J)zyzh^dU6;D zXI1gzd+x|eMY0kE794DN9^8dmI)G~Qa66)dJ4<=gIc6h2>oo%Nv0**iow*zjSRfZe zsuy|gl2rXxtEtArGY4ghRV3kj=4!9aXLFv3wT|1om{UnF>&f3WtFj(Kd9LGSmaU(L zt%fb1qFy3BZwUoEIi}G1!T~^+56~qBF0cR(hDM-LK90a<>$Qb^laDWMo7M40XL z>G^cmjXFFw;^MbYrVNWCcmKm3)BoYRCZ2%|&OPSXISEOpteFYs)aM))HBumIHg^UhzHuSo)KNNWZvBLFebucA^ZDM{h z#ahru#Z)EUW(CzU7kk-=^(3^E5oDRI>v_59yyjaJ((0g4jzGM1=H}*NYpI0PVVQE0 z9ty0_=}q$qkUN-ONl{Gwh*K|dV0(HqwpxUZV!WO4EEm^OEUm6rL&H6UFhIWW_Q4wk z0U|4qbJ%oB@#Et>_h4a;!cgmAzUuokqKdm6%3L=ldbK?fy^wiZTAwzNc+C2w(2U=P zzMGpcG!wMu7X*h~0?pQW+je*0p6emiL74RVwfhs%V!2UWNP2si03U(uyB?xY0Wzun z;sFE;&xN}IKY>}&2dJ?l4E|(w!2ixsCY%HnlV!xVb-(c!9a=*0D;xH<(1$9-WyC}Y zwk1*~kyEFvAk`BzAJdn83U^7*!`Lw}SRz<`(B^?gWA?2E z0(z){-idTuZY+hjXI06H#dsu~gxc+V);=5q$YU`e0CEO#r3?+au0SG2Cp<5nHi3BY z?Gq_6d$8_bgJcYJyKquYB4!^V<$%QoepEaP!gqu@JOx{UgC%w+87eiD4FDEX0Ge7ow7`^qPDs!n5|y+Qiu&Hu!wf9e5BN|MlL!4+>^Ly9D=7fFn;;1Sja|SYZA7c(RAQdw0&>-d;{w`SSGi^jE`nMpznoq!bjA+S)M#Wv;!3o10Xp z>FF;4^nl!_X56L`>SD-fXrVpW7#JAfGCyZzwDE{syePp+{o3$&=!>3?jn_-sK88>2}rhW1Hbp`1tp-Z~Dx+fSI*nnHep-wn1Bs@jfkC1nIUg_YeF=%@ zbAu(lJZ%;Q&EU206Lclf2V8}^_2e%D0@jO$r6Tz5Lzy1N8zNQwNgA5h7_-p0xN}AJ zv!Sc&>rEgpshzfKf;4v=ionZr7#>Wydr6u~w%8EblS3w2_+4dX<&hE_9j-3D0xWVz zDJdy6qgMA+R6c{*x(p^c6Ai)_Z5p6x%R#oMRB z1kDM0&rQ?ApZC`f?{gkmlq4i1?9$^-9<=CX67|?TW4pa-eCoKlk&ZB z$^>S7rq0eq0RaIienzUQ;k_!_ScCO<(a{-DEr9XMgL(y%uoI_gBzgzrB&mKzGBSa^ z>+|y#tt~AIL5-imZ>kHroge}wzw#>{ocC&%QtZY#m_B&$id{Yb;cW?1Q&UC7y!?En z+Y-J$J_dvJPqK~R0Gu>jR8&;4IhOBDU|=s$HX;lBTT43lN=+y1BHV zeQIH$7YK)TYc;e-8C;a=N?uypLwENxAmmH(GqSZ^NyF}z@qvwYZc?j3=i;JOrtZvw zK-<~?qljA)WMo}nWk*Iq(F-x%SW7Fqr?+>uz;c+N=SQZ6b|8Q663TYM-@wq&R7~ud zA-PbLG;Vfw<@CnV*xkW0*8;dE(|h;)hKu%P;HX}C-|9F}QJn@juat_EB~qxUsI-+al0BTP&GG;(PnQ z$tvkebPNn3^!yeLP)ddb;Z9pLihk|{{z!rI>eM^KZ_!ZKgSd4o{RFRK`&ABw z$9nSXWv>H6JsDQs>!J89)06Ena#~vK@U~&2qdJciAs4op>NuC2lyua=84nL{^YvR4 zKAwsYd^r4s{6~6>%mM%Rw|HI&FY37ed|yF%y7|wyc;f$hd0aN>pI>u6|Nr{M`-gnk WI+l~@(k+ndy?5fa%gbU=XXyn}!m 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