CI for csaxs_bec / test (push) Successful in 4m31s
The names now say which half is simulated: the beamline (ddg1, fsh) is simulated in all of them, LamNI is either real or simulated. Device tags, the include key (was 'flomni'), the generator output name and docs follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8zoDzDfsCTY2o5bT3J7Wv
154 lines
7.0 KiB
Python
154 lines
7.0 KiB
Python
"""
|
|
Generate simulated device configs for LamNI and OMNY from the real ptycho configs.
|
|
|
|
Mirrors ptycho_lamni.yaml / ptycho_omny.yaml, swapping deviceClass to the simulated
|
|
counterparts and injecting sim_* seed parameters (resolutions from lamni.dmc, boot
|
|
positions from the 'in' user parameters where available). Devices without a simulated
|
|
counterpart are dropped and reported.
|
|
|
|
Run from the repo root: python bin/generate_simulated_configs.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
CONFIGS = REPO / "csaxs_bec" / "device_configs"
|
|
|
|
CLASS_MAP = {
|
|
"csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor": "csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor",
|
|
"csaxs_bec.devices.omny.rt.rt_lamni_ophyd.RtLamniMotor": "csaxs_bec.devices.sim.sim_lamni.SimRtLamniMotor",
|
|
"csaxs_bec.devices.omny.rt.rt_lamni_ophyd.RtLamniFlyer": "csaxs_bec.devices.sim.sim_lamni.SimRtLamniFlyer",
|
|
"csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor": "csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor",
|
|
"csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor": "csaxs_bec.devices.sim.sim_omny.SimRtOMNYMotor",
|
|
"csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYFlyer": "csaxs_bec.devices.sim.sim_omny.SimRtOMNYFlyer",
|
|
"csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage": "csaxs_bec.devices.sim.sim_omny.SimOMNYSampleStorage",
|
|
"csaxs_bec.devices.omny.omny_dewar.OMNYDewar": "csaxs_bec.devices.sim.sim_omny.SimOMNYDewar",
|
|
"csaxs_bec.devices.omny.omny_temperatures.OMNYTemperatures": "csaxs_bec.devices.sim.sim_omny.SimOMNYTemperatures",
|
|
"csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor": "csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor",
|
|
"csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera": "csaxs_bec.devices.sim.sim_cameras.SimIDSCamera",
|
|
"csaxs_bec.devices.omny.webcam_viewer.WebcamViewer": "csaxs_bec.devices.sim.sim_cameras.SimWebcamViewer",
|
|
"csaxs_bec.devices.omny.flomni_sample_storage.FlomniSampleStorage": "csaxs_bec.devices.sim.sim_sample_storage.SimFlomniSampleStorage",
|
|
"ophyd_devices.ComputedSignal": "ophyd_devices.ComputedSignal",
|
|
}
|
|
|
|
# lamni.dmc #INIT: stppermm / encpermm per axis index (A=0 .. H=7)
|
|
LAMNI_STPPERMM = {
|
|
0: 12800,
|
|
1: 12800,
|
|
2: 50154.32099,
|
|
3: 12800,
|
|
4: 12800,
|
|
5: 38400,
|
|
6: 12800,
|
|
7: 12800,
|
|
}
|
|
LAMNI_ENCPERMM = {0: 2000, 1: 2000, 2: 36000, 3: 10000, 4: 10000, 5: 30000}
|
|
LAMNI_ENCODER_AXES = set(range(6))
|
|
# lamni.dmc #INIT axspeed table (units/s)
|
|
LAMNI_VELOCITY = {0: 3, 1: 3, 2: 12, 3: 1, 4: 0.5, 5: 0.5, 6: 3, 7: 3}
|
|
|
|
SIM_BEAMLINE_DEVICES = """
|
|
ddg1:
|
|
description: Simulated main delay generator for triggering
|
|
deviceClass: csaxs_bec.devices.sim.simulated_beamline_devices.SimulatedDDG1
|
|
enabled: true
|
|
deviceConfig:
|
|
prefix: 'X12SA-CPCL-DDG1:'
|
|
onFailure: raise
|
|
readOnly: false
|
|
readoutPriority: baseline
|
|
softwareTrigger: true
|
|
fsh:
|
|
description: Simulated fast shutter manual control and readback
|
|
deviceClass: csaxs_bec.devices.sim.simulated_beamline_devices.cSAXSSimulatedFastShutter
|
|
deviceConfig:
|
|
prefix: 'X12SA-ES1-TTL:'
|
|
onFailure: raise
|
|
enabled: true
|
|
readoutPriority: monitored
|
|
"""
|
|
|
|
|
|
def axis_index(axis_id: str) -> int:
|
|
return ord(axis_id.lower()) - ord("a")
|
|
|
|
|
|
def transform(src_name: str, dst_dir: str, dst_name: str, setup: str) -> None:
|
|
src = CONFIGS / src_name
|
|
config = yaml.safe_load(src.read_text())
|
|
out, dropped = {}, []
|
|
for name, dev in config.items():
|
|
if not isinstance(dev, dict) or "deviceClass" not in dev:
|
|
dropped.append(name)
|
|
continue
|
|
dev_class = dev["deviceClass"].strip()
|
|
if dev_class not in CLASS_MAP:
|
|
dropped.append(f"{name} ({dev_class.rsplit('.', 1)[-1]})")
|
|
continue
|
|
dev = dict(dev)
|
|
dev["deviceClass"] = CLASS_MAP[dev_class]
|
|
dev_config = dict(dev.get("deviceConfig") or {})
|
|
user_params = dev.get("userParameter") or {}
|
|
|
|
if "SimLamniGalilMotor" in dev["deviceClass"]:
|
|
axis = axis_index(dev_config["axis_Id"])
|
|
dev_config["sim_stppermm"] = LAMNI_STPPERMM.get(axis, 12800)
|
|
if axis in LAMNI_ENCODER_AXES:
|
|
dev_config["sim_encpermm"] = LAMNI_ENCPERMM[axis]
|
|
dev_config.setdefault("sim_velocity", LAMNI_VELOCITY.get(axis, 1))
|
|
if "GalilMotor" in dev["deviceClass"] or "SmaractMotor" in dev["deviceClass"]:
|
|
# boot at the aligned position: 'center' (LamNI) or 'in' user parameter
|
|
initial = user_params.get("center", user_params.get("in", 0))
|
|
limits = dev_config.get("limits")
|
|
if isinstance(limits, list) and len(limits) == 2 and None not in limits:
|
|
low, high = sorted(float(limit) for limit in limits)
|
|
# low == high means 'no limit check' in BEC; do not clamp then
|
|
if low < high and not low <= float(initial) <= high:
|
|
initial = round((low + high) / 2, 4)
|
|
dev_config["sim_initial_position"] = initial
|
|
if "SimRtOMNYFlyer" in dev["deviceClass"] or "SimRtLamniFlyer" in dev["deviceClass"]:
|
|
dev_config.setdefault("sim_point_dwell_s", 0.02)
|
|
|
|
dev["deviceConfig"] = dev_config
|
|
out[name] = dev
|
|
|
|
# LamNI: couple the rt interferometer readback to the coarse stages so that
|
|
# lamni_new_scan_center_interferometer converges (see SimRtLamniState)
|
|
if setup == "LamNI" and "lsamx" in out and "lsamy" in out:
|
|
coupling = {
|
|
"galil_port": out["lsamx"]["deviceConfig"]["port"],
|
|
"x_axis": axis_index(out["lsamx"]["deviceConfig"]["axis_Id"]),
|
|
"x_sign": out["lsamx"]["deviceConfig"].get("sign", 1),
|
|
"x_center": (config["lsamx"].get("userParameter") or {}).get("center", 0),
|
|
"y_axis": axis_index(out["lsamy"]["deviceConfig"]["axis_Id"]),
|
|
"y_sign": out["lsamy"]["deviceConfig"].get("sign", 1),
|
|
"y_center": (config["lsamy"].get("userParameter") or {}).get("center", 0),
|
|
}
|
|
for rt_name in ("rtx", "rty", "rt_positions"):
|
|
if rt_name in out:
|
|
out[rt_name]["deviceConfig"]["sim_coarse_coupling"] = coupling
|
|
|
|
header = (
|
|
f"############################################################\n"
|
|
f"# Simulated {setup} endstation (generated by bin/generate_simulated_configs.py)\n"
|
|
f"# Real device classes run against protocol-level simulations.\n"
|
|
f"# Dropped (no simulated counterpart): {', '.join(dropped) if dropped else 'none'}\n"
|
|
f"############################################################\n\n"
|
|
)
|
|
body = yaml.dump(out, sort_keys=False, default_flow_style=False)
|
|
dst = CONFIGS / dst_dir / dst_name
|
|
dst.parent.mkdir(exist_ok=True)
|
|
(dst.parent / "__init__.py").touch()
|
|
dst.write_text(header + body + SIM_BEAMLINE_DEVICES)
|
|
print(f"{dst_name}: {len(out)} devices, dropped: {dropped}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
transform("ptycho_lamni.yaml", "simulated_omny", "simulated_bl_simulated_lamni.yaml", "LamNI")
|
|
transform("ptycho_omny.yaml", "simulated_omny", "simulated_omny.yaml", "OMNY")
|