sim next version, including lamni and omny
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
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_lamni.yaml", "LamNI")
|
||||
transform("ptycho_omny.yaml", "simulated_omny", "simulated_omny.yaml", "OMNY")
|
||||
@@ -500,6 +500,8 @@ cam_flomni_gripper:
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimWebcamViewer
|
||||
deviceConfig:
|
||||
url: http://flomnicamserver:5000/video_high
|
||||
# per-frame noise in counts; 0 = static frames (default), e.g. 4 for visibly live previews
|
||||
sim_noise_std: 0
|
||||
num_rotation_90: 3
|
||||
transpose: false
|
||||
enabled: true
|
||||
@@ -512,6 +514,8 @@ cam_flomni_overview:
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimWebcamViewer
|
||||
deviceConfig:
|
||||
url: http://flomnicamserver:5001/video_high
|
||||
# per-frame noise in counts; 0 = static frames (default), e.g. 4 for visibly live previews
|
||||
sim_noise_std: 0
|
||||
num_rotation_90: 3
|
||||
transpose: false
|
||||
enabled: true
|
||||
@@ -529,6 +533,8 @@ cam_xeye:
|
||||
transpose: true
|
||||
force_monochrome: true
|
||||
m_n_colormode: 1
|
||||
# per-frame noise in counts; 0 = static frames (default), e.g. 4 for visibly live previews
|
||||
sim_noise_std: 0
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
@@ -536,6 +542,25 @@ cam_xeye:
|
||||
userParameter:
|
||||
pixel_calibration: 0.00044247787610619477 # mm/pixel (= 0.05 / 113)
|
||||
|
||||
############################################################
|
||||
#################### Sample storage ########################
|
||||
############################################################
|
||||
|
||||
flomni_samples:
|
||||
description: Sample storage database (simulated, in-memory)
|
||||
deviceClass: csaxs_bec.devices.sim.sim_sample_storage.SimFlomniSampleStorage
|
||||
deviceConfig:
|
||||
# boots with a sample on the flomni stage (slot 0) and in slots 1-3
|
||||
sim_samples:
|
||||
"0": "sim_stage_sample"
|
||||
"1": "sim_sample_1"
|
||||
"2": "sim_sample_2"
|
||||
"3": "sim_sample_3"
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
############################################################
|
||||
#################### Computed signal #######################
|
||||
############################################################
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
############################################################
|
||||
# Simulated LamNI endstation (generated by bin/generate_simulated_configs.py)
|
||||
# Real device classes run against protocol-level simulations.
|
||||
# Dropped (no simulated counterpart): none
|
||||
############################################################
|
||||
|
||||
leyex:
|
||||
description: Xray eye X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
sim_stppermm: 12800
|
||||
sim_velocity: 3
|
||||
sim_initial_position: 14.117
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 14.117
|
||||
leyey:
|
||||
description: Xray eye Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
sim_stppermm: 12800
|
||||
sim_velocity: 3
|
||||
sim_initial_position: 48.069
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 48.069
|
||||
out: 0.5
|
||||
loptx:
|
||||
description: Optics X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_stppermm: 12800
|
||||
sim_encpermm: 10000
|
||||
sim_velocity: 0.5
|
||||
sim_initial_position: -0.244
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -0.244
|
||||
out: -0.699
|
||||
lopty:
|
||||
description: Optics Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_stppermm: 38400
|
||||
sim_encpermm: 30000
|
||||
sim_velocity: 0.5
|
||||
sim_initial_position: 3.724
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 3.724
|
||||
out: 3.53
|
||||
loptz:
|
||||
description: Optics Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
sim_stppermm: 12800
|
||||
sim_encpermm: 10000
|
||||
sim_velocity: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
lsamrot:
|
||||
description: Sample rotation
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_stppermm: 50154.32099
|
||||
sim_encpermm: 36000
|
||||
sim_velocity: 12
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
lsamx:
|
||||
description: Sample coarse X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
sim_stppermm: 12800
|
||||
sim_encpermm: 2000
|
||||
sim_velocity: 3
|
||||
sim_initial_position: 8.768
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
center: 8.768
|
||||
lsamy:
|
||||
description: Sample coarse Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimLamniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_stppermm: 12800
|
||||
sim_encpermm: 2000
|
||||
sim_velocity: 3
|
||||
sim_initial_position: 10.041
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
center: 10.041
|
||||
losax:
|
||||
description: OSA X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8085
|
||||
sign: -1
|
||||
sim_initial_position: -1.442
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -1.442
|
||||
losay:
|
||||
description: OSA Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8085
|
||||
sign: -1
|
||||
sim_initial_position: -0.171
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -0.171
|
||||
out: 3.8
|
||||
losaz:
|
||||
description: OSA Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2680.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8085
|
||||
sign: 1
|
||||
sim_initial_position: -1
|
||||
deviceTags:
|
||||
- lamni
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -1
|
||||
out: -3
|
||||
rtx:
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimRtLamniMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
device_access: true
|
||||
host: mpc2680.psi.ch
|
||||
labels: rtx
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 3333
|
||||
sign: 1
|
||||
sim_coarse_coupling: &id001
|
||||
galil_port: 8081
|
||||
x_axis: 0
|
||||
x_sign: -1
|
||||
x_center: 8.768
|
||||
y_axis: 1
|
||||
y_sign: 1
|
||||
y_center: 10.041
|
||||
deviceTags:
|
||||
- lamni
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
enabled: true
|
||||
readOnly: false
|
||||
rty:
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimRtLamniMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
device_access: true
|
||||
host: mpc2680.psi.ch
|
||||
labels: rty
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 3333
|
||||
sign: 1
|
||||
sim_coarse_coupling: *id001
|
||||
deviceTags:
|
||||
- lamni
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
enabled: true
|
||||
readOnly: false
|
||||
rt_positions:
|
||||
deviceClass: csaxs_bec.devices.sim.sim_lamni.SimRtLamniFlyer
|
||||
deviceConfig:
|
||||
host: mpc2680.psi.ch
|
||||
port: 3333
|
||||
sim_point_dwell_s: 0.02
|
||||
sim_coarse_coupling: *id001
|
||||
readoutPriority: async
|
||||
connectionTimeout: 20
|
||||
enabled: true
|
||||
readOnly: false
|
||||
cam_xeye:
|
||||
description: Camera LamNI Xray eye ID15
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimIDSCamera
|
||||
deviceConfig:
|
||||
camera_id: 15
|
||||
bits_per_pixel: 24
|
||||
num_rotation_90: 3
|
||||
transpose: false
|
||||
force_monochrome: true
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
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
|
||||
@@ -0,0 +1,654 @@
|
||||
############################################################
|
||||
# Simulated OMNY endstation (generated by bin/generate_simulated_configs.py)
|
||||
# Real device classes run against protocol-level simulations.
|
||||
# Dropped (no simulated counterpart): none
|
||||
############################################################
|
||||
|
||||
cam200:
|
||||
description: Camera200
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimIDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 200
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
cam201:
|
||||
description: Camera201
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimIDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 201
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
cam202:
|
||||
description: Camera202
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimIDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 202
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
cam203:
|
||||
description: Camera203
|
||||
deviceClass: csaxs_bec.devices.sim.sim_cameras.SimIDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 203
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
rtx:
|
||||
description: OMNY rt
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimRtOMNYMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
low_signal: 8500
|
||||
min_signal: 8000
|
||||
rty:
|
||||
description: OMNY rt
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimRtOMNYMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
tomo_additional_offsety: 0
|
||||
rtz:
|
||||
description: OMNY rt
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimRtOMNYMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
connectionTimeout: 20
|
||||
rt_positions:
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimRtOMNYFlyer
|
||||
deviceConfig:
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sim_point_dwell_s: 0.02
|
||||
readoutPriority: async
|
||||
connectionTimeout: 20
|
||||
enabled: true
|
||||
readOnly: false
|
||||
omny_samples:
|
||||
description: OMNYSampleStorage
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYSampleStorage
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
omny_dewar:
|
||||
description: OMNY Dewar Information
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYDewar
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
omny_temperatures:
|
||||
description: OMNY Temperatures and pressures
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYTemperatures
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
ofzpx:
|
||||
description: FZP X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_initial_position: -0.4317
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -0.4317
|
||||
ofzpy:
|
||||
description: FZP Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_initial_position: 0.7944
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0.7944
|
||||
out: 0.6377
|
||||
ofzpz:
|
||||
description: FZP Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
otransx:
|
||||
description: Transfer X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
otransy:
|
||||
description: Transfer Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
up_position: -1.2
|
||||
gripper_sensorvoltagetarget: -2.3
|
||||
otransz:
|
||||
description: Transfer Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
osamx:
|
||||
description: Sample X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
sim_initial_position: -0.1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -0.1
|
||||
osamz:
|
||||
description: Sample Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: false
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
oosay:
|
||||
description: OSA Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
near_field_in: 0.531
|
||||
far_field_in: 0.4122
|
||||
oosax:
|
||||
description: OSA X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: -1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
near_field_in: 3.2044
|
||||
far_field_in: 3.022
|
||||
oosaz:
|
||||
description: OSA Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: -1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
near_field_in: -0.4452
|
||||
far_field_in: 6.5
|
||||
oparkz:
|
||||
description: OSA Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
oshuttleopen:
|
||||
description: Shuttle opener
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
oshuttlealign:
|
||||
description: Shuttle aligner
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
osamy:
|
||||
description: Sample Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
otracky:
|
||||
description: Laser Tracker Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
start_pos: -4.3431
|
||||
osamroy:
|
||||
description: Sample rotation
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0
|
||||
otrackz:
|
||||
description: Laser Tracker Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: -1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
start_pos: -0.6948
|
||||
oeyex:
|
||||
description: Xray eye X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
xray_in: -45.7394
|
||||
oeyez:
|
||||
description: Xray eye Z
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
xray_in: -2
|
||||
oeyey:
|
||||
description: Xray eye Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_omny.SimOMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
xray_in: 0.0229
|
||||
ocsx:
|
||||
description: Central Stop X
|
||||
deviceClass: csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- -2
|
||||
- 2
|
||||
port: 3334
|
||||
sign: -1
|
||||
sim_initial_position: 0
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
nothing: 0
|
||||
ocsy:
|
||||
description: Central Stop Y
|
||||
deviceClass: csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- -2
|
||||
- 2
|
||||
port: 3334
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
nothing: 0
|
||||
oshield:
|
||||
description: Thermal Shield Sample Stage
|
||||
deviceClass: csaxs_bec.devices.sim.sim_smaract.SimSmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- -14.5
|
||||
- 15.8
|
||||
port: 3334
|
||||
sign: 1
|
||||
sim_initial_position: 0
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
nothing: 0
|
||||
|
||||
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
|
||||
@@ -8,9 +8,10 @@ methods (`start_live_mode`, `set_rect_roi`, `get_last_image`, ...) and the ROI/t
|
||||
logic therefore run through the unmodified code paths, so GUIs and the X-ray eye
|
||||
alignment can be exercised against the simulation.
|
||||
|
||||
The synthetic frame is a static test pattern: dark background, centered gaussian blob
|
||||
and a crosshair, which gives the previews something recognizable and provides contrast
|
||||
for image analysis.
|
||||
The synthetic frame is a test pattern (dark background, centered gaussian blob and a
|
||||
crosshair). Per-frame gaussian noise can be enabled via sim_noise_std (counts, default 0
|
||||
= static frames; noisy frames make live views visibly live but cost bandwidth e.g. over
|
||||
remote desktop).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -39,6 +40,14 @@ def make_test_pattern(height: int, width: int, rgb: bool) -> np.ndarray:
|
||||
return frame
|
||||
|
||||
|
||||
def add_frame_noise(frame: np.ndarray, noise_std: float) -> np.ndarray:
|
||||
"""Return a copy of the frame with gaussian noise, clipped to uint8."""
|
||||
if noise_std <= 0:
|
||||
return frame
|
||||
noisy = frame.astype(np.float32) + np.random.normal(0.0, noise_std, frame.shape)
|
||||
return noisy.clip(0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
class SimWebcamViewer(WebcamViewer):
|
||||
"""WebcamViewer serving a static synthetic frame instead of an MJPEG stream.
|
||||
|
||||
@@ -53,16 +62,18 @@ class SimWebcamViewer(WebcamViewer):
|
||||
num_rotation_90=0,
|
||||
transpose=False,
|
||||
sim_shape=(480, 640),
|
||||
sim_noise_std=0.0,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self._sim_frame = make_test_pattern(int(sim_shape[0]), int(sim_shape[1]), rgb=True)
|
||||
self._sim_noise_std = float(sim_noise_std)
|
||||
super().__init__(
|
||||
url=url, name=name, num_rotation_90=num_rotation_90, transpose=transpose, **kwargs
|
||||
)
|
||||
|
||||
def _update_loop(self) -> None:
|
||||
while not self._shutdown_event.is_set():
|
||||
self.preview.put(self._sim_frame)
|
||||
self.preview.put(add_frame_noise(self._sim_frame, self._sim_noise_std))
|
||||
self._shutdown_event.wait(0.2) # 5 Hz
|
||||
|
||||
|
||||
@@ -81,11 +92,12 @@ class _SimIDSSensor:
|
||||
class _SimIDSBackend:
|
||||
"""Drop-in replacement for `base_integration.camera.Camera` serving synthetic frames."""
|
||||
|
||||
def __init__(self, width: int, height: int, rgb: bool):
|
||||
def __init__(self, width: int, height: int, rgb: bool, noise_std: float = 0.0):
|
||||
self.cam = _SimIDSSensor(width, height)
|
||||
self.force_monochrome = False
|
||||
self._connected = False
|
||||
self._rgb = rgb
|
||||
self._noise_std = float(noise_std)
|
||||
self._frame = make_test_pattern(height, width, rgb=rgb)
|
||||
|
||||
def on_connect(self):
|
||||
@@ -98,7 +110,7 @@ class _SimIDSBackend:
|
||||
frame = self._frame
|
||||
if self.force_monochrome and frame.ndim == 3:
|
||||
frame = frame[:, :, 0]
|
||||
return frame
|
||||
return add_frame_noise(frame, self._noise_std)
|
||||
|
||||
def set_camera_rate_limiting(self, enabled: bool):
|
||||
pass
|
||||
@@ -120,9 +132,14 @@ class SimIDSCamera(IDSCamera):
|
||||
num_rotation_90: int = 0,
|
||||
transpose: bool = False,
|
||||
force_monochrome: bool = False,
|
||||
camera_ID: int | None = None,
|
||||
channels: int | None = None, # legacy OMNY config keys, accepted for compatibility
|
||||
sim_shape=(1024, 1280),
|
||||
sim_noise_std=0.0,
|
||||
**kwargs,
|
||||
):
|
||||
if camera_ID is not None and not camera_id:
|
||||
camera_id = camera_ID
|
||||
super().__init__(
|
||||
name=name,
|
||||
camera_id=camera_id,
|
||||
@@ -136,5 +153,7 @@ class SimIDSCamera(IDSCamera):
|
||||
force_monochrome=force_monochrome,
|
||||
**kwargs,
|
||||
)
|
||||
self.cam = _SimIDSBackend(int(sim_shape[1]), int(sim_shape[0]), rgb=not force_monochrome)
|
||||
self.cam = _SimIDSBackend(
|
||||
int(sim_shape[1]), int(sim_shape[0]), rgb=not force_monochrome, noise_std=sim_noise_std
|
||||
)
|
||||
self.cam.force_monochrome = self._force_monochrome
|
||||
|
||||
@@ -45,6 +45,9 @@ class _SimGalilAxis:
|
||||
self.motor_on = True
|
||||
self.auto_motor_off = True # fgalil #MOTOFFA switches motors off when idle
|
||||
self.stppermm = 25600.0
|
||||
self.encpermm = None # encoder counts per unit; LamNI axes 0-5 read TP in encoder counts
|
||||
self.pa_target_steps = None # pending absolute target (PA{ax}=)
|
||||
self.pr_target_steps = None # pending relative target (PR{ax}=)
|
||||
self.speed_steps = 5.0 * 25600.0
|
||||
self.pos_steps = 0.0
|
||||
self._move_start_steps = 0.0
|
||||
@@ -121,6 +124,17 @@ class SimGalilState:
|
||||
"allaxref": 0.0,
|
||||
"axisref": 0.0,
|
||||
"frmmv": 0.0,
|
||||
# dialect status variables (LamNI / OMNY); overridable via sim_variables
|
||||
"swver": 0.0,
|
||||
"allaxrer": 0.0,
|
||||
"caperr": 0.0,
|
||||
"angintf": 0.0,
|
||||
"tempab": 0.0,
|
||||
"timeab": 0.0,
|
||||
"IgNoFol": 0.0,
|
||||
"grstat": 0.0,
|
||||
"relpos": 0.0,
|
||||
"toppin": -30.0,
|
||||
}
|
||||
self.digital_inputs = {14: 1.0} # light curtain reads triggered by default
|
||||
self.digital_outputs = {9: 0.0, 15: 1.0} # gripper closed, lights on
|
||||
@@ -141,6 +155,8 @@ class SimGalilState:
|
||||
ax.connected = True
|
||||
if "stppermm" in kwargs and kwargs["stppermm"] is not None:
|
||||
ax.stppermm = float(kwargs["stppermm"])
|
||||
if "encpermm" in kwargs and kwargs["encpermm"] is not None:
|
||||
ax.encpermm = float(kwargs["encpermm"])
|
||||
if "velocity" in kwargs and kwargs["velocity"] is not None:
|
||||
ax.speed_steps = float(kwargs["velocity"]) * ax.stppermm
|
||||
if "referenced" in kwargs and kwargs["referenced"] is not None:
|
||||
@@ -150,6 +166,12 @@ class SimGalilState:
|
||||
if "analog_inputs" in kwargs and kwargs["analog_inputs"]:
|
||||
for chan, val in kwargs["analog_inputs"].items():
|
||||
self.analog_inputs[int(chan)] = float(val)
|
||||
if "digital_outputs" in kwargs and kwargs["digital_outputs"]:
|
||||
for chan, val in kwargs["digital_outputs"].items():
|
||||
self.digital_outputs[int(chan)] = float(val)
|
||||
if "variables" in kwargs and kwargs["variables"]:
|
||||
for var, val in kwargs["variables"].items():
|
||||
self.variables[str(var)] = float(val) if not isinstance(val, str) else val
|
||||
if "digital_inputs" in kwargs and kwargs["digital_inputs"]:
|
||||
for chan, val in kwargs["digital_inputs"].items():
|
||||
self.digital_inputs[int(chan)] = float(val)
|
||||
@@ -157,6 +179,7 @@ class SimGalilState:
|
||||
ax.limit_low_active = bool(kwargs["limit_low_active"])
|
||||
if kwargs.get("limit_high_active") is not None:
|
||||
ax.limit_high_active = bool(kwargs["limit_high_active"])
|
||||
self.recompute_allaxref()
|
||||
|
||||
def thread_active(self, thread_id: int) -> bool:
|
||||
if thread_id == 0:
|
||||
@@ -173,10 +196,20 @@ class SimGalilState:
|
||||
self._thread_deadlines[thread_id] = time.time() + duration
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
connected = [ax for ax in self.axes if ax.connected]
|
||||
if not connected:
|
||||
return False
|
||||
return all(ax.referenced for ax in connected)
|
||||
"""Current value of the allaxref DMC variable.
|
||||
|
||||
Like on the real controller, allaxref is a plain variable: it is recomputed by
|
||||
the referencing routines (#FES/#FRM/#REFAX, see recompute_allaxref) but can also
|
||||
be overwritten directly via 'allaxref=<val>' until the next referencing.
|
||||
"""
|
||||
with self.lock:
|
||||
return bool(self.variables.get("allaxref", 0.0))
|
||||
|
||||
def recompute_allaxref(self):
|
||||
with self.lock:
|
||||
connected = [ax for ax in self.axes if ax.connected]
|
||||
referenced = bool(connected) and all(ax.referenced for ax in connected)
|
||||
self.variables["allaxref"] = 1.0 if referenced else 0.0
|
||||
|
||||
# --- routines ---------------------------------------------------------------
|
||||
def stop_all(self):
|
||||
@@ -202,18 +235,20 @@ class SimGalilState:
|
||||
ax.start_move(target_mm * ax.stppermm)
|
||||
|
||||
def reference_axis(self, axis_index: int, direction: float = 0.0):
|
||||
"""Simulate #FES / #FRM: short motion, then define zero at the reference."""
|
||||
"""Simulate #FES / #FRM: define zero at the reference.
|
||||
|
||||
The outcome (position 0, referenced, limit switch) is applied synchronously so
|
||||
that callers polling is_axis_moving and then immediately checking the limit
|
||||
switch (drive_axis_to_limit) never race the result; only the apparent motion
|
||||
(thread 0 active window) is time-based.
|
||||
"""
|
||||
ax = self.axes[axis_index]
|
||||
ax.motor_on = True
|
||||
ax.start_move(ax.position()) # ensure no stale motion
|
||||
# emulate the drive to the limit / reference with a short, visible motion
|
||||
self.set_thread_active(0, 0.5)
|
||||
deadline = time.time() + 0.5
|
||||
while time.time() < deadline:
|
||||
time.sleep(0.02)
|
||||
ax.pos_steps = 0.0
|
||||
ax.stop_move()
|
||||
ax.motor_on = True
|
||||
self.set_thread_active(0, 0.5) # emulate the drive duration
|
||||
ax.pos_steps = 0.0
|
||||
ax.referenced = True
|
||||
self.recompute_allaxref()
|
||||
if direction > 0:
|
||||
ax.limit_high_active = True
|
||||
elif direction < 0:
|
||||
@@ -270,7 +305,7 @@ class SimGalilState:
|
||||
if self.variables.get(approach_var, 0.0) == 0.0:
|
||||
logger.warning(f"[sim galil] transfer requested but {approach_var} not set")
|
||||
return
|
||||
self.variables["mntprgs"] = 1.0
|
||||
self.variables["mntprgs"] = 1.0 if approach_var == "getaprch" else -1.0
|
||||
self._transfer_abort.clear()
|
||||
self._transfer_thread = threading.Thread(
|
||||
target=self._transfer_routine, args=(approach_var,), daemon=True
|
||||
@@ -303,28 +338,73 @@ class SimGalilSocket(SimSocketBase):
|
||||
state.digital_outputs[channel] = 1.0 if line.startswith("SB") else 0.0
|
||||
return ":"
|
||||
|
||||
# FUPR: absolute position in counts, e.g. "PAA=25600"
|
||||
match = re.match(r"^PA([A-H])=(-?[0-9.]+)$", line)
|
||||
# direct axis commands: PA (absolute, counts), PR (relative, counts)
|
||||
match = re.match(r"^P([AR])([A-H])=(-?[0-9.]+)$", line)
|
||||
if match:
|
||||
axis = ord(match.group(1).lower()) - 97
|
||||
state.variables[f"_fupr_target_{axis}"] = float(match.group(2))
|
||||
axis = ord(match.group(2).lower()) - 97
|
||||
ax = state.axis(axis)
|
||||
if match.group(1) == "A":
|
||||
ax.pa_target_steps = float(match.group(3))
|
||||
else:
|
||||
ax.pr_target_steps = float(match.group(3))
|
||||
return ":"
|
||||
|
||||
# FUPR: begin motion, e.g. "BGA"
|
||||
# begin motion, e.g. "BGA": pending absolute target wins, else relative
|
||||
match = re.match(r"^BG([A-H])$", line)
|
||||
if match:
|
||||
axis = ord(match.group(1).lower()) - 97
|
||||
target = state.variables.get(f"_fupr_target_{axis}", 0.0)
|
||||
ax = state.axis(axis)
|
||||
ax.motor_on = True
|
||||
ax.start_move(target)
|
||||
if ax.pa_target_steps is not None:
|
||||
ax.start_move(ax.pa_target_steps)
|
||||
ax.pa_target_steps = None
|
||||
elif ax.pr_target_steps is not None:
|
||||
ax.start_move(ax.position() + ax.pr_target_steps)
|
||||
ax.pr_target_steps = None
|
||||
return ":"
|
||||
|
||||
# motor servo on/off per axis (OMNY shuttle/dewar handling)
|
||||
match = re.match(r"^SH([A-H])$", line)
|
||||
if match:
|
||||
state.axis(ord(match.group(1).lower()) - 97).motor_on = True
|
||||
return ":"
|
||||
match = re.match(r"^MO([A-H])?$", line)
|
||||
if match:
|
||||
if match.group(1):
|
||||
state.axis(ord(match.group(1).lower()) - 97).motor_on = False
|
||||
else:
|
||||
for ax in state.axes:
|
||||
ax.motor_on = False
|
||||
return ":"
|
||||
|
||||
# acceleration / deceleration / speed / limit-disable per axis
|
||||
match = re.match(r"^(AC|DC|SP|LD)([A-H])=(-?[0-9.]+)$", line)
|
||||
if match:
|
||||
if match.group(1) == "SP":
|
||||
state.axis(ord(match.group(2).lower()) - 97).speed_steps = float(match.group(3))
|
||||
return ":"
|
||||
|
||||
# FUPR: position readback in counts, e.g. "TPA"
|
||||
match = re.match(r"^T[DP]([A-H])$", line)
|
||||
match = re.match(r"^T([DP])([A-H])$", line)
|
||||
if match:
|
||||
axis = ord(match.group(1).lower()) - 97
|
||||
return f"{state.axis(axis).position():.4f}"
|
||||
ax = state.axis(ord(match.group(2).lower()) - 97)
|
||||
if match.group(1) == "P" and ax.encpermm is not None:
|
||||
return f"{ax.position() / ax.stppermm * ax.encpermm:.4f}"
|
||||
return f"{ax.position():.4f}"
|
||||
|
||||
if line == "AB" or line.startswith("AB"):
|
||||
# Abort: stops all motion and all program threads (variables keep their
|
||||
# values, unlike XQ#STOP which also clears mntprgs/mntmod)
|
||||
for ax in state.axes:
|
||||
ax.stop_move()
|
||||
state._transfer_abort.set()
|
||||
state._thread_deadlines.clear()
|
||||
state._tracker_running = False
|
||||
return ":"
|
||||
if line == "ST" or re.match(r"^ST[A-H]*$", line):
|
||||
for ax in state.axes:
|
||||
ax.stop_move()
|
||||
return ":"
|
||||
|
||||
match = _ASSIGNMENT_RE.match(line)
|
||||
if match:
|
||||
@@ -352,6 +432,10 @@ class SimGalilSocket(SimSocketBase):
|
||||
parsed = float(value)
|
||||
except ValueError:
|
||||
parsed = value
|
||||
if name == "axisref" and index is not None and isinstance(parsed, float):
|
||||
state.axis(int(index)).referenced = bool(parsed)
|
||||
state.recompute_allaxref()
|
||||
return ":"
|
||||
if index is not None:
|
||||
state.variables[f"{name}[{index}]"] = parsed
|
||||
else:
|
||||
@@ -395,7 +479,7 @@ class SimGalilSocket(SimSocketBase):
|
||||
if arg.startswith("@AN["):
|
||||
channel = int(arg[4:].split("]")[0])
|
||||
return f"{state.analog_inputs.get(channel, 0.0):.4f}"
|
||||
if arg == "allaxref" or arg == "axisref":
|
||||
if arg in ("allaxref", "axisref"):
|
||||
return f"{1.0 if state.all_axes_referenced() else 0.0:.4f}"
|
||||
if arg.startswith("axisref["):
|
||||
axis = int(arg.split("[")[1].split("]")[0])
|
||||
@@ -405,6 +489,10 @@ class SimGalilSocket(SimSocketBase):
|
||||
if arg.startswith("stppermm["):
|
||||
axis = int(arg.split("[")[1].split("]")[0])
|
||||
return f"{state.axis(axis).stppermm:.4f}"
|
||||
if arg.startswith("encpermm["):
|
||||
axis = int(arg.split("[")[1].split("]")[0])
|
||||
ax = state.axis(axis)
|
||||
return f"{ax.encpermm if ax.encpermm is not None else ax.stppermm:.4f}"
|
||||
with state.lock:
|
||||
if arg in state.variables:
|
||||
val = state.variables[arg]
|
||||
@@ -423,12 +511,10 @@ class SimGalilSocket(SimSocketBase):
|
||||
with state.lock:
|
||||
axis_index = int(state.variables.get("naxis", 0))
|
||||
direction = float(state.variables.get("ndir", 0.0))
|
||||
threading.Thread(
|
||||
target=state.reference_axis, args=(axis_index, direction), daemon=True
|
||||
).start()
|
||||
state.reference_axis(axis_index, direction)
|
||||
return ":"
|
||||
if routine == "#REFAX": # FUPR
|
||||
threading.Thread(target=state.reference_axis, args=(0, 0.0), daemon=True).start()
|
||||
state.reference_axis(0, 0.0)
|
||||
return ":"
|
||||
if routine == "#MOTON": # FUPR
|
||||
state.axis(0).motor_on = True
|
||||
@@ -461,6 +547,18 @@ class SimGalilSocket(SimSocketBase):
|
||||
state.set_thread_active(4, 1.0)
|
||||
state._set_gripper(False)
|
||||
return ":"
|
||||
if routine == "#GRSTAT":
|
||||
# gripper status measurement: report success
|
||||
state.set_thread_active(4, 0.5)
|
||||
with state.lock:
|
||||
state.variables["grstat"] = 1.0
|
||||
return ":"
|
||||
if routine == "#CRESET":
|
||||
with state.lock:
|
||||
state.variables["confirm"] = 0.0
|
||||
return ":"
|
||||
if routine == "#SAFETY":
|
||||
return ":"
|
||||
if routine == "#Tracker":
|
||||
with state.lock:
|
||||
if state.variables.get("tracken", 0.0) == 1.0 and state.all_axes_referenced():
|
||||
@@ -480,6 +578,9 @@ def _sim_state_kwargs(
|
||||
sim_limit_low_active,
|
||||
sim_limit_high_active,
|
||||
sign,
|
||||
sim_encpermm=None,
|
||||
sim_digital_outputs=None,
|
||||
sim_variables=None,
|
||||
) -> dict:
|
||||
initial_position = None
|
||||
if sim_initial_position is not None:
|
||||
@@ -493,6 +594,9 @@ def _sim_state_kwargs(
|
||||
"digital_inputs": sim_digital_inputs,
|
||||
"limit_low_active": sim_limit_low_active,
|
||||
"limit_high_active": sim_limit_high_active,
|
||||
"encpermm": sim_encpermm,
|
||||
"digital_outputs": sim_digital_outputs,
|
||||
"variables": sim_variables,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
Simulated LamNI endstation devices.
|
||||
|
||||
Galil (lamni.dmc, mpc2680:8081): reuses the shared SimGalilState with LamNI specifics —
|
||||
encoder-read axes 0-5 (`TP`/`encpermm`), `swver=28`, error flags `allaxrer`/`caperr`,
|
||||
air bearing lights/interlocks on outputs 1/9/13. Motion uses the same base
|
||||
`naxis/ntarget/movereq/#NEWPAR` mechanics as flOMNI.
|
||||
|
||||
RT (LAMNI_server.cpp, port 3333): its own protocol dialect —
|
||||
J0/J1/J5/J6 feedback control (no reply), J2 -> "status,ssi0,ssi1",
|
||||
J4 -> "status,pos_y,pos_x", J7 -> angle interferometer "status,pos,signal",
|
||||
As (start cap sampling, no reply) / Ar -> 8-field cap sensor data,
|
||||
Ss (start rt sampler, no reply) / Sr -> 7-field sampler sums,
|
||||
V<um_per_s> velocity limit (no reply), a<rad> (no reply) / ar -> angle,
|
||||
pa/pr, scan s/sc/sd/sr/sh, r<i> -> 21-field sample rows
|
||||
(x/y target-avg-stdev, five cap-sensor avg/stdev pairs, angle avg/stdev), o/t.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from csaxs_bec.devices.omny.galil.lgalil_ophyd import LamniGalilMotor
|
||||
from csaxs_bec.devices.omny.rt.rt_lamni_ophyd import RtLamniFlyer, RtLamniMotor
|
||||
from csaxs_bec.devices.sim.sim_galil import SimGalilSocket, SimGalilState, _sim_state_kwargs
|
||||
from csaxs_bec.devices.sim.sim_socket import SimSocketBase, SimStateRegistry
|
||||
|
||||
# single source of truth for the LamNI geometry (same constants the scan uses)
|
||||
from csaxs_bec.scans.LamNIFermatScan import MOVEMENT_SCALE_X, MOVEMENT_SCALE_Y
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
_POSITION_NOISE_UM = 0.002
|
||||
|
||||
|
||||
class SimLamniGalilMotor(LamniGalilMotor):
|
||||
"""LamniGalilMotor connected to the simulated Galil controller.
|
||||
|
||||
The signature lists all deviceConfig keys explicitly; the BEC device server only
|
||||
passes config keys present in inspect.signature(dev_cls).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
host="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
device_manager=None,
|
||||
sim_initial_position=None,
|
||||
sim_velocity=None,
|
||||
sim_stppermm=None,
|
||||
sim_encpermm=None,
|
||||
sim_referenced=True,
|
||||
sim_analog_inputs=None,
|
||||
sim_digital_inputs=None,
|
||||
sim_digital_outputs=None,
|
||||
sim_variables=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
axis_Id,
|
||||
prefix,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
limits=limits,
|
||||
sign=sign,
|
||||
socket_cls=SimGalilSocket,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
state = SimStateRegistry.get(SimGalilState, host, port)
|
||||
variables = {"swver": 28}
|
||||
variables.update(sim_variables or {})
|
||||
# air off and rt-not-blocked by default so the rt feedback interlock passes
|
||||
outputs = {"9": 1, "13": 1}
|
||||
outputs.update(sim_digital_outputs or {})
|
||||
state.configure_axis(
|
||||
self.axis_Id_numeric,
|
||||
**_sim_state_kwargs(
|
||||
sim_initial_position,
|
||||
sim_velocity,
|
||||
sim_stppermm,
|
||||
sim_referenced,
|
||||
sim_analog_inputs,
|
||||
sim_digital_inputs,
|
||||
None,
|
||||
None,
|
||||
self.sign,
|
||||
sim_encpermm=sim_encpermm,
|
||||
sim_digital_outputs=outputs,
|
||||
sim_variables=variables,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SimRtLamniState:
|
||||
"""Shared state of the simulated LAMNI communication server."""
|
||||
|
||||
def __init__(self, host=None, port=None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.lock = threading.RLock()
|
||||
self.targets = [0.0, 0.0, 0.0]
|
||||
self.feedback_running = False
|
||||
self.angle_rad = 0.0
|
||||
self.ssi = (12000.0, 12000.0)
|
||||
self.angle_interferometer_signal = 12000.0
|
||||
self.cap_sensors = [1.0, 1.1, 8.36, 0.29, -4.08, 0.5, 0.6]
|
||||
self.velocity_um_per_s = 0.0
|
||||
self.point_dwell_s = 0.02
|
||||
# coupling of the interferometer readback to the coarse stages (lsamx/lsamy):
|
||||
# with feedback off, the measured position is determined by the coarse stages
|
||||
self.coarse_coupling = None
|
||||
self.scan_positions = []
|
||||
self.scan_mode = 0
|
||||
self.scan_current = 0
|
||||
self._scan_thread = None
|
||||
self._scan_abort = threading.Event()
|
||||
|
||||
def configure(self, **kwargs):
|
||||
with self.lock:
|
||||
if kwargs.get("point_dwell_s") is not None:
|
||||
self.point_dwell_s = float(kwargs["point_dwell_s"])
|
||||
if kwargs.get("ssi_signal") is not None:
|
||||
self.ssi = (float(kwargs["ssi_signal"]), float(kwargs["ssi_signal"]))
|
||||
if kwargs.get("coarse_coupling") is not None:
|
||||
self.coarse_coupling = dict(kwargs["coarse_coupling"])
|
||||
|
||||
def measured_positions(self):
|
||||
"""Interferometer-measured x/y in um.
|
||||
|
||||
With the feedback loop running, the piezos hold the trajectory-generator
|
||||
targets. With the loop off, the measured position follows the coarse stages
|
||||
through the LamNI geometry:
|
||||
(x, y) = lamni_from_stage_coordinates(lsamx - center_x, lsamy - center_y) * 1000.
|
||||
This is what makes lamni_new_scan_center_interferometer converge.
|
||||
"""
|
||||
with self.lock:
|
||||
if self.feedback_running or self.coarse_coupling is None:
|
||||
return list(self.targets)
|
||||
coupling = self.coarse_coupling
|
||||
galil = SimStateRegistry.get(
|
||||
SimGalilState, coupling.get("galil_host", self.host), coupling.get("galil_port", 8081)
|
||||
)
|
||||
|
||||
def user_pos(axis_key, sign_key):
|
||||
ax = galil.axis(int(coupling[axis_key]))
|
||||
return ax.position() / ax.stppermm * float(coupling.get(sign_key, 1))
|
||||
|
||||
dx_stage = user_pos("x_axis", "x_sign") - float(coupling.get("x_center", 0.0))
|
||||
dy_stage = user_pos("y_axis", "y_sign") - float(coupling.get("y_center", 0.0))
|
||||
x_um = (dx_stage * 0.5 + dy_stage * MOVEMENT_SCALE_X) * 1000.0
|
||||
y_um = dy_stage * MOVEMENT_SCALE_Y * 1000.0
|
||||
return [x_um, y_um, self.targets[2]]
|
||||
|
||||
def clear_scan(self):
|
||||
self._scan_abort.set()
|
||||
if self._scan_thread is not None and self._scan_thread.is_alive():
|
||||
self._scan_thread.join(timeout=2)
|
||||
with self.lock:
|
||||
self.scan_positions = []
|
||||
self.scan_mode = 0
|
||||
self.scan_current = 0
|
||||
self.targets = [0.0, 0.0, self.targets[2]]
|
||||
|
||||
def add_scan_position(self, pos):
|
||||
with self.lock:
|
||||
self.scan_positions.append(pos)
|
||||
return len(self.scan_positions)
|
||||
|
||||
def start_scan(self) -> int:
|
||||
with self.lock:
|
||||
num_pos = len(self.scan_positions)
|
||||
if num_pos == 0 or self.scan_mode > 0:
|
||||
return num_pos
|
||||
self.scan_mode = 3
|
||||
self.scan_current = 0
|
||||
self._scan_abort.clear()
|
||||
self._scan_thread = threading.Thread(target=self._scan_loop, daemon=True)
|
||||
self._scan_thread.start()
|
||||
return num_pos
|
||||
|
||||
def _scan_loop(self):
|
||||
with self.lock:
|
||||
self.scan_mode = 2
|
||||
num_pos = len(self.scan_positions)
|
||||
for index in range(num_pos):
|
||||
if self._scan_abort.wait(self.point_dwell_s):
|
||||
return
|
||||
with self.lock:
|
||||
self.scan_current = index + 1
|
||||
self.targets[: len(self.scan_positions[index])] = self.scan_positions[index]
|
||||
with self.lock:
|
||||
self.scan_mode = 0
|
||||
|
||||
def sample_row(self, index: int) -> str:
|
||||
with self.lock:
|
||||
target = (
|
||||
self.scan_positions[index]
|
||||
if 0 <= index < len(self.scan_positions)
|
||||
else self.targets
|
||||
)
|
||||
angle = self.angle_rad
|
||||
caps = list(self.cap_sensors[2:7])
|
||||
noise = _POSITION_NOISE_UM
|
||||
parts = [f"{index}", "100", f"{angle:.6f}"]
|
||||
for target_val in (target[0], target[1]):
|
||||
parts += [
|
||||
f"{target_val:.5f}",
|
||||
f"{target_val + random.gauss(0.0, noise):.5f}",
|
||||
f"{abs(random.gauss(noise, noise / 4)):.5f}",
|
||||
]
|
||||
for cap in caps:
|
||||
parts += [f"{cap + random.gauss(0.0, 0.001):.5f}", f"{0.001:.5f}"]
|
||||
parts += [f"{angle + random.gauss(0.0, 1e-6):.5f}", f"{1e-6:.5f}"]
|
||||
return ", ".join(parts) + "\n"
|
||||
|
||||
|
||||
class SimRtLamniSocket(SimSocketBase):
|
||||
"""Simulated socket implementing the LAMNI communication server protocol."""
|
||||
|
||||
state_cls = SimRtLamniState
|
||||
|
||||
def handle_command(self, line: str): # noqa: C901
|
||||
state: SimRtLamniState = self.state
|
||||
cmd, args = line[0], line[1:]
|
||||
|
||||
if cmd == "J":
|
||||
if args.startswith("0") or args.startswith("6"):
|
||||
state.feedback_running = False
|
||||
return None
|
||||
if args.startswith("1") or args.startswith("5"):
|
||||
state.feedback_running = True
|
||||
return None
|
||||
if args.startswith("2"):
|
||||
status = 0 if state.feedback_running else 1
|
||||
return f"{status},{state.ssi[0]:.0f},{state.ssi[1]:.0f}\n"
|
||||
if args.startswith("3"): # start ssi averaging
|
||||
return None
|
||||
if args.startswith("4"):
|
||||
measured = state.measured_positions()
|
||||
return f"0,{measured[1]:f},{measured[0]:f}\n"
|
||||
if args.startswith("7"):
|
||||
return f"1,{state.angle_rad:f},{state.angle_interferometer_signal:f}\n"
|
||||
return None
|
||||
|
||||
if cmd == "A":
|
||||
if args.startswith("s"): # start cap sensor sampling
|
||||
return None
|
||||
caps = state.cap_sensors
|
||||
values = ",".join(f"{val + random.gauss(0.0, 0.001):.3f}" for val in caps)
|
||||
return f"100,{values}\n"
|
||||
|
||||
if cmd == "S":
|
||||
if args.startswith("s"): # reset and start rt sampler
|
||||
return None
|
||||
return "100,0.0,0.0,0.0,0.0,0.0,0.0\n"
|
||||
|
||||
if cmd == "V": # velocity limit
|
||||
with state.lock:
|
||||
state.velocity_um_per_s = float(args) if args else 0.0
|
||||
return None
|
||||
|
||||
if cmd == "a":
|
||||
if args.startswith("r"):
|
||||
return f"{state.angle_rad:f}\n"
|
||||
state.angle_rad = float(args)
|
||||
return None
|
||||
|
||||
if cmd == "p":
|
||||
if args.startswith("r"):
|
||||
measured = state.measured_positions()
|
||||
return f"{measured[0]:f},{measured[1]:f},{measured[2]:f}\n"
|
||||
if args.startswith("a"):
|
||||
axis_str, value_str = args[1:].split(",")
|
||||
with state.lock:
|
||||
state.targets[int(float(axis_str))] = float(value_str)
|
||||
return None
|
||||
values = [float(val) for val in args.split(",")]
|
||||
with state.lock:
|
||||
state.targets[: len(values)] = values
|
||||
return None
|
||||
|
||||
if cmd == "s":
|
||||
if args.startswith("c"):
|
||||
state.clear_scan()
|
||||
return None
|
||||
if args.startswith("r"):
|
||||
mode, num_pos, current = (
|
||||
state.scan_mode,
|
||||
len(state.scan_positions),
|
||||
state.scan_current,
|
||||
)
|
||||
# LAMNI_server: "%.0f,%.0f,%.0f" - integer-formatted, parsed with int()
|
||||
return f"{mode:.0f},{num_pos:.0f},{current:.0f}\n"
|
||||
if args.startswith("d"):
|
||||
num_pos = state.start_scan()
|
||||
return f"Scan mode started with #positions in scan {num_pos:.0f}. Timing: Detector Trigger.\n"
|
||||
if args.startswith("h"):
|
||||
state.clear_scan()
|
||||
return "0.0, 0.0, 0.0\n"
|
||||
count = state.add_scan_position([float(val) for val in args.split(",")])
|
||||
return f"{count:.0f}\n"
|
||||
|
||||
if cmd == "r":
|
||||
return state.sample_row(int(args))
|
||||
|
||||
if cmd == "o":
|
||||
return "1\n"
|
||||
if cmd == "t":
|
||||
return "1\n"
|
||||
if cmd == "d":
|
||||
return None
|
||||
|
||||
logger.warning(f"[sim rt lamni] {self.host}:{self.port} unhandled command '{line}'")
|
||||
return None
|
||||
|
||||
|
||||
def _rt_lamni_sim_config(sim_point_dwell_s, sim_ssi_signal, sim_coarse_coupling) -> dict:
|
||||
return {
|
||||
"point_dwell_s": sim_point_dwell_s,
|
||||
"ssi_signal": sim_ssi_signal,
|
||||
"coarse_coupling": sim_coarse_coupling,
|
||||
}
|
||||
|
||||
|
||||
class SimRtLamniMotor(RtLamniMotor):
|
||||
"""RtLamniMotor connected to the simulated LAMNI server."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
host="mpc2680.psi.ch",
|
||||
port=3333,
|
||||
limits=None,
|
||||
sign=1,
|
||||
device_manager=None,
|
||||
device_access=None,
|
||||
sim_point_dwell_s=None,
|
||||
sim_ssi_signal=None,
|
||||
sim_coarse_coupling=None,
|
||||
**kwargs,
|
||||
):
|
||||
# device_access is a BEC config flag (device_manager injection), not a
|
||||
# constructor parameter of RtLamniMotor; accepted here only so the BEC
|
||||
# kwarg filter passes it without error, then intentionally discarded
|
||||
del device_access
|
||||
super().__init__(
|
||||
axis_Id,
|
||||
prefix,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
limits=limits,
|
||||
sign=sign,
|
||||
socket_cls=SimRtLamniSocket,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
SimStateRegistry.get(SimRtLamniState, host, port).configure(
|
||||
**_rt_lamni_sim_config(sim_point_dwell_s, sim_ssi_signal, sim_coarse_coupling)
|
||||
)
|
||||
|
||||
|
||||
class SimRtLamniFlyer(RtLamniFlyer):
|
||||
"""RtLamniFlyer connected to the simulated LAMNI server."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
host="mpc2680.psi.ch",
|
||||
port=3333,
|
||||
device_manager=None,
|
||||
sim_point_dwell_s=None,
|
||||
sim_ssi_signal=None,
|
||||
sim_coarse_coupling=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
socket_cls=SimRtLamniSocket,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
SimStateRegistry.get(SimRtLamniState, host, port).configure(
|
||||
**_rt_lamni_sim_config(sim_point_dwell_s, sim_ssi_signal, sim_coarse_coupling)
|
||||
)
|
||||
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
Simulated OMNY endstation devices.
|
||||
|
||||
Galil (controller1.dmc and siblings): reuses the shared SimGalilState — same base move
|
||||
mechanics as flOMNI and the *identical* gripper transfer variable protocol
|
||||
(getaprch/mntaprch/mntprgs/mntmod/confirm/atconfir, with mntax=4 and signed mntprgs),
|
||||
plus OMNY status variables (swver=134, tempab/timeab, IgNoFol, grstat/relpos/toppin)
|
||||
and direct axis commands for the shuttle/dewar handling (AC/DC/SP/PR/BG/SH/MO/LD).
|
||||
Motor resolutions are hardcoded Python-side (51200; 89565.87 for the rotation axis on
|
||||
port 8083) and seeded accordingly.
|
||||
|
||||
RT (OMNY_server.cpp, port 3333): the flOMNI protocol's ancestor. SimRtOmnySocket is a
|
||||
dialect of the flOMNI RT simulation: identical pa/pr/l/T/Ts/y/k/scan handling, plus
|
||||
J0/J1/J3 (no reply) and J2 -> "status,ssi0..ssi4", and 19-field sample rows
|
||||
(ST-OSA and OSA-FZP interferometer combinations, rotz, ST-FZP).
|
||||
|
||||
EPICS periphery (sample storage incl. shuttle/dewar bookkeeping, dewar, temperatures)
|
||||
uses the patched-device pattern with in-memory mock PVs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd_devices.tests.utils import patched_device
|
||||
|
||||
from csaxs_bec.devices.omny.galil.ogalil_ophyd import OMNYGalilMotor
|
||||
from csaxs_bec.devices.omny.omny_dewar import OMNYDewar
|
||||
from csaxs_bec.devices.omny.omny_sample_storage import OMNYSampleStorage
|
||||
from csaxs_bec.devices.omny.omny_temperatures import OMNYTemperatures
|
||||
from csaxs_bec.devices.omny.rt.rt_omny_ophyd import RtOMNYFlyer, RtOMNYMotor
|
||||
from csaxs_bec.devices.sim.sim_galil import SimGalilSocket, SimGalilState, _sim_state_kwargs
|
||||
from csaxs_bec.devices.sim.sim_rt_flomni import SimRtFlomniSocket, SimRtFlomniState
|
||||
from csaxs_bec.devices.sim.sim_socket import SimStateRegistry
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
_POSITION_NOISE_UM = 0.002
|
||||
|
||||
_OMNY_ROTATION_PORT = 8083
|
||||
_OMNY_ROTATION_AXIS = 2
|
||||
_OMNY_RESOLUTION = 51200.0
|
||||
_OMNY_ROTATION_RESOLUTION = 89565.8666667
|
||||
|
||||
|
||||
class SimOMNYGalilMotor(OMNYGalilMotor):
|
||||
"""OMNYGalilMotor connected to the simulated Galil controller.
|
||||
|
||||
The signature lists all deviceConfig keys explicitly; the BEC device server only
|
||||
passes config keys present in inspect.signature(dev_cls).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
host="mpc2412.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
device_manager=None,
|
||||
sim_initial_position=None,
|
||||
sim_velocity=None,
|
||||
sim_referenced=True,
|
||||
sim_analog_inputs=None,
|
||||
sim_digital_inputs=None,
|
||||
sim_digital_outputs=None,
|
||||
sim_limit_low_active=None,
|
||||
sim_limit_high_active=None,
|
||||
sim_variables=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
axis_Id,
|
||||
prefix,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
limits=limits,
|
||||
sign=sign,
|
||||
socket_cls=SimGalilSocket,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
# the OMNY controller additionally manages axis power via a NETIO switch
|
||||
# webserver (http HTTP, outside the socket seam); simulate it as always-on
|
||||
self.controller._ogalil_switchsocket_status = lambda: [1, 1, 1, 1]
|
||||
self.controller._ogalil_switchsocket_switch_all_on = lambda: None
|
||||
self.controller._ogalil_switchsocket_switch_all_off = lambda: None
|
||||
state = SimStateRegistry.get(SimGalilState, host, port)
|
||||
# resolutions are hardcoded in ogalil_ophyd.GalilMotorResolution
|
||||
stppermm = _OMNY_RESOLUTION
|
||||
if int(port) == _OMNY_ROTATION_PORT and self.axis_Id_numeric == _OMNY_ROTATION_AXIS:
|
||||
stppermm = _OMNY_ROTATION_RESOLUTION
|
||||
variables = {"swver": 134, "mntax": 4}
|
||||
variables.update(sim_variables or {})
|
||||
state.configure_axis(
|
||||
self.axis_Id_numeric,
|
||||
**_sim_state_kwargs(
|
||||
sim_initial_position,
|
||||
sim_velocity,
|
||||
stppermm,
|
||||
sim_referenced,
|
||||
sim_analog_inputs,
|
||||
sim_digital_inputs,
|
||||
sim_limit_low_active,
|
||||
sim_limit_high_active,
|
||||
self.sign,
|
||||
sim_digital_outputs=sim_digital_outputs,
|
||||
sim_variables=variables,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SimRtOmnyState(SimRtFlomniState):
|
||||
"""OMNY RT server state: flOMNI state plus 5 interferometer signals and 19-field rows."""
|
||||
|
||||
def __init__(self, host=None, port=None):
|
||||
super().__init__(host=host, port=port)
|
||||
self.ssi_signals = [12000.0] * 5
|
||||
|
||||
def sample_row(self, index: int) -> str:
|
||||
with self.lock:
|
||||
target = (
|
||||
self.scan_positions[index]
|
||||
if 0 <= index < len(self.scan_positions)
|
||||
else self.targets
|
||||
)
|
||||
angle = self.angle_rad
|
||||
noise = _POSITION_NOISE_UM
|
||||
fields = [f"{index}", "100", f"{angle:.6f}"]
|
||||
# [3..8] target/avg/stdev of x and y (sample stage <-> OSA)
|
||||
for target_val in (target[0], target[1]):
|
||||
fields += [
|
||||
f"{target_val:.5f}",
|
||||
f"{target_val + random.gauss(0.0, noise):.5f}",
|
||||
f"{abs(random.gauss(noise, noise / 4)):.5f}",
|
||||
]
|
||||
# [9..12] OSA <-> FZP x/y avg/stdev
|
||||
fields += [f"{random.gauss(0.0, noise):.5f}", f"{noise:.5f}"] * 2
|
||||
# [13..14] rotz avg/stdev
|
||||
fields += [f"{random.gauss(0.0, 0.1):.5f}", f"{0.02:.5f}"]
|
||||
# [15..18] sample stage <-> FZP x/y avg/stdev
|
||||
for target_val in (target[0], target[1]):
|
||||
fields += [f"{target_val + random.gauss(0.0, noise):.5f}", f"{noise:.5f}"]
|
||||
return ", ".join(fields) + "\n"
|
||||
|
||||
|
||||
class SimRtOmnySocket(SimRtFlomniSocket):
|
||||
"""Simulated socket implementing the OMNY communication server protocol."""
|
||||
|
||||
state_cls = SimRtOmnyState
|
||||
|
||||
def handle_command(self, line: str):
|
||||
state: SimRtOmnyState = self.state
|
||||
cmd, args = line[0], line[1:]
|
||||
if cmd == "y":
|
||||
# OMNY: single combined slew-rate-limiter flag (flOMNI returns the sum, 3)
|
||||
return "1.000000\n"
|
||||
if cmd == "J":
|
||||
if args.startswith("2"):
|
||||
signals = ",".join(f"{val:.0f}" for val in state.ssi_signals)
|
||||
status = 0 if state.feedback_running else 1
|
||||
return f"{status},{signals}\n"
|
||||
# J0 disable / J1 enable with reset / J3 averaging: no reply
|
||||
if args.startswith("0"):
|
||||
state.feedback_running = False
|
||||
elif args.startswith("1"):
|
||||
state.feedback_running = True
|
||||
return None
|
||||
return super().handle_command(line)
|
||||
|
||||
|
||||
class SimRtOMNYMotor(RtOMNYMotor):
|
||||
"""RtOMNYMotor connected to the simulated OMNY server."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
host="mpc2412.psi.ch",
|
||||
port=3333,
|
||||
limits=None,
|
||||
sign=1,
|
||||
device_manager=None,
|
||||
sim_point_dwell_s=None,
|
||||
sim_pid_x_voltage=None,
|
||||
sim_ssi_signal=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
axis_Id,
|
||||
prefix,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
limits=limits,
|
||||
sign=sign,
|
||||
socket_cls=SimRtOmnySocket,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
SimStateRegistry.get(SimRtOmnyState, host, port).configure(
|
||||
point_dwell_s=sim_point_dwell_s,
|
||||
pid_x_voltage=sim_pid_x_voltage,
|
||||
ssi_signal=sim_ssi_signal,
|
||||
)
|
||||
|
||||
|
||||
class SimRtOMNYFlyer(RtOMNYFlyer):
|
||||
"""RtOMNYFlyer connected to the simulated OMNY server."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
host="mpc2412.psi.ch",
|
||||
port=3333,
|
||||
device_manager=None,
|
||||
sim_point_dwell_s=None,
|
||||
sim_pid_x_voltage=None,
|
||||
sim_ssi_signal=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
socket_cls=SimRtOmnySocket,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
SimStateRegistry.get(SimRtOmnyState, host, port).configure(
|
||||
point_dwell_s=sim_point_dwell_s,
|
||||
pid_x_voltage=sim_pid_x_voltage,
|
||||
ssi_signal=sim_ssi_signal,
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_OMNY_SAMPLES = {"1": "sim_sample_1", "2": "sim_sample_2", "3": "sim_sample_3"}
|
||||
|
||||
|
||||
class SimOMNYSampleStorage(OMNYSampleStorage):
|
||||
"""OMNYSampleStorage with all EPICS PVs replaced by in-memory mock PVs."""
|
||||
|
||||
def __new__(cls, *args, sim_samples: dict | None = None, **kwargs):
|
||||
with patched_device(OMNYSampleStorage, *args, **kwargs) as storage:
|
||||
samples = _DEFAULT_OMNY_SAMPLES if sim_samples is None else sim_samples
|
||||
storage.unset_sample_in_gripper()
|
||||
storage.set_sample_in_samplestage("sim_stage_sample")
|
||||
for slot_nr, name in samples.items():
|
||||
storage.set_sample_slot("A", int(slot_nr), str(name))
|
||||
return storage
|
||||
|
||||
def __init__(self, *args, sim_samples: dict | None = None, **kwargs):
|
||||
# instantiation handled in __new__ via patched_device; signature declares
|
||||
# the deviceConfig keys for the BEC device server kwarg filter
|
||||
pass
|
||||
|
||||
|
||||
class SimOMNYDewar(OMNYDewar):
|
||||
"""OMNYDewar with all EPICS PVs replaced by in-memory mock PVs."""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
with patched_device(OMNYDewar, *args, **kwargs) as dewar:
|
||||
return dewar
|
||||
|
||||
|
||||
class SimOMNYTemperatures(OMNYTemperatures):
|
||||
"""OMNYTemperatures with all EPICS PVs replaced by in-memory mock PVs."""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
with patched_device(OMNYTemperatures, *args, **kwargs) as temperatures:
|
||||
return temperatures
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Simulated flOMNI sample storage.
|
||||
|
||||
`FlomniSampleStorage` is purely EPICS-backed (XOMNY-SAMPLE_DB_flomni* PVs). The simulated
|
||||
version follows the patched-device pattern of `simulated_beamline_devices`: the real class
|
||||
is instantiated with all PVs replaced by in-memory mock PVs, so `set_sample_slot`,
|
||||
`show_all`, the gripper bookkeeping and the sample-transfer scripts run unchanged; the
|
||||
database content simply lives in process memory instead of EPICS.
|
||||
|
||||
By default the storage boots with samples in slots 1-3 and a sample on the flOMNI stage
|
||||
(slot 0), so `ftransfer_flomni_to_gripper` / `ftransfer_get_sample` can be exercised
|
||||
immediately. Override via deviceConfig:
|
||||
|
||||
sim_samples:
|
||||
"0": "sim_stage_sample"
|
||||
"1": "sim_sample_1"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ophyd_devices.tests.utils import patched_device
|
||||
|
||||
from csaxs_bec.devices.omny.flomni_sample_storage import FlomniSampleStorage
|
||||
|
||||
_DEFAULT_SAMPLES = {
|
||||
"0": "sim_stage_sample",
|
||||
"1": "sim_sample_1",
|
||||
"2": "sim_sample_2",
|
||||
"3": "sim_sample_3",
|
||||
}
|
||||
|
||||
|
||||
class SimFlomniSampleStorage(FlomniSampleStorage):
|
||||
"""FlomniSampleStorage with all EPICS PVs replaced by in-memory mock PVs."""
|
||||
|
||||
def __new__(cls, *args, sim_samples: dict | None = None, **kwargs):
|
||||
with patched_device(FlomniSampleStorage, *args, **kwargs) as storage:
|
||||
samples = _DEFAULT_SAMPLES if sim_samples is None else sim_samples
|
||||
for slot_nr in range(21):
|
||||
storage.unset_sample_slot(slot_nr)
|
||||
storage.unset_sample_in_gripper()
|
||||
for slot_nr, name in samples.items():
|
||||
storage.set_sample_slot(int(slot_nr), str(name))
|
||||
return storage
|
||||
|
||||
def __init__(self, *args, sim_samples: dict | None = None, **kwargs):
|
||||
# instantiation is fully handled in __new__ via patched_device; the signature
|
||||
# only declares the deviceConfig keys for the BEC device server kwarg filter
|
||||
pass
|
||||
@@ -0,0 +1,153 @@
|
||||
# Simulated Endstations for cSAXS BEC (flOMNI / LamNI / OMNY)
|
||||
|
||||
Status: implemented and validated (2026-07). flOMNI extensively live-tested against a real
|
||||
BEC deployment; LamNI/OMNY validated by offline harness, live smoke test pending.
|
||||
|
||||
## Purpose
|
||||
|
||||
Full software development and testing of the flOMNI/LamNI/OMNY control code without
|
||||
hardware and without a running instrument. The real ophyd device classes, controllers,
|
||||
scan plugins, GUI widgets, and IPython-client scripts run **unchanged**; only the wire
|
||||
protocols are simulated. Swapping a device config is the only difference between the
|
||||
simulated and the real system.
|
||||
|
||||
## Architecture: protocol-level socket injection
|
||||
|
||||
All three endstations funnel every hardware interaction through socket-based controllers
|
||||
(`ophyd_devices.utils.controller.Controller`), which accept a `socket_cls` constructor
|
||||
parameter. The simulation injects `SocketIO`-compatible classes that implement the wire
|
||||
protocols as in-process state machines:
|
||||
|
||||
- Commands are dispatched synchronously inside `put()`; replies go to a queue drained by
|
||||
`receive()`. Commands that are reply-less on the real hardware must not enqueue replies
|
||||
(the main desync risk of naive mocks) — reply behavior follows the real server sources.
|
||||
- Simulation state is shared per (host, port) via `SimStateRegistry`, mirroring the
|
||||
controller singleton. Device subclasses seed axis parameters at construction.
|
||||
- The sim device classes are thin subclasses of the real ones whose only jobs are
|
||||
(a) defaulting `socket_cls` and (b) seeding sim state from `sim_*` deviceConfig keys.
|
||||
- Cross-device choreography (e.g. flOMNI `feedback_enable_with_reset` touching fsamroy,
|
||||
fsamx and the ftrackz Galil) runs through the real Python code against each
|
||||
controller's own sim socket — no logic is duplicated.
|
||||
|
||||
Protocol fidelity is grounded in the actual hardware-side sources: `fgalil.dmc`,
|
||||
`galil_micos_upr.dmc` (FUPR), `lamni.dmc`, `controller1.dmc` (OMNY), flOMNI
|
||||
`CommunicationServer.cpp`, `LAMNI_server.cpp`, `OMNY_server.cpp`.
|
||||
|
||||
## File inventory (all under csaxs_bec unless noted)
|
||||
|
||||
- `devices/sim/sim_socket.py` — `SimSocketBase` + `SimStateRegistry`
|
||||
- `devices/sim/sim_galil.py` — shared Galil DMC state machine (motion at constant
|
||||
velocity, threads, referencing `#FES/#FRM/#REFAX`, gripper transfer routines
|
||||
`#GRGET/#GRPUT` with `confirm` handshake and `atconfir` autoconfirm, `#Tracker`,
|
||||
`#MNTMODE/#POSMODE`, `#STOP`, `AB`/`ST`, direct axis commands `PA/PR/BG/SH/MO/AC/DC/SP/LD`,
|
||||
encoder axes `TP`/`encpermm`, dialect variables via `sim_variables`) plus
|
||||
`SimFlomniGalilMotor`, `SimFuprGalilMotor`
|
||||
- `devices/sim/sim_smaract.py` — Smaract MCS protocol + `SimSmaractMotor` (serves
|
||||
flOMNI fosa*, LamNI losa*, OMNY ocs*/oshield)
|
||||
- `devices/sim/sim_rt_flomni.py` — flOMNI Orchestra server (feedback l0/l1/l2/l3,
|
||||
tracker T/Ts, scans s/sc/sd/sr, 10-field sample rows, paced by `sim_point_dwell_s`)
|
||||
- `devices/sim/sim_lamni.py` — LamNI Galil dialect (swver 28, encoder axes 0–5,
|
||||
air-bearing interlocks) + LAMNI server (J0/J1/J5/J6, J2/J4/J7, As/Ar cap sensors,
|
||||
Ss/Sr sampler, V velocity, 21-field rows)
|
||||
- `devices/sim/sim_omny.py` — OMNY Galil dialect (swver 134, mntax=4, signed mntprgs,
|
||||
tempab/timeab/IgNoFol, grstat/relpos/toppin, `#GRSTAT/#CRESET/#SAFETY`, NETIO power
|
||||
switch stubbed always-on) + OMNY server dialect (J2 six fields, `y`→1 semantics,
|
||||
19-field rows) + patched-EPICS `SimOMNYSampleStorage/Dewar/Temperatures`
|
||||
- `devices/sim/sim_cameras.py` — `SimWebcamViewer` / `SimIDSCamera`: real camera classes
|
||||
with synthetic frame source (crosshair + gaussian blob); optional per-frame noise via
|
||||
`sim_noise_std` (default 0 = static, cheap over remote desktop)
|
||||
- `devices/sim/sim_sample_storage.py` — `SimFlomniSampleStorage` (patched EPICS,
|
||||
seedable via `sim_samples`, default slots 0–3 populated)
|
||||
- `device_configs/simulated_omny/` — **all** simulated configs live here:
|
||||
`simulated_flomni.yaml` (hand-maintained), `simulated_lamni.yaml` and
|
||||
`simulated_omny.yaml` (generated), `simulated_bl_endstation.yaml` (pre-existing)
|
||||
- `bin/generate_simulated_configs.py` — regenerates the LamNI/OMNY configs from
|
||||
`ptycho_lamni.yaml`/`ptycho_omny.yaml` (rerun after real-config changes)
|
||||
- `tests/sim_flomni_harness.py`, `tests/sim_lamni_omny_harness.py` — offline harnesses
|
||||
(no BEC deployment needed): 39 + 26 checks covering moves, referencing, feedback,
|
||||
tracker, scans through the flyers, gripper transfers, storage, cameras
|
||||
|
||||
## Real-code fixes made along the way (separate commits)
|
||||
|
||||
- `fgalil_ophyd.py` and `galil_ophyd.py` (base class, affects LamNI/OMNY): removed the
|
||||
trailing thread-0 wait in the setpoint `_socket_set` — it blocked `user_setpoint.put()` (holding the
|
||||
controller lock) for the whole move, so no intermediate readback/progress was possible.
|
||||
Applies to the real instrument too.
|
||||
- `ddg_1.py`: `bec_server.scan_server...ScanInfo` import moved under `TYPE_CHECKING`
|
||||
(typing-only runtime dependency broke against the redeployed BEC layout).
|
||||
- `flomni.py`: all four scilog sends wrapped in try/except RuntimeError — accounts
|
||||
without scilog no longer crash `tomo_scan`/alignment/timing reports.
|
||||
|
||||
## Delivery policy
|
||||
|
||||
Tarballs contain ONLY simulation-package files (`devices/sim/*` except
|
||||
`simulated_beamline_devices.py`, `device_configs/simulated_omny/*`, generator, harnesses,
|
||||
this doc) plus the two agreed device-layer fixes (`galil_ophyd.py`, `fgalil_ophyd.py`,
|
||||
`ddg_1.py`). Fixes to user-maintained files (`flomni.py`, ...) are delivered as diffs
|
||||
only — never as full files, since the working copies evolve during testing.
|
||||
|
||||
## BEC integration lessons (important for future sim devices)
|
||||
|
||||
1. **Explicit signatures required**: the device server passes only deviceConfig keys that
|
||||
appear as named parameters in `inspect.signature(dev_cls)`, and injects
|
||||
`device_manager` only if named. `*args/**kwargs`-only subclasses silently lose their
|
||||
entire config.
|
||||
2. **msgpack strict_map_key**: no integer keys anywhere in deviceConfig — quote channel
|
||||
numbers (`"1": -1.6`).
|
||||
3. Hidden second transports exist outside the socket seam (OMNY NETIO power-switch HTTP,
|
||||
camera MJPEG/pyueye) and need their own stubs.
|
||||
|
||||
## Deliberate simulation semantics ("working hardware", no error injection)
|
||||
|
||||
- Axes boot **referenced** (real init zeroes at limits; set `sim_referenced: false` to
|
||||
exercise init procedures) at positions from `in` user params, clamped into limits.
|
||||
- `allaxref`/`axisref[n]` are genuine writable DMC variables (forcing `allaxref=0` makes
|
||||
moves refuse, as on hardware); recomputed by referencing routines.
|
||||
- Constant-velocity motion with per-axis speeds from the DMC `#INIT` tables; RT scans
|
||||
advance at `sim_point_dwell_s` per position (default 20 ms) so ETA/timing logic is
|
||||
meaningfully testable.
|
||||
- Sampled scan rows carry targets + ~2 nm gaussian noise.
|
||||
- LamNI: the interferometer readback is coupled to the coarse stages — with feedback off,
|
||||
measured (x, y) = lamni_from_stage_coordinates(lsamx-center, lsamy-center)*1000 um
|
||||
(geometry constants imported from LamNIFermatScan). This makes
|
||||
lamni_new_scan_center_interferometer converge. Coupling is configured via
|
||||
`sim_coarse_coupling` on rtx/rty/rt_positions (generator fills it from the lsamx/lsamy
|
||||
config). lsamx/lsamy boot at their 'center' user parameters; speeds follow the
|
||||
lamni.dmc axspeed table.
|
||||
- A LamNI fermat scan re-enables feedback itself (`feedback_enable_without_reset` in the
|
||||
scan plugin) — starting a scan with user-disabled feedback therefore works, on the
|
||||
real system as well as in the sim; it only fails on real hardware when feedback cannot
|
||||
engage (air on, interferometer error), an error mode deliberately not simulated.
|
||||
- Referencing (`#FES/#FRM/#REFAX`) applies its outcome synchronously (limit switch,
|
||||
position 0) while only the apparent motion is time-based, so status polls never race
|
||||
the result.
|
||||
- Transfer confirm handshakes are real (GUI + gripper-camera flow works); set
|
||||
`atconfir=1` on the controller for unattended runs.
|
||||
- `#GRSTAT` always reports success; NETIO always on; unknown Galil commands reply `:`
|
||||
and log `[sim galil] unhandled` warnings — grep the device-server log after a session
|
||||
to find fidelity gaps worth adding (full DMC command-reference emulation was
|
||||
deliberately not pursued).
|
||||
- LamNI `sd` reply uses the detector-trigger wording; `sd<cycles>` internal-timing
|
||||
variant not modeled. `sr` replies are integer-formatted per LAMNI_server.
|
||||
|
||||
## Live-test status (flOMNI, testing account, redeployed BEC)
|
||||
|
||||
Passed: sample storage + GUI, moves incl. live progress bars and Ctrl-C, rotation→RT
|
||||
angle coupling, feedback enable/disable/status, tracker, fermat scans incl. progress and
|
||||
abort, X-ray eye GUI with camera frames, storage bookkeeping. Pending: full transfer
|
||||
chain end-to-end, tomo_scan (unblocked by the scilog fix), tomo queue executor
|
||||
(crash-resume checklist), then queue-reordering implementation. LamNI/OMNY: harness-
|
||||
validated; a short live smoke test (config load, a move, one scan, one OMNY transfer)
|
||||
recommended before relying on them, since flOMNI testing surfaced integration issues the
|
||||
harness cannot see.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- Deploy: extract the tarball in the repo root (overwrites sim files + the three fixed
|
||||
real files). Remove stale `device_configs/simulated_flomni/` and `simulated_lamni/`
|
||||
folders from earlier iterations if present.
|
||||
- Sim tuning knobs (deviceConfig): `sim_initial_position`, `sim_velocity`,
|
||||
`sim_stppermm`, `sim_encpermm`, `sim_referenced`, `sim_analog_inputs`,
|
||||
`sim_digital_inputs/outputs`, `sim_variables`, `sim_limit_low/high_active`,
|
||||
`sim_point_dwell_s`, `sim_noise_std`, `sim_samples`.
|
||||
- Harnesses run standalone: `python tests/sim_flomni_harness.py` (repo on PYTHONPATH).
|
||||
@@ -268,6 +268,17 @@ def main(): # noqa: C901
|
||||
duration = time.time() - t0
|
||||
check(np.isclose(fsamy.readback.get(), 3.05, atol=1e-3), "fsamy moved to 3.05")
|
||||
check(duration > 0.3, f"fsamy move at 0.1 mm/s took {duration:.2f} s (constant velocity)")
|
||||
move_status = fsamy.move(3.0, wait=False)
|
||||
time.sleep(0.25)
|
||||
mid_position = fsamy.readback.get()
|
||||
while not move_status.done:
|
||||
time.sleep(0.05)
|
||||
check(
|
||||
3.0 < mid_position < 3.05, f"intermediate readback during fsamy move ({mid_position:.4f})"
|
||||
)
|
||||
check(np.isclose(fsamy.readback.get(), 3.0, atol=1e-3), "fsamy arrived at 3.0")
|
||||
fsamy.move(3.05, wait=True)
|
||||
|
||||
sensor = float(ftransy.controller.socket_put_and_receive("MG@AN[1]").strip())
|
||||
check(np.isclose(sensor, -1.6), "ftransy gripper sensor voltage reads -1.6")
|
||||
|
||||
@@ -289,6 +300,29 @@ def main(): # noqa: C901
|
||||
curtain = foptz.controller.fosaz_light_curtain_is_triggered()
|
||||
check(curtain, "fosaz light curtain reads triggered")
|
||||
|
||||
# --- 3b. allaxref is a real, writable DMC variable ------------------------------
|
||||
con1 = fsamx.controller
|
||||
con1.socket_put_confirmed("allaxref=0")
|
||||
check(
|
||||
con1.socket_put_and_receive("MGallaxref").strip() == "0.0000",
|
||||
"allaxref=0 sticks (MG allaxref reads 0)",
|
||||
)
|
||||
move_refused = False
|
||||
try:
|
||||
fsamx.move(-1.2, wait=True)
|
||||
except Exception:
|
||||
move_refused = True
|
||||
check(move_refused, "move refused while allaxref=0")
|
||||
con1.socket_put_confirmed("allaxref=1")
|
||||
check(con1.all_axes_referenced(), "allaxref=1 restores referenced state")
|
||||
fsamx.move(-1.14, wait=True)
|
||||
|
||||
con1.socket_put_and_receive("AB")
|
||||
check(
|
||||
not con1.is_thread_active(5) and not fsamx.controller.is_axis_moving("E", 4),
|
||||
"AB aborts motion and program threads",
|
||||
)
|
||||
|
||||
# --- 4. smaract ---------------------------------------------------------------
|
||||
fosax.move(10.5, wait=True)
|
||||
check(np.isclose(fosax.readback.get(), 10.5, atol=1e-4), "fosax moved to 10.5")
|
||||
@@ -362,11 +396,36 @@ def main(): # noqa: C901
|
||||
|
||||
# --- 8. cameras -----------------------------------------------------------------
|
||||
webcam = SimWebcamViewer(url="http://sim/video", name="cam_flomni_gripper")
|
||||
ids_cam = SimIDSCamera(name="cam_xeye", camera_id=11, force_monochrome=True)
|
||||
ids_cam = SimIDSCamera(name="cam_xeye", camera_id=11, force_monochrome=True, sim_noise_std=4.0)
|
||||
frame = ids_cam.cam.get_image_data()
|
||||
check(frame.ndim == 2 and frame.max() > 200, f"IDS sim frame {frame.shape}, max {frame.max()}")
|
||||
check(webcam._sim_frame.shape == (480, 640, 3), "webcam sim frame 480x640 RGB")
|
||||
|
||||
# --- 9. sample storage -----------------------------------------------------------
|
||||
from csaxs_bec.devices.sim.sim_sample_storage import SimFlomniSampleStorage
|
||||
|
||||
samples = SimFlomniSampleStorage(name="flomni_samples")
|
||||
check(
|
||||
samples.is_sample_slot_used(0) and samples.is_sample_slot_used(3),
|
||||
"sample storage boots with samples in slots 0-3",
|
||||
)
|
||||
check(not samples.is_sample_in_gripper(), "gripper boots empty")
|
||||
samples.set_sample_in_gripper(samples.get_sample_name(1))
|
||||
samples.unset_sample_slot(1)
|
||||
check(
|
||||
samples.is_sample_in_gripper() and not samples.is_sample_slot_used(1),
|
||||
"slot 1 -> gripper bookkeeping",
|
||||
)
|
||||
|
||||
frame_a = ids_cam.cam.get_image_data()
|
||||
frame_b = ids_cam.cam.get_image_data()
|
||||
check(not np.array_equal(frame_a, frame_b), "IDS frames are live (noise explicitly enabled)")
|
||||
static_cam = SimIDSCamera(name="cam_xeye_static", camera_id=12, force_monochrome=True)
|
||||
check(
|
||||
np.array_equal(static_cam.cam.get_image_data(), static_cam.cam.get_image_data()),
|
||||
"IDS frames are static by default (noise disabled)",
|
||||
)
|
||||
|
||||
# --- feedback disable puts stages back to writable -------------------------------
|
||||
rtx.controller.feedback_disable()
|
||||
check(not dm.devices["fsamx"].read_only, "fsamx writable again after feedback_disable")
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
Offline harness for the simulated LamNI and OMNY hardware stacks.
|
||||
|
||||
Exercises the real device classes against the protocol simulations without a BEC
|
||||
deployment, covering the paths used by the LamNI/OMNY IPython-client scripts.
|
||||
|
||||
Run with the repo root on PYTHONPATH: python tests/sim_lamni_omny_harness.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from sim_flomni_harness import FakeDeviceManager, check # noqa: E402
|
||||
|
||||
|
||||
def run_lamni(): # noqa: C901
|
||||
from csaxs_bec.devices.sim.sim_lamni import SimLamniGalilMotor, SimRtLamniFlyer, SimRtLamniMotor
|
||||
|
||||
dm = FakeDeviceManager()
|
||||
host = "mpc2680.psi.ch"
|
||||
lsamx = dm.add(
|
||||
"lsamx",
|
||||
SimLamniGalilMotor(
|
||||
"A",
|
||||
name="lsamx",
|
||||
host=host,
|
||||
port=8081,
|
||||
sign=1,
|
||||
device_manager=dm,
|
||||
sim_stppermm=12800,
|
||||
sim_encpermm=2000,
|
||||
sim_initial_position=87.5,
|
||||
),
|
||||
user_parameter={"center": 87.5},
|
||||
)
|
||||
lsamy = dm.add(
|
||||
"lsamy",
|
||||
SimLamniGalilMotor(
|
||||
"B",
|
||||
name="lsamy",
|
||||
host=host,
|
||||
port=8081,
|
||||
sign=1,
|
||||
device_manager=dm,
|
||||
sim_stppermm=12800,
|
||||
sim_encpermm=2000,
|
||||
sim_initial_position=9.8,
|
||||
),
|
||||
user_parameter={"center": 9.8},
|
||||
)
|
||||
lsamrot = dm.add(
|
||||
"lsamrot",
|
||||
SimLamniGalilMotor(
|
||||
"C",
|
||||
name="lsamrot",
|
||||
host=host,
|
||||
port=8081,
|
||||
sign=1,
|
||||
device_manager=dm,
|
||||
sim_stppermm=50154.32099,
|
||||
sim_encpermm=36000,
|
||||
sim_velocity=20,
|
||||
sim_initial_position=300,
|
||||
),
|
||||
)
|
||||
coupling = {
|
||||
"galil_port": 8081,
|
||||
"x_axis": 0,
|
||||
"x_sign": 1,
|
||||
"x_center": 87.5,
|
||||
"y_axis": 1,
|
||||
"y_sign": 1,
|
||||
"y_center": 9.8,
|
||||
}
|
||||
rtx = dm.add(
|
||||
"rtx",
|
||||
SimRtLamniMotor(
|
||||
"A",
|
||||
name="rtx",
|
||||
host=host,
|
||||
port=3333,
|
||||
sign=1,
|
||||
device_manager=dm,
|
||||
limits=[-200, 200],
|
||||
sim_coarse_coupling=coupling,
|
||||
),
|
||||
)
|
||||
rty = dm.add(
|
||||
"rty",
|
||||
SimRtLamniMotor(
|
||||
"B", name="rty", host=host, port=3333, sign=1, device_manager=dm, limits=[-200, 200]
|
||||
),
|
||||
)
|
||||
flyer = dm.add(
|
||||
"rt_positions",
|
||||
SimRtLamniFlyer(
|
||||
name="rt_positions", host=host, port=3333, device_manager=dm, sim_point_dwell_s=0.005
|
||||
),
|
||||
)
|
||||
for device in (lsamx, lsamy, lsamrot, rtx, rty):
|
||||
device.wait_for_connection()
|
||||
flyer.controller.on()
|
||||
print("--- LamNI simulated devices connected ---")
|
||||
|
||||
# encoder-read axes: TP scaled by encpermm
|
||||
check(np.isclose(lsamx.readback.get(), 87.5, atol=1e-3), "lamni: lsamx boots at center 87.5")
|
||||
lsamx.move(88.0, wait=True)
|
||||
check(
|
||||
np.isclose(lsamx.readback.get(), 88.0, atol=1e-3),
|
||||
"lamni: lsamx encoder readback after move",
|
||||
)
|
||||
raw_tp = float(lsamx.controller.socket_put_and_receive("TPA"))
|
||||
check(
|
||||
np.isclose(raw_tp, 88.0 * 2000, atol=5), f"lamni: TP returns encoder counts ({raw_tp:.0f})"
|
||||
)
|
||||
lsamx.move(87.5, wait=True)
|
||||
|
||||
# rotation propagates to rt with the 300 - 30.538 deg offset
|
||||
lsamrot.move(310, wait=True)
|
||||
rt_state = rtx.controller.sock.state
|
||||
expected = (310 - 300 + 30.538) / 180 * np.pi
|
||||
check(
|
||||
np.isclose(rt_state.angle_rad, expected, atol=1e-3),
|
||||
"lamni: rt angle follows lsamrot with offset",
|
||||
)
|
||||
lsamrot.move(300, wait=True)
|
||||
|
||||
move_status = lsamrot.move(320, wait=False)
|
||||
time.sleep(0.4)
|
||||
mid = lsamrot.readback.get()
|
||||
while not move_status.done:
|
||||
time.sleep(0.05)
|
||||
check(300 < mid < 320, f"lamni: intermediate readback during lsamrot move ({mid:.2f})")
|
||||
lsamrot.move(300, wait=True)
|
||||
|
||||
check(
|
||||
lsamx.controller.lgalil_is_air_off_and_orchestra_enabled(),
|
||||
"lamni: air off / orchestra enabled interlock",
|
||||
)
|
||||
check(
|
||||
float(lsamx.controller.socket_put_and_receive("MGswver")) == 28.0,
|
||||
"lamni: galil reports swver 28",
|
||||
)
|
||||
|
||||
rtx.controller.feedback_enable_with_reset()
|
||||
check(rtx.controller.feedback_is_running(), "lamni: feedback running after enable with reset")
|
||||
rtx.move(5.0, wait=True)
|
||||
check(np.isclose(rtx.readback.get(), 5.0, atol=1e-3), "lamni: rtx J4 readback after move")
|
||||
|
||||
positions = [[5 + np.sin(i * 0.3), np.cos(i * 0.3)] for i in range(200)]
|
||||
rtx.controller.clear_trajectory_generator()
|
||||
rtx.controller.add_pos_to_scan(positions)
|
||||
row = rtx.controller.socket_put_and_receive("r0").split(",")
|
||||
check(len(row) == 21, f"lamni: sample row has 21 fields ({len(row)})")
|
||||
signals = flyer._get_signals_from_table(row)
|
||||
check(
|
||||
np.isclose(float(signals["target_x"]["value"]), positions[0][0], atol=0.01),
|
||||
"lamni: flyer parses target_x from row",
|
||||
)
|
||||
mode, planned, current = rtx.controller.get_scan_status()
|
||||
check((mode, planned) == (0, 200), "lamni: get_scan_status parses integer sr reply")
|
||||
flyer.stage()
|
||||
status = flyer.kickoff()
|
||||
status.wait(timeout=10)
|
||||
complete_status = flyer.complete()
|
||||
deadline = time.time() + 30
|
||||
while not complete_status.done and time.time() < deadline:
|
||||
time.sleep(0.1)
|
||||
check(complete_status.done, "lamni: flyer completed the scan readout")
|
||||
check(rtx.controller.get_scan_status()[0] == 0, "lamni: scan mode back to 0 after scan")
|
||||
rtx.controller.feedback_disable()
|
||||
|
||||
# coarse-stage coupling: with feedback off, the interferometer follows lsamx/lsamy
|
||||
# through the LamNI geometry (this is what makes the scan-center routine converge)
|
||||
from csaxs_bec.scans.LamNIFermatScan import lamni_from_stage_coordinates
|
||||
|
||||
lsamx.move(87.55, wait=True) # +0.05 mm from center
|
||||
expected_x, expected_y = (val * 1000 for val in lamni_from_stage_coordinates(0.05, 0.0))
|
||||
check(
|
||||
np.isclose(rtx.readback.get(), expected_x, atol=0.5)
|
||||
and np.isclose(rty.readback.get(), expected_y, atol=0.5),
|
||||
f"lamni: interferometer follows coarse stage ({rtx.readback.get():.1f} um)",
|
||||
)
|
||||
lsamx.move(87.5, wait=True)
|
||||
check(
|
||||
np.isclose(rtx.readback.get(), 0.0, atol=0.5), "lamni: interferometer back to 0 at center"
|
||||
)
|
||||
print("--- LamNI checks passed ---\n")
|
||||
|
||||
|
||||
def run_omny(): # noqa: C901
|
||||
from csaxs_bec.devices.sim.sim_omny import (
|
||||
SimOMNYDewar,
|
||||
SimOMNYGalilMotor,
|
||||
SimOMNYSampleStorage,
|
||||
SimOMNYTemperatures,
|
||||
SimRtOMNYFlyer,
|
||||
SimRtOMNYMotor,
|
||||
)
|
||||
|
||||
dm = FakeDeviceManager()
|
||||
host = "mpc2412.psi.ch"
|
||||
otransy = dm.add(
|
||||
"otransy",
|
||||
SimOMNYGalilMotor(
|
||||
"E",
|
||||
name="otransy",
|
||||
host=host,
|
||||
port=8081,
|
||||
sign=1,
|
||||
device_manager=dm,
|
||||
sim_initial_position=0,
|
||||
),
|
||||
)
|
||||
otracky = dm.add(
|
||||
"otracky",
|
||||
SimOMNYGalilMotor(
|
||||
"G",
|
||||
name="otracky",
|
||||
host=host,
|
||||
port=8082,
|
||||
sign=1,
|
||||
device_manager=dm,
|
||||
sim_initial_position=2.5,
|
||||
),
|
||||
)
|
||||
rtx = dm.add(
|
||||
"rtx",
|
||||
SimRtOMNYMotor(
|
||||
"A", name="rtx", host=host, port=3333, sign=1, device_manager=dm, limits=[-200, 200]
|
||||
),
|
||||
user_parameter={"low_signal": 8500, "min_signal": 8000},
|
||||
)
|
||||
rty = dm.add(
|
||||
"rty",
|
||||
SimRtOMNYMotor(
|
||||
"B", name="rty", host=host, port=3333, sign=1, device_manager=dm, limits=[-100, 100]
|
||||
),
|
||||
)
|
||||
rtz = dm.add(
|
||||
"rtz",
|
||||
SimRtOMNYMotor(
|
||||
"C", name="rtz", host=host, port=3333, sign=1, device_manager=dm, limits=[-100, 100]
|
||||
),
|
||||
)
|
||||
flyer = dm.add(
|
||||
"rt_positions",
|
||||
SimRtOMNYFlyer(
|
||||
name="rt_positions", host=host, port=3333, device_manager=dm, sim_point_dwell_s=0.005
|
||||
),
|
||||
)
|
||||
for device in (otransy, otracky, rtx, rty, rtz):
|
||||
device.wait_for_connection()
|
||||
flyer.controller.on()
|
||||
print("--- OMNY simulated devices connected ---")
|
||||
|
||||
con = otransy.controller
|
||||
check(float(con.socket_put_and_receive("MGswver")) == 134.0, "omny: galil reports swver 134")
|
||||
check(
|
||||
float(con.socket_put_and_receive("MGtempab")) == 0.0
|
||||
and float(con.socket_put_and_receive("MGtimeab")) == 0.0,
|
||||
"omny: temperature/time abort flags clear",
|
||||
)
|
||||
check(
|
||||
float(con.socket_put_and_receive("MGtoppin")) < -25.4,
|
||||
"omny: toppin position below threshold",
|
||||
)
|
||||
|
||||
# motor resolution hardcoded python-side must match seeded stppermm
|
||||
check(np.isclose(otransy.motor_resolution.get(), 51200), "omny: motor resolution 51200")
|
||||
otransy.move(5.0, wait=True)
|
||||
check(np.isclose(otransy.readback.get(), 5.0, atol=1e-3), "omny: otransy moved to 5.0")
|
||||
|
||||
# shuttle-style direct axis commands
|
||||
con.socket_put_confirmed("SPG=15000")
|
||||
con.socket_put_confirmed("SHG")
|
||||
con.socket_put_confirmed("PRG=17000")
|
||||
con.socket_put_confirmed("BGG")
|
||||
time.sleep(0.1)
|
||||
while bool(float(con.socket_put_and_receive("MG_BGG").strip())):
|
||||
time.sleep(0.05)
|
||||
shuttle_pos = float(con.socket_put_and_receive("TDG"))
|
||||
check(
|
||||
np.isclose(shuttle_pos, 17000, atol=10),
|
||||
f"omny: shuttle PR/BG relative move ({shuttle_pos:.0f})",
|
||||
)
|
||||
con.socket_put_confirmed("MOG")
|
||||
|
||||
# gripper transfer with mntax=4 (otransy axis E): identical protocol to flomni
|
||||
con.socket_put_confirmed("XQ#MNTMODE")
|
||||
con.socket_put_confirmed("atconfir=1") # autoconfirm for unattended harness run
|
||||
con.socket_put_confirmed("mntaprch=-17.5")
|
||||
con.socket_put_confirmed("XQ#GRPUT,3")
|
||||
time.sleep(0.2)
|
||||
check(
|
||||
int(float(con.socket_put_and_receive("MGmntprgs").strip())) == -1,
|
||||
"omny: unmount in progress reports mntprgs -1",
|
||||
)
|
||||
deadline = time.time() + 30
|
||||
while time.time() < deadline:
|
||||
if not float(con.socket_put_and_receive("MGmntprgs").strip()):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
check(
|
||||
float(con.socket_put_and_receive("MGmntprgs").strip()) == 0,
|
||||
"omny: #GRPUT finished with autoconfirm",
|
||||
)
|
||||
con.socket_put_confirmed("XQ#GRSTAT")
|
||||
time.sleep(0.1)
|
||||
check(
|
||||
bool(float(con.socket_put_and_receive("MGgrstat").strip())), "omny: #GRSTAT reports success"
|
||||
)
|
||||
con.socket_put_confirmed("XQ#POSMODE")
|
||||
|
||||
# RT: J2 signal strengths, feedback, scan rows
|
||||
j2 = rtx.controller.socket_put_and_receive("J2").strip().split(",")
|
||||
check(len(j2) == 6, f"omny: J2 returns 6 fields ({len(j2)})")
|
||||
rtx.controller.socket_put("l1")
|
||||
check(rtx.controller.feedback_is_running(), "omny: feedback running after l1")
|
||||
rtx.move(3.0, wait=True)
|
||||
check(np.isclose(rtx.readback.get(), 3.0, atol=1e-3), "omny: rtx pr readback after move")
|
||||
|
||||
positions = [[3 + np.sin(i * 0.3), np.cos(i * 0.3), 0.0] for i in range(200)]
|
||||
rtx.controller.clear_trajectory_generator()
|
||||
rtx.controller.add_pos_to_scan(positions)
|
||||
row = rtx.controller.socket_put_and_receive("r0").split(",")
|
||||
check(len(row) == 19, f"omny: sample row has 19 fields ({len(row)})")
|
||||
signals = flyer._get_signals_from_table(row)
|
||||
check(
|
||||
np.isclose(float(signals["target_x"]["value"]), positions[0][0], atol=0.01),
|
||||
"omny: flyer parses target_x from row",
|
||||
)
|
||||
|
||||
# EPICS periphery
|
||||
storage = SimOMNYSampleStorage(name="omny_samples")
|
||||
check(
|
||||
storage.is_sample_slot_used("A", 1) and storage.is_sample_in_samplestage(),
|
||||
"omny: sample storage boots seeded (slots + samplestage)",
|
||||
)
|
||||
SimOMNYDewar(name="omny_dewar")
|
||||
SimOMNYTemperatures(name="omny_temperatures")
|
||||
check(True, "omny: dewar and temperatures instantiate with mock PVs")
|
||||
print("--- OMNY checks passed ---\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_lamni()
|
||||
run_omny()
|
||||
print("All LamNI/OMNY harness checks passed.")
|
||||
Reference in New Issue
Block a user