Changes for X10DA
CI for debye_bec / test (push) Successful in 1m1s

This commit is contained in:
Stephan
2026-06-29 07:45:11 +02:00
parent fb752d5311
commit 215d6a90ce
9 changed files with 645 additions and 187 deletions
@@ -6,12 +6,12 @@ import numpy as np
from bec_lib import bec_logger
from .. import parameters as bl
from ..types import ConfigDict
from ..types import BeamlineId, ConfigDict
logger = bec_logger.logger
def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]:
def calc_positions(beamline: BeamlineId, cfg: ConfigDict) -> dict[str, dict[str, float]]:
"""
Calculates the positions of axes based on a beamline config.
@@ -150,7 +150,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]:
d = bl.opSlits1.center[1] - bl.cm.center[1] - dz
sl1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
pos["sl1_centery"] = {"value": sl1_beam_height}
pos["sl1_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam
pos["sl1_gapy"] = {"value": beam_vs}
## Beam Monitor 1
d = bl.opBM1.center[1] - bl.cm.center[1] - dz
@@ -226,10 +226,11 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]:
pos["fm_rotz"] = {"value": 0}
## Slits 2
d = bl.opSlits2.center[1] - bl.fm.center[1]
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["sl2_centery"] = {"value": sl2_beam_height}
pos["sl2_gapy"] = {"value": beam_vs + 1} # Add 0.5 mm space on both sides of the beam
if hasattr(bl, "opSlits2"):
d = bl.opSlits2.center[1] - bl.fm.center[1]
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["sl2_centery"] = {"value": sl2_beam_height}
pos["sl2_gapy"] = {"value": beam_vs}
## Beam Monitor 2
d = bl.opBM2.center[1] - bl.fm.center[1]
@@ -238,22 +239,59 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]:
## Optical Table
# TRY
d = bl.ehWindow.center[1] - bl.fm.center[1]
ot_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["ot_try"] = {"value": ot_height}
if beamline == "x01da":
# TRY
d = bl.ehWindow.center[1] - bl.fm.center[1]
ot_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["ot_try"] = {"value": ot_height}
# Pitch
ot_pitch = -(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])
pos["ot_rotx"] = {"value": ot_pitch * 1e3}
# Pitch
ot_pitch = -(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])
pos["ot_rotx"] = {"value": ot_pitch * 1e3}
# TRZ ES1
ot_es1_trz = cfg["smpl"]
pos["ot_es1_trz"] = {"value": ot_es1_trz}
# TRZ ES1
ot_es1_trz = cfg["smpl"]
pos["ot_es1_trz"] = {"value": ot_es1_trz}
# ES0 exit window
pos["es0wi_try"] = {
"value": 5
} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
# ES0 exit window
pos["es0wi_try"] = {
"value": 5
} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
else:
# Exit window height
d = bl.ehWindow.center[1] - bl.fm.center[1]
es0wi_try = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["es0wi_try"] = {"value": es0wi_try}
# ES1 table height
d = bl.es1.center[1] - bl.fm.center[1]
es1_try = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["es1_try"] = {"value": es1_try}
# IC0 height
d = bl.es1ic0.center[1] - bl.fm.center[1]
es1ic0_try = (
fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - es1_try
)
pos["es1ic0_try"] = {"value": es1ic0_try}
# IC1 height
d = bl.es1ic1.center[1] - bl.fm.center[1]
es1ic1_try = (
fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - es1_try
)
pos["es1ic1_try"] = {"value": es1ic1_try}
# IC2 height
d = bl.es1ic2.center[1] - bl.fm.center[1]
es1ic2_try = (
fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - es1_try
)
pos["es1ic2_try"] = {"value": es1ic2_try}
# ES2 table height
d = bl.es2.center[1] - bl.fm.center[1]
es2_try = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
pos["es2_try"] = {"value": es2_try}
return pos
@@ -37,7 +37,7 @@ def calc_surfaces(cfg: ConfigDict) -> SurfaceDict:
w1 = 2 * (bl.cm.center[1] - l / 2) * np.tan(cfg["h_acc"])
w2 = 2 * (bl.cm.center[1] + l / 2) * np.tan(cfg["h_acc"])
index = bl.cm.surface.index(cfg["cm_stripe"])
# index = bl.cm.surface.index(cfg["cm_stripe"])
cen = -cfg["cm_trx"]
@@ -96,6 +96,8 @@ def calc_surfaces(cfg: ConfigDict) -> SurfaceDict:
out["mo1_2"]["x"] = []
out["mo1_2"]["y"] = []
if cfg["fm_stripe"] is None:
return out
# Focusing mirror
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
surface = bl.fm.surfaceToroid
@@ -85,9 +85,10 @@ def fm_trx_to_stripe(fm_trx: float) -> str | None:
str | None: Stripe of the mirror, None if not found
"""
fm_stripe = None
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if low <= fm_trx <= high:
fm_stripe = name + " (flat)"
if hasattr(bl.fm, "surfaceFlat"):
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if low <= fm_trx <= high:
fm_stripe = name + " (flat)"
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
if low <= fm_trx <= high:
fm_stripe = name + " (toroid)"
@@ -105,9 +106,10 @@ def fm_stripe_to_trx(fm_stripe: str) -> float | None:
Returns:
float | None: TRX value of the stripe. None if not found
"""
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if fm_stripe == name + " (flat)":
return (low + high) / 2
if hasattr(bl.fm, "surfaceFlat"):
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
if fm_stripe == name + " (flat)":
return (low + high) / 2
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
if fm_stripe == name + " (toroid)":
return -(low + high) / 2
@@ -165,6 +167,8 @@ def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float:
Returns:
float: Reflectivity [0-1]
"""
if cm_stripe is None:
return np.nan
index = bl.cm.surface.index(cm_stripe)
rs, _ = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2]
refl = abs(rs) ** 2
@@ -184,6 +188,8 @@ def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float:
Returns:
float: Reflectivity [0-1]
"""
if fm_stripe is None:
return np.nan
if fm_stripe in ("Rh (toroid)", "Pt (toroid)"):
surface = bl.fm.surfaceToroid
material = bl.fm.materialToroid
@@ -391,6 +397,8 @@ def wall_geometries() -> list[list[float]]:
list[list[float]]: List of [x, y, width, height] geometry values for each wall.
"""
geom = []
if not hasattr(bl, "walls"):
return geom
for i, _ in enumerate(bl.walls.start):
geom.append(
[
@@ -413,6 +421,8 @@ def pipe_geometries() -> list[dict[str, np.ndarray]]:
the start and end coordinates of the pipe top and bottom edges.
"""
pipes = []
if not hasattr(bl, "vacuum_pipes"):
return pipes
for i, _ in enumerate(bl.vacuum_pipes.center):
top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
@@ -439,6 +449,8 @@ def table_to_smpl_pos(table: str) -> float:
table (str): Table name, e.g. ES1 or ES2
"""
if table in bl.tables:
return bl.tables[table]["smpl"]
if table == bl.es1.name:
return bl.es1.center[1]
if table == bl.es2.name:
return bl.es2.center[1]
raise ValueError(f"Table {table} not found in beamline parameter file")
@@ -117,12 +117,12 @@ class DigitalTwin(BECWidget, QWidget):
self.plot_layout.setContentsMargins(4, 4, 4, 4)
self.plot_layout.setSpacing(6)
self.sideview_plot = SideviewPlot()
self.surface_plots = SurfacePlots()
self.surface_plots = SurfacePlots(self.beamline)
self.plot_layout.addWidget(self.sideview_plot, stretch=1)
self.plot_layout.addWidget(self.surface_plots, stretch=1)
self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.mover = MoverPanel(self.dev)
self.mover = MoverPanel(self.beamline, self.dev)
self.input_scroll = self._scroll_area(self.input_widget, min_width=320, max_width=360)
self.mover_scroll = self._scroll_area(self.mover, min_width=380, max_width=460)
@@ -232,7 +232,6 @@ class DigitalTwin(BECWidget, QWidget):
"fm_roty",
"fm_rotz",
"bm2_try",
"ot_try",
"es0wi_try",
]
if self.beamline == "x01da": # X01DA specific devices
@@ -467,7 +466,14 @@ class DigitalTwin(BECWidget, QWidget):
fm_stripe = fm_trx_to_stripe(-fm_trx)
fm_rotx = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
fm_rotx_real = 2 * cm_pitch - fm_rotx
smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
match self.input.smpl:
case InputNumberField():
smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
case ComboBox():
table = self.input.smpl.currentText()
smpl = table_to_smpl_pos(table)
raw = { # Config in SI units!
"energy": mo1_bragg["mo1_bragg"]["value"],
"h_acc": h_acc,
@@ -525,17 +531,34 @@ class DigitalTwin(BECWidget, QWidget):
self.mover.fm_rotx.set_feedback(fm_rotx)
self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)["fm_roty"]["value"])
self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)["fm_rotz"]["value"])
self.mover.sl2_centery.set_feedback(
self.dev.sl2_centery.read(cached=True)["sl2_centery"]["value"]
)
self.mover.sl2_gapy.set_feedback(self.dev.sl2_gapy.read(cached=True)["sl2_gapy"]["value"])
if self.beamline == "x01da":
self.mover.sl2_centery.set_feedback(
self.dev.sl2_centery.read(cached=True)["sl2_centery"]["value"]
)
self.mover.sl2_gapy.set_feedback(
self.dev.sl2_gapy.read(cached=True)["sl2_gapy"]["value"]
)
self.mover.bm2_try.set_feedback(self.dev.bm2_try.read(cached=True)["bm2_try"]["value"])
self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)["ot_try"]["value"])
self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)["ot_rotx"]["value"])
self.mover.ot_es1_trz.set_feedback(smpl)
if self.beamline == "x01da":
self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)["ot_try"]["value"])
self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)["ot_rotx"]["value"])
self.mover.ot_es1_trz.set_feedback(smpl)
self.mover.es0wi_try.set_feedback(
self.dev.es0wi_try.read(cached=True)["es0wi_try"]["value"]
)
if self.beamline == "x10da":
self.mover.es1_try.set_feedback(self.dev.es1_try.read(cached=True)["es1_try"]["value"])
self.mover.es1ic0_try.set_feedback(
self.dev.es1ic0_try.read(cached=True)["es1ic0_try"]["value"]
)
self.mover.es1ic1_try.set_feedback(
self.dev.es1ic1_try.read(cached=True)["es1ic1_try"]["value"]
)
self.mover.es1ic2_try.set_feedback(
self.dev.es1ic2_try.read(cached=True)["es1ic2_try"]["value"]
)
self.mover.es2_try.set_feedback(self.dev.es2_try.read(cached=True)["es2_try"]["value"])
self.mover.abs.set_feedback(abs_open)
return config
@@ -553,7 +576,8 @@ class DigitalTwin(BECWidget, QWidget):
pos["mo1_trx"] = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"]
pos["fm_trx"] = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"]
pos["fm_rotx"] = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
if self.beamline == "x01da":
pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
# Removing offsets
for axis, _ in pos.items():
@@ -819,7 +843,7 @@ class DigitalTwin(BECWidget, QWidget):
"""
Calculates the positions for the axes based on the assistant values
"""
out = calc_positions(self.get_assistant_config())
out = calc_positions(self.beamline, self.get_assistant_config())
# Apply offsets
for axis, axis_data in out.items():
@@ -851,13 +875,21 @@ class DigitalTwin(BECWidget, QWidget):
self.mover.fm_rotx.set_target(out["fm_rotx"]["value"])
self.mover.fm_roty.set_target(out["fm_roty"]["value"])
self.mover.fm_rotz.set_target(out["fm_rotz"]["value"])
self.mover.sl2_centery.set_target(out["sl2_centery"]["value"])
self.mover.sl2_gapy.set_target(out["sl2_gapy"]["value"])
if self.beamline == "x01da":
self.mover.sl2_centery.set_target(out["sl2_centery"]["value"])
self.mover.sl2_gapy.set_target(out["sl2_gapy"]["value"])
self.mover.bm2_try.set_target(out["bm2_try"]["value"])
self.mover.ot_try.set_target(out["ot_try"]["value"])
self.mover.ot_rotx.set_target(out["ot_rotx"]["value"])
self.mover.ot_es1_trz.set_target(out["ot_es1_trz"]["value"])
if self.beamline == "x01da":
self.mover.ot_try.set_target(out["ot_try"]["value"])
self.mover.ot_rotx.set_target(out["ot_rotx"]["value"])
self.mover.ot_es1_trz.set_target(out["ot_es1_trz"]["value"])
self.mover.es0wi_try.set_target(out["es0wi_try"]["value"])
if self.beamline == "x10da":
self.mover.es1_try.set_target(out["es1_try"]["value"])
self.mover.es1ic0_try.set_target(out["es1ic0_try"]["value"])
self.mover.es1ic1_try.set_target(out["es1ic1_try"]["value"])
self.mover.es1ic2_try.set_target(out["es1ic2_try"]["value"])
self.mover.es2_try.set_target(out["es2_try"]["value"])
def calc_mo1_bragg_angle(self):
"""
@@ -7,6 +7,7 @@ from typing import Literal
# pylint: disable=E0611
from qtpy.QtWidgets import QVBoxLayout, QWidget
from ..types import BeamlineId
from ..widgets.move_widget import AbsorberWidget, MoveWidget
from ..widgets.qt_widgets import Group
@@ -14,7 +15,7 @@ from ..widgets.qt_widgets import Group
class MoverPanel(QWidget):
""" "Panel to move an axis to a certain position"""
def __init__(self, dev, parent=None):
def __init__(self, beamline: BeamlineId, dev, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(4, 4, 4, 4)
@@ -24,12 +25,24 @@ class MoverPanel(QWidget):
# FE Slits
self.sldi_gapx = MoveWidget(
dev=dev, motor="sldi_gapx", label="GAPX", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="sldi_gapx",
label="GAPX",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.sldi_gapx)
self.sldi_gapy = MoveWidget(
dev=dev, motor="sldi_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="sldi_gapy",
label="GAPY",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.sldi_gapy)
@@ -42,22 +55,46 @@ class MoverPanel(QWidget):
# Collimating mirror
self.cm_trx = MoveWidget(
dev=dev, motor="cm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="cm_trx",
label="TRX",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.cm_trx)
self.cm_try = MoveWidget(
dev=dev, motor="cm_try", label="TRY", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="cm_try",
label="TRY",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.cm_try)
self.cm_bnd = MoveWidget(
dev=dev, motor="cm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2
beamline=beamline,
dev=dev,
motor="cm_bnd",
label="BENDER",
unit="km",
decimals=2,
deadband=0.2,
)
self.mover_widgets.append(self.cm_bnd)
self.cm_rotx = MoveWidget(
dev=dev, motor="cm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01
beamline=beamline,
dev=dev,
motor="cm_rotx",
label="PITCH",
unit="mrad",
decimals=3,
deadband=0.01,
)
self.mover_widgets.append(self.cm_rotx)
@@ -67,6 +104,7 @@ class MoverPanel(QWidget):
# Monochromator
self.mo1_bragg_angle = MoveWidget(
beamline=beamline,
dev=dev,
motor="mo1_bragg_angle",
label="Bragg Angle",
@@ -77,12 +115,24 @@ class MoverPanel(QWidget):
self.mover_widgets.append(self.mo1_bragg_angle)
self.mo1_trx = MoveWidget(
dev=dev, motor="mo1_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="mo1_trx",
label="TRX",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.mo1_trx)
self.mo1_try = MoveWidget(
dev=dev, motor="mo1_try", label="TRY", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="mo1_try",
label="TRY",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.mo1_try)
@@ -92,12 +142,24 @@ class MoverPanel(QWidget):
# OP Slits 1
self.sl1_centery = MoveWidget(
dev=dev, motor="sl1_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1
beamline=beamline,
dev=dev,
motor="sl1_centery",
label="CENTERY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.sl1_centery)
self.sl1_gapy = MoveWidget(
dev=dev, motor="sl1_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1
beamline=beamline,
dev=dev,
motor="sl1_gapy",
label="GAPY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.sl1_gapy)
@@ -105,7 +167,13 @@ class MoverPanel(QWidget):
# OP Beam Monitor 1
self.bm1_try = MoveWidget(
dev=dev, motor="bm1_try", label="TRY", unit="mm", decimals=2, deadband=0.1
beamline=beamline,
dev=dev,
motor="bm1_try",
label="TRY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.bm1_try)
@@ -113,32 +181,68 @@ class MoverPanel(QWidget):
# Focusing Mirror
self.fm_trx = MoveWidget(
dev=dev, motor="fm_trx", label="TRX", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="fm_trx",
label="TRX",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.fm_trx)
self.fm_try = MoveWidget(
dev=dev, motor="fm_try", label="TRY", unit="mm", decimals=2, deadband=0.01
beamline=beamline,
dev=dev,
motor="fm_try",
label="TRY",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.fm_try)
self.fm_bnd = MoveWidget(
dev=dev, motor="fm_bnd", label="BENDER", unit="km", decimals=2, deadband=0.2
beamline=beamline,
dev=dev,
motor="fm_bnd",
label="BENDER",
unit="km",
decimals=2,
deadband=0.2,
)
self.mover_widgets.append(self.fm_bnd)
self.fm_rotx = MoveWidget(
dev=dev, motor="fm_rotx", label="PITCH", unit="mrad", decimals=3, deadband=0.01
beamline=beamline,
dev=dev,
motor="fm_rotx",
label="PITCH",
unit="mrad",
decimals=3,
deadband=0.01,
)
self.mover_widgets.append(self.fm_rotx)
self.fm_roty = MoveWidget(
dev=dev, motor="fm_roty", label="YAW", unit="mrad", decimals=3, deadband=0.01
beamline=beamline,
dev=dev,
motor="fm_roty",
label="YAW",
unit="mrad",
decimals=3,
deadband=0.01,
)
self.mover_widgets.append(self.fm_roty)
self.fm_rotz = MoveWidget(
dev=dev, motor="fm_rotz", label="ROLL", unit="mrad", decimals=3, deadband=0.01
beamline=beamline,
dev=dev,
motor="fm_rotz",
label="ROLL",
unit="mrad",
decimals=3,
deadband=0.01,
)
self.mover_widgets.append(self.fm_rotz)
@@ -147,74 +251,202 @@ class MoverPanel(QWidget):
[self.fm_trx, self.fm_try, self.fm_bnd, self.fm_rotx, self.fm_roty, self.fm_rotz],
)
# OP Slits 2
self.sl2_centery = MoveWidget(
dev=dev, motor="sl2_centery", label="CENTERY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.sl2_centery)
if beamline == "x01da":
# OP Slits 2
self.sl2_centery = MoveWidget(
beamline=beamline,
dev=dev,
motor="sl2_centery",
label="CENTERY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.sl2_centery)
self.sl2_gapy = MoveWidget(
dev=dev, motor="sl2_gapy", label="GAPY", unit="mm", decimals=2, deadband=0.1
)
self.mover_widgets.append(self.sl2_gapy)
self.sl2_gapy = MoveWidget(
beamline=beamline,
dev=dev,
motor="sl2_gapy",
label="GAPY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.sl2_gapy)
self.sl2_mov_group = Group("OP Slits 2", [self.sl2_centery, self.sl2_gapy])
self.sl2_mov_group = Group("OP Slits 2", [self.sl2_centery, self.sl2_gapy])
# OP Beam Monitor 2
self.bm2_try = MoveWidget(
dev=dev, motor="bm2_try", label="TRY", unit="mm", decimals=2, deadband=0.1
beamline=beamline,
dev=dev,
motor="bm2_try",
label="TRY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.bm2_try)
self.bm2_mov_group = Group("OP Beam Monitor 2", [self.bm2_try])
# Optical Table
self.ot_try = MoveWidget(
dev=dev, motor="ot_try", label="TRY", unit="mm", decimals=2, deadband=0.2
)
self.mover_widgets.append(self.ot_try)
if beamline == "x01da":
# Optical Table
self.ot_try = MoveWidget(
beamline=beamline,
dev=dev,
motor="ot_try",
label="TRY",
unit="mm",
decimals=2,
deadband=0.2,
)
self.mover_widgets.append(self.ot_try)
self.ot_rotx = MoveWidget(
dev=dev, motor="ot_rotx", label="ROTX", unit="mrad", decimals=3, deadband=0.05
)
self.mover_widgets.append(self.ot_rotx)
self.ot_rotx = MoveWidget(
beamline=beamline,
dev=dev,
motor="ot_rotx",
label="ROTX",
unit="mrad",
decimals=3,
deadband=0.05,
)
self.mover_widgets.append(self.ot_rotx)
self.ot_mov_group = Group("Optical Table", [self.ot_try, self.ot_rotx])
self.ot_mov_group = Group("Optical Table", [self.ot_try, self.ot_rotx])
# Experimental Station 0
self.es0wi_try = MoveWidget(
dev=dev, motor="es0wi_try", label="ES0 WI", unit="mm", decimals=0, deadband=0.1
beamline=beamline,
dev=dev,
motor="es0wi_try",
label="ES0 WI",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.es0wi_try)
self.es0_mov_group = Group("Experimental Station 0", [self.es0wi_try])
# Experimental Station 1
self.ot_es1_trz = MoveWidget(
dev=dev, motor="ot_es1_trz", label="ES1 TRZ", unit="mm", decimals=0, deadband=5
)
self.mover_widgets.append(self.ot_es1_trz)
if beamline == "x01da":
self.ot_es1_trz = MoveWidget(
beamline=beamline,
dev=dev,
motor="ot_es1_trz",
label="ES1 TRZ",
unit="mm",
decimals=0,
deadband=5,
)
self.mover_widgets.append(self.ot_es1_trz)
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz])
if beamline == "x10da":
self.es1_try = MoveWidget(
beamline=beamline,
dev=dev,
motor="es1_try",
label="ES1 TRY",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.es1_try)
self.es1ic0_try = MoveWidget(
beamline=beamline,
dev=dev,
motor="es1ic0_try",
label="IC0 TRY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.es1ic0_try)
self.es1ic1_try = MoveWidget(
beamline=beamline,
dev=dev,
motor="es1ic1_try",
label="IC1 TRY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.es1ic1_try)
self.es1ic2_try = MoveWidget(
beamline=beamline,
dev=dev,
motor="es1ic2_try",
label="IC2 TRY",
unit="mm",
decimals=2,
deadband=0.1,
)
self.mover_widgets.append(self.es1ic2_try)
if beamline == "x01da":
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz])
else:
self.es1_mov_group = Group(
"Experimental Station 1", [self.es1_try, self.es1ic1_try, self.es1ic2_try]
)
# Experimental Station 2
if beamline == "x10da":
self.es2_try = MoveWidget(
beamline=beamline,
dev=dev,
motor="es2_try",
label="ES2 TRY",
unit="mm",
decimals=2,
deadband=0.01,
)
self.mover_widgets.append(self.es2_try)
self.es2_mov_group = Group("Experimental Station 2", [self.es2_try])
# Assemble complete mover group
self.mover_group = Group(
"Mover",
[
self.sldi_mov_group,
self.abs_group,
self.cm_mov_group,
self.mo1_mov_group,
self.sl1_mov_group,
self.bm1_mov_group,
self.fm_mov_group,
self.sl2_mov_group,
self.bm2_mov_group,
self.ot_mov_group,
self.es0_mov_group,
self.es1_mov_group,
],
)
if beamline == "x01da":
self.mover_group = Group(
"Mover",
[
self.sldi_mov_group,
self.abs_group,
self.cm_mov_group,
self.mo1_mov_group,
self.sl1_mov_group,
self.bm1_mov_group,
self.fm_mov_group,
self.sl2_mov_group,
self.bm2_mov_group,
self.ot_mov_group,
self.es0_mov_group,
self.es1_mov_group,
],
)
else:
self.mover_group = Group(
"Mover",
[
self.sldi_mov_group,
self.abs_group,
self.cm_mov_group,
self.mo1_mov_group,
self.sl1_mov_group,
self.bm1_mov_group,
self.fm_mov_group,
self.bm2_mov_group,
self.es0_mov_group,
self.es1_mov_group,
self.es2_mov_group,
],
)
self._layout.addWidget(self.mover_group)
self._layout.addStretch()
@@ -21,7 +21,7 @@ from ..calculations.calc_varia import (
pipe_geometries,
wall_geometries,
)
from ..types import DataDict, SurfaceDict
from ..types import BeamlineId, DataDict, SurfaceDict
from ..widgets.qt_widgets import Group
logger = bec_logger.logger
@@ -30,8 +30,9 @@ logger = bec_logger.logger
class SurfacePlots(QWidget):
"""Plot widget with two curves and legend."""
def __init__(self, parent=None):
def __init__(self, beamline: BeamlineId, parent=None):
super().__init__(parent=parent)
self.beamline = beamline
self._layout = QHBoxLayout(self)
self._layout.setContentsMargins(4, 4, 4, 4)
self._layout.setSpacing(6)
@@ -174,7 +175,8 @@ class SurfacePlots(QWidget):
elif name == "mo1_2":
plot_surface(plot["widget"], mo_surface_geometries("mo1", 1))
elif name == "fm":
plot_surface(plot["widget"], mirror_surface_geometries("fm_flat"))
if self.beamline == "x01da":
plot_surface(plot["widget"], mirror_surface_geometries("fm_flat"))
plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid"))
else:
raise ValueError(f"Plot {name} not found!")
@@ -17,6 +17,7 @@ from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidg
# pylint: disable=E0402
from .....devices.absorber import STATUS as ABS_STATUS
from ..types import BeamlineId
logger = bec_logger.logger
@@ -132,8 +133,9 @@ class MotionWorker(QObject):
error = Signal()
finished = Signal()
def __init__(self, dev, motor, target_pos: float):
def __init__(self, beamline: BeamlineId, dev, motor, target_pos: float):
super().__init__()
self.beamline = beamline
self.dev = dev
self.motor = motor
self._target = target_pos
@@ -182,16 +184,23 @@ class MotionWorker(QObject):
],
)
case "cm_bnd":
p1 = (
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
) / 2e-6
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
if self.beamline == "x01da":
p1 = (
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
) / 2e-6
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
else:
p1 = 541900 / self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] - 32570
p2 = 541900 / self._target - 32570
self._target = p2 - p1
self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]})
case "mo1_try" | "mo1_trx" | "mo1_roty":
self.motion(abs_closed=True)
case "mo1_bragg_angle":
self.motion()
if self.beamline == "x01da":
self.motion()
else: # x10da needs to move goniometer
self.motion(alias="mo1_rotx")
case "sl1_centery" | "sl1_gapy" | "bm1_try":
self.motion()
case "fm_trx":
@@ -204,44 +213,77 @@ class MotionWorker(QObject):
abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}]
)
case "fm_try":
if self.beamline == "x01da":
abs_tol = 0.05
else: # superxas mirror less stable thus needs higher tolerance
abs_tol = 0.2
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["fm_rotx"], "abs_tol": 0.05},
{"device": self.dev["fm_rotz"], "abs_tol": 0.05},
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
],
)
case "fm_rotx":
if self.beamline == "x01da":
abs_tol = 0.05
else: # superxas mirror less stable thus needs higher tolerance
abs_tol = 0.2
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["fm_try"], "abs_tol": 0.05},
{"device": self.dev["fm_rotz"], "abs_tol": 0.05},
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
],
)
case "fm_rotz":
if self.beamline == "x01da":
abs_tol = 0.05
else: # superxas mirror less stable thus needs higher tolerance
abs_tol = 0.2
self.motion(
abs_closed=True,
surveyed_axes=[
{"device": self.dev["fm_try"], "abs_tol": 0.05},
{"device": self.dev["fm_rotx"], "abs_tol": 0.05},
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
],
)
case "fm_bnd":
p1 = (
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3) + 4.28e-5
) / 1.84e-9
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
if self.beamline == "x01da":
p1 = (
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3)
+ 4.28e-5
) / 1.84e-9
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
else:
p1 = (
593088.7 / self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"]
+ 26124.41
)
p2 = 593088.7 / self._target + 26124.41
self._target = p2 - p1
self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]})
case "sl2_centery" | "sl2_gapy" | "bm2_try":
self.motion()
case "ot_try" | "ot_rotx" | "ot_es1_trz":
self.motion()
case "es0wi_try":
self.motion()
case "es1_try" | "es2_try":
self.motion()
case "es1ic0_try" | "es1ic1_try" | "es1ic2_try":
self.motion()
case _:
logger.warning(f"Motor {self.motor} not integrated in digital twin!")
def motion(self, abs_closed: bool = False, relative: bool = False, rb=None, surveyed_axes=None):
def motion(
self,
abs_closed: bool = False,
relative: bool = False,
rb=None,
surveyed_axes=None,
alias=None,
):
"""
Moves an axis while surverying a set of axes (if set).
Example surveyed_axes:
@@ -250,44 +292,53 @@ class MotionWorker(QObject):
Args:
surveyed_axes (list): List of dictionaries of devices
"""
if abs_closed:
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
status = self.dev.abs.close()
# TODO Set timeout to 0.001 and check if it actually raises
# (it should not start motion).
# Check of behavior of digital twin afterwards.
status.wait(timeout=5)
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
surv_ax["name"] = surv_ax["device"].dotted_name
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
if rb is not None:
rb["name"] = rb["device"].dotted_name
status = self.dev[self.motor].move(self._target, relative=relative)
last_check = time.time()
update_interval = 0.1
while status.status == "RUNNING":
now = time.time()
if time.time() - last_check < update_interval:
time.sleep(0.01)
last_check = now
if self._stop_flag.is_set():
self.dev[self.motor].stop()
self._stop_flag.clear()
if rb is not None:
self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"])
else:
self.position_changed.emit(
self.dev[self.motor].read(cached=True)[self.motor]["value"]
)
try:
if alias:
self.motor = alias
if abs_closed:
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
status = self.dev.abs.close()
# TODO Set timeout to 0.001 and check if it actually raises
# (it should not start motion).
# Check of behavior of digital twin afterwards.
status.wait(timeout=5)
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
self.dev[self.motor].stop()
self.error.emit()
break
self.finished.emit()
surv_ax["name"] = surv_ax["device"].dotted_name
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]][
"value"
]
if rb is not None:
rb["name"] = rb["device"].dotted_name
status = self.dev[self.motor].move(self._target, relative=relative)
last_check = time.time()
update_interval = 0.1
while status.status == "RUNNING":
now = time.time()
if time.time() - last_check < update_interval:
time.sleep(0.01)
last_check = now
if self._stop_flag.is_set():
self.dev[self.motor].stop()
self._stop_flag.clear()
if rb is not None:
self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"])
else:
self.position_changed.emit(
self.dev[self.motor].read(cached=True)[self.motor]["value"]
)
if surveyed_axes is not None:
for surv_ax in surveyed_axes:
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
self.dev[self.motor].stop()
self.error.emit()
self.finished.emit()
break
self.finished.emit()
except:
self.error.emit()
self.finished.emit()
class MoveWidget(QWidget):
@@ -299,10 +350,13 @@ class MoveWidget(QWidget):
- Start / Stop button
"""
def __init__(self, dev, motor, label: str = "", unit=None, decimals=3, deadband=0.0):
def __init__(
self, beamline: BeamlineId, dev, motor, label: str = "", unit=None, decimals=3, deadband=0.0
):
super().__init__()
self.fb = 0.0
self.target = 0
self.beamline = beamline
self.dev = dev
self.motor = motor
self.deadband = deadband
@@ -449,7 +503,7 @@ class MoveWidget(QWidget):
self._set_status(Status.MOVING)
self._apply_button_style("stop")
self._worker = MotionWorker(self.dev, self.motor, target)
self._worker = MotionWorker(self.beamline, self.dev, self.motor, target)
self._thread = QThread()
self._worker.moveToThread(self._thread)
@@ -0,0 +1,59 @@
cm_try:
offset: -0.7
mo1_try:
offset: -31.42
mo1_trx:
modifier:
axis: mo1_trx
range: [[-30, -0.1], [0.1, 30]]
offset: [-4.3, 0]
sl1_centery:
offset: -55.54
bm1_try:
offset: 52.22
fm_trx:
modifier:
axis: fm_trx
range: [[-100, -48], [-47, 0]]
offset: [-0.3, 0.52]
fm_try:
modifier:
axis: fm_trx
range: [[-100, -48], [-47, 0]]
offset: [-42.56, -41.49]
# pitch
fm_rotx:
modifier:
axis: fm_trx
range: [[-100, -48], [-47, 0]]
offset: [1.30, 1.049]
# yaw
fm_roty:
modifier:
axis: fm_trx
range: [[-100, -48], [-47, 0]]
offset: [1.754, 1.924]
bm2_try:
offset: -19
es0wi_try:
offset: -71.98
es1_try:
offset: -113.26
es1ic1_try:
offset: 10.39
es1ic2_try:
offset: 3.55
@@ -67,9 +67,16 @@ sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0
sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0)
# FE slits
slits = namedtuple("slits", ["name", "center", "maxDivH", "maxDivV"])
fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"])
feSlits = slits(name="FE-SLITS", center=(0, 5290, sourceHeight), maxDivH=1.8e-3, maxDivV=0.8e-3)
feSlits = fe_slits(
name="FE-SLITS",
center=(0, 6117, sourceHeight),
center1=(0, 5038.4, sourceHeight),
center2=(0, 5282.9, sourceHeight),
maxDivH=1.8e-3,
maxDivV=0.8e-3,
)
# Filters
filt = namedtuple(
@@ -87,7 +94,7 @@ feWindow = filt(
thickness=0.1,
)
feWindow = feWindow._replace(
surface="CVD Diamond window {0:0.0f} $\mu$m".format(feWindow.thickness * 1e3)
surface="CVD Diamond window {0:0.0f} $\\mu$m".format(feWindow.thickness * 1e3)
)
feFilt = filt(
@@ -100,7 +107,7 @@ feFilt = filt(
material=filterGraphite,
thickness=0.25,
)
feFilt = feFilt._replace(surface="Graphite filter {0:0.0f} $\mu$m".format(feFilt.thickness * 1e3))
feFilt = feFilt._replace(surface="Graphite filter {0:0.0f} $\\mu$m".format(feFilt.thickness * 1e3))
# Collimating mirror
collimatingMirror = namedtuple(
@@ -126,12 +133,12 @@ collimatingMirror = namedtuple(
cm = collimatingMirror(
name="FE-CM",
center=[0, 7618, sourceHeight],
surface=("Si", "Pt", "Rh"),
material=(stripeSi, stripePt, stripeRh),
center=[0, 7560.8, sourceHeight],
surface=("Pt", "Si", "Rh"),
material=(stripePt, stripeSi, stripeRh),
limPhysX=(-30, 30),
limPhysY=(-600, 600),
limOptX=((-21, -8, 5), (-11, 2, 21)),
limOptX=((-21, -0.5, 11), (-4, 9.5, 23)),
limOptY=((-500, -500, -500), (500, 500, 500)),
R=[3e6, 15e6],
pitch=[1.4e-3, 4.5e-3],
@@ -152,10 +159,17 @@ opWbBsBlock = apertures(
name="OP-WB-BS-BLOCK", center=[0.0, 13606 - 135, sourceHeight], opening=[-18.0, 18.0, 42, 76]
) # left, right, bottom, top
opSlits = apertures(
name="OP-SLITS", center=[0, 14145 - 135, sourceHeight], opening=[-35 / 2, 35 / 2, 47.5, 82.5]
opSlits1 = apertures(
name="OP-SLITS 1", center=[0, 14145 - 135, sourceHeight], opening=[-35 / 2, 35 / 2, 47.5, 82.5]
)
# OP Beam Monitors
op_bm = namedtuple("op_bm", ["name", "center"])
opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14525 - 135, sourceHeight))
opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 17161.6 - 135, sourceHeight))
# Monochromator
monochromator = namedtuple(
"monochromator",
@@ -187,7 +201,7 @@ mo1 = monochromator(
material1=(si311_1, si111_1),
material2=(si311_2, si111_2),
xtalWidth=(20, 20),
xtalOffsetX=(-19.2, 19.2),
xtalOffsetX=(19.2, -19.2),
xtalLength1=(60, 60),
xtalLength2=(60, 60),
xtalGap=(8, 8),
@@ -225,19 +239,24 @@ focusingMirror = namedtuple(
],
)
OFFSET_TRX = 46.8735
fm = focusingMirror(
name="OP-FM",
center=[0.0, 15580 - 135, sourceHeight],
surfaceToroid=("Rh (toroid)", "Pt (toroid)"),
surfaceToroid=("Rh", "Pt"),
materialToroid=(stripeRh, stripePt),
limPhysXToroid=(-54.0, 54.0),
limPhysYToroid=(-565.0, 565.0),
limOptXToroid=((4.865, -40.882), (43.388, -4.865)),
limOptXToroid=(
(43.388 + OFFSET_TRX, -4.865 + OFFSET_TRX),
(4.865 + OFFSET_TRX, -40.882 + OFFSET_TRX),
),
limOptYToroid=((-500.0, -500.0), (500.0, 500.0)),
R=[3e6, 15e6],
pitch=[1.4e-3, 4.5e-3],
r=[30, 20],
xToroid=[24.126, -22, 874], # offset in local x
xToroid=[24.126 + OFFSET_TRX, -22 + OFFSET_TRX], # offset in local x
hToroid=[7.0, 11.3], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
jack1=[0.0, 14980.0, 0.0],
jack2=[-75.0, 16180.0, 0.0],
@@ -246,9 +265,12 @@ fm = focusingMirror(
tx2=[0.0, 575.0],
) # X-Stage 2 [x, y]
# Entry wall experimental hutch: 21593 mm from source (SLS2)
# Exit window
ehWindow = filt(
name="EH-WINDOW",
center=(0.0, 22225 - 135, sourceHeight),
center=(0.0, 22063, sourceHeight),
pitch=np.pi / 2,
limPhysX=(-10.0, 10.0),
limPhysY=(17.5, 92.5),
@@ -257,13 +279,18 @@ ehWindow = filt(
thickness=0.25,
)
ehWindow = ehWindow._replace(
surface="Beryllium window {0:0.0f} $\mu$m".format(ehWindow.thickness * 1e3)
surface="Beryllium window {0:0.0f} $\\mu$m".format(ehWindow.thickness * 1e3)
)
# Sample
sample = namedtuple("sample", ["name", "center"])
smpl = sample(name="OP-SMPL", center=[0, 24000 - 135, sourceHeight])
es1 = sample(name="ES1", center=[0, 23823, sourceHeight])
es2 = sample(name="ES2", center=[0, 25843, sourceHeight])
ES1 = sample(name="EH-ES1", center=[0, 24000, sourceHeight])
ES2 = sample(name="EH-ES2", center=[0, 25000, sourceHeight])
# Ionization chambers
ic = namedtuple("sample", ["name", "center"])
es1ic0 = ic(name="ES1 IC0", center=[0, 23633, sourceHeight])
es1ic1 = ic(name="ES1 IC1", center=[0, 24383, sourceHeight])
es1ic2 = ic(name="ES1 IC2", center=[0, 24723, sourceHeight])