Compare commits

...

3 Commits

Author SHA1 Message Date
x01da 5c0bd34641 wip-from-the-beamline
CI for debye_bec / test (push) Successful in 1m9s
2026-05-27 08:21:16 +02:00
x01da a6814eff2b wip
CI for debye_bec / test (push) Successful in 1m11s
2026-05-26 08:57:36 +02:00
x01da 8be85f7a9a wip from beamline, 22.05.2026
CI for debye_bec / test (push) Successful in 1m2s
2026-05-22 10:50:55 +02:00
11 changed files with 465 additions and 39 deletions
@@ -416,3 +416,16 @@ def pipe_geometries() -> list[dict[str, np.ndarray]]:
}
)
return pipes
def table_to_smpl_pos(table: str) -> float:
"""
Return the sample position based on the table name.
Args:
table (str): Table name, e.g. ES1 or ES2
"""
if table in bl.tables:
return bl.tables[table]["smpl"]
raise ValueError(f"Table {table} not found in beamline parameter file")
@@ -2,6 +2,7 @@
Digital Twin: Custom BEC widget to support the beamline alignment.
"""
import socket
import sys
from pathlib import Path
from typing import Literal, cast
@@ -20,6 +21,7 @@ from qtpy.QtCore import Qt, QTimer
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QDialog,
QDialogButtonBox,
QHBoxLayout,
@@ -46,17 +48,17 @@ from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import (
mo1_bragg_angle,
mo1_energy_resolution,
sldi_gap_to_acc,
table_to_smpl_pos,
)
from debye_bec.bec_widgets.widgets.digital_twin.panels.input_panel import InputPanel
from debye_bec.bec_widgets.widgets.digital_twin.panels.mover_panel import MoverPanel
from debye_bec.bec_widgets.widgets.digital_twin.panels.plots import SideviewPlot, SurfacePlots
from debye_bec.bec_widgets.widgets.digital_twin.panels.settings_panel import SettingsPanel
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
from debye_bec.bec_widgets.widgets.digital_twin.types import BeamlineId, ConfigDict
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ComboBox, InputNumberField
logger = bec_logger.logger
OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml"
class DigitalTwin(BECWidget, QWidget):
"""
@@ -70,16 +72,28 @@ class DigitalTwin(BECWidget, QWidget):
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
self.get_bec_shortcuts()
self.beamline = self.get_beamline_id()
# Debugging, override beamline!
# self.beamline = BeamlineId.X10DA
self.offset_fie = None
self.set_offset_file()
# Check if devices are all in config
self.check_config()
self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update())
self.check_bec_config()
self.bec_dispatcher.connect_slot(
self.check_bec_config, MessageEndpoints.device_config_update()
)
logger.info(f"Start Digital Twin with beamline {self.beamline} and all devices available")
central = QWidget()
self.root_layout = QHBoxLayout(central)
self.input_widget = QWidget()
self.input_layout = QVBoxLayout(self.input_widget)
self.input = InputPanel()
self.input = InputPanel(self.beamline)
self.settings = SettingsPanel()
self.input_layout.addWidget(self.input)
self.input_layout.addWidget(self.settings)
@@ -113,7 +127,11 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_rotx.value_changed_connect(self.calc_assistant)
self.input.fm_focx.value_changed_connect(self.calc_assistant)
self.input.fm_focy.value_changed_connect(self.calc_assistant)
self.input.smpl.value_changed_connect(self.calc_assistant)
match self.input.smpl:
case InputNumberField():
self.input.smpl.value_changed_connect(self.calc_assistant)
case ComboBox():
self.input.smpl.activated_connect(self.calc_assistant)
self.input.adapt_reality.clicked_connect(self.adapt_reality)
self.settings.load_offsets.clicked_connect(self.load_offsets)
@@ -144,8 +162,40 @@ class DigitalTwin(BECWidget, QWidget):
self.surface_plots.apply_theme(theme)
self.mover.apply_theme(theme)
def get_beamline_id(self) -> BeamlineId:
"""
Based on the bec servers hostname, tries to extract the beamline
identifier (e.g. x01da, x10da, etc).
Raises:
ValueError if beamline cannot be extracted from hostname or beamline not implemented.
"""
bec_hostname = socket.gethostname()
start = bec_hostname.find("x")
if start != -1:
beamline = bec_hostname[start : start + 5]
match beamline:
case "x01da":
return BeamlineId.X01DA
case "x10da":
return BeamlineId.X10DA
case _:
raise ValueError(f"Not implemented beamline {beamline}")
else:
raise ValueError(f"Failed to extract beamline from bec server hostname {bec_hostname}")
def set_offset_file(self):
"""
Depending on the beamline, set the offset file path accordingly.
"""
files: dict[BeamlineId, str] = {
BeamlineId.X01DA: "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml",
BeamlineId.X10DA: "superxas_bec/superxas_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml",
}
self.offset_file = files[self.beamline]
@SafeSlot()
def check_config(self, *args):
def check_bec_config(self, *args):
"""
Checks the BEC config and opens a window if not all necessary
devices are loaded in the config. If called from a slot from
@@ -161,7 +211,7 @@ class DigitalTwin(BECWidget, QWidget):
"sldi_gapy",
"cm_trx",
"cm_try",
"cm_bnd_radius",
"cm_bnd",
"cm_rotx",
"mo1_bragg",
"mo1_trx",
@@ -171,23 +221,33 @@ class DigitalTwin(BECWidget, QWidget):
"bm1_try",
"fm_trx",
"fm_try",
"fm_bnd_radius",
"fm_bnd",
"fm_rotx",
"fm_roty",
"fm_rotz",
"sl2_centery",
"sl2_gapy",
"bm2_try",
"ot_try",
"ot_rotx",
"es0wi_try",
"ot_es1_trz",
]
if self.beamline == "x01da": # X01DA specific devices
devices.extend(
[
"cm_bnd_radius",
"fm_bnd_radius",
"sl2_centery",
"sl2_gapy",
"ot_try",
"ot_es1_trz",
]
)
if self.beamline == "x10da": # X10DA specific devices
devices.extend(["mo1_rotx", "es1_try", "es1ic0_try", "es1ic1_try", "es1ic2_try"])
while True:
missing = [d for d in devices if d not in self.dev]
if not missing:
break
dialog = QDialog()
dialog = QDialog(self)
dialog.setWindowTitle("Digital Twin - Config Check")
dialog.setFixedWidth(400)
layout = QVBoxLayout()
@@ -327,6 +387,13 @@ class DigitalTwin(BECWidget, QWidget):
assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!"
assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!"
match self.input.smpl:
case InputNumberField():
smpl = self.input.smpl.value()
case ComboBox():
table = self.input.smpl.currentText()
smpl = table_to_smpl_pos(table)
config: ConfigDict = {
"energy": self.input.energy.value(),
"h_acc": self.input.sldi_hacc.value(),
@@ -342,7 +409,7 @@ class DigitalTwin(BECWidget, QWidget):
"fm_trx": fm_trx,
"fm_qy": fm_qy,
"fm_gain_height": 1,
"smpl": self.input.smpl.value(),
"smpl": smpl,
}
# Apply offsets
@@ -508,9 +575,52 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_focus.set_current_text("Manual")
fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"]
self.input.fm_rotx.set_number(fm_rotx_real)
self.input.smpl.set_number(pos["ot_es1_trz"])
match self.input.smpl:
case InputNumberField():
self.input.smpl.set_number(pos["ot_es1_trz"])
case ComboBox():
table = self.ask_table_selection(self.input.smpl.currentText())
self.input.smpl.set_current_text(table)
self.calc_assistant(identifier="init")
def ask_table_selection(self, preset=None) -> str | None:
"""
Opens a dialog asking the user to select a table (ES1 or ES2).
Args:
preset (str): Preset text for the table, either 'ES1' or 'ES2'.
Returns:
The selected table ('ES1' or 'ES2'), or None if the user cancelled.
"""
dialog = QDialog(self)
dialog.setWindowTitle("Select Table")
layout = QVBoxLayout(dialog)
text = QLabel("Select the current table in use.")
combo = QComboBox()
choice = ["ES1", "ES2"]
combo.addItems(choice)
if preset is not None:
if preset in choice:
combo.setCurrentText(preset)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(text)
layout.addWidget(combo)
layout.addWidget(buttons)
if dialog.exec() == QDialog.DialogCode.Accepted:
return combo.currentText()
return None
@SafeSlot()
def load_offsets(self, *_, recalculate: bool = True):
"""
@@ -523,9 +633,9 @@ class DigitalTwin(BECWidget, QWidget):
if self.offsets == {}:
# Load offsets
file = Path(OFFSET_FILE)
file = Path(self.offset_file)
if not file.exists():
raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}")
raise FileNotFoundError(f"Offset file not found: {self.offset_file}")
with file.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
@@ -568,7 +678,7 @@ class DigitalTwin(BECWidget, QWidget):
intro_label.setWordWrap(True)
layout.addWidget(intro_label)
file = QLabel(OFFSET_FILE)
file = QLabel(self.offset_file)
file.setWordWrap(True)
font = QFont()
font.setItalic(True)
@@ -784,7 +894,12 @@ class DigitalTwin(BECWidget, QWidget):
Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText()
)
fm_stripe = self.input.fm_stripe.currentText()
smpl = self.input.smpl.value()
match self.input.smpl:
case InputNumberField():
smpl = self.input.smpl.value()
case ComboBox():
table = self.input.smpl.currentText()
smpl = table_to_smpl_pos(table)
sldi_hacc = self.input.sldi_hacc.value() * 1e-3
sldi_vacc = self.input.sldi_vacc.value() * 1e-3
fm_focx = self.input.fm_focx.value()
@@ -2,9 +2,12 @@
Panel for user inputs of the digital twin widget
"""
from typing import Union
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.types import BeamlineId
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
Button,
ComboBox,
@@ -15,9 +18,14 @@ from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
class InputPanel(QWidget):
"""Panel for user inputs of the digital twin widget"""
"""
Panel for user inputs of the digital twin widget
def __init__(self, parent=None):
Args:
beamline (BeamlineId): Beamline id type
"""
def __init__(self, beamline: BeamlineId, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
@@ -91,9 +99,11 @@ class InputPanel(QWidget):
)
# Focusing Mirror
self.fm_stripe = ComboBox(
"fm_stripe", "Stripe", ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"]
)
stripes: dict[BeamlineId, list[str]] = {
BeamlineId.X01DA: ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"],
BeamlineId.X10DA: ["Rh (toroid)", "Pt (toroid)"],
}
self.fm_stripe = ComboBox("fm_stripe", "Stripe", stripes[beamline])
self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"])
self.fm_rotx = InputNumberField(
"fm_rotx",
@@ -144,18 +154,9 @@ class InputPanel(QWidget):
# Sample
self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0)
self.smpl = InputNumberField(
"smpl",
"Sample Position",
unit="mm",
init=23511,
decimals=0,
single_step=100,
ll=23000,
hl=30000,
)
self.smpl = self._create_smpl(beamline)
# Assemble complete assitant group
# Assemble complete assistant group
self.input_group = Group(
"User Input",
[
@@ -172,3 +173,19 @@ class InputPanel(QWidget):
self._layout.addWidget(self.input_group)
self._layout.addStretch()
def _create_smpl(self, beamline: BeamlineId) -> Union[InputNumberField, ComboBox]:
match beamline:
case BeamlineId.X01DA:
return InputNumberField(
"smpl",
"Sample Position",
unit="mm",
init=23511,
decimals=0,
single_step=100,
ll=23000,
hl=30000,
)
case BeamlineId.X10DA:
return ComboBox("smpl", "Sample Position", ["ES1", "ES2"])
@@ -189,7 +189,7 @@ class MoverPanel(QWidget):
)
self.mover_widgets.append(self.es0wi_try)
self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try])
self.es0_mov_group = Group("Experimental Station 0", [self.es0wi_try])
# Experimental Station 1
self.ot_es1_trz = MoveWidget(
@@ -197,7 +197,7 @@ class MoverPanel(QWidget):
)
self.mover_widgets.append(self.ot_es1_trz)
self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz])
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz])
# Assemble complete mover group
self.mover_group = Group(
@@ -1,8 +1,18 @@
"""Types used for the beamline config and for plotting data"""
from enum import Enum
from typing import TypedDict
class BeamlineId(str, Enum):
"""
Identifier for supported beamlines.
"""
X01DA = "x01da"
X10DA = "x10da"
class ConfigDict(TypedDict):
"""
Typed dictionary representing the beamline configuration.
@@ -304,6 +304,8 @@ smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight])
smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight])
tables = {}
# Vacuum pipes
# DN40CF ID = 35 mm oder 37 mm
# DN50CF ID = 47.5 mm
@@ -0,0 +1,269 @@
"""
X10DA / SuperXAS Beamline Parameters.
This file describes the parameter of each component of the SuperXAS beamline
to be used for raytracing and geometrical calculations.
"""
from collections import namedtuple
import numpy as np
import xrt.backends.raycing.materials as rm
# XRT definitions
filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType]
stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType]
stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType]
stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType]
stripePyrex = rm.Material(
"Si", rho=2.20
) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
filterSi3N4 = rm.Material(
["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate"
) # pyright: ignore[reportArgumentType]
filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType]
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
# General parameters
sourceHeight = 0
# Synchrotron
synchrotron = namedtuple(
"synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"]
)
sls1 = synchrotron(
eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4
)
sls2 = synchrotron(
eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6
)
# Source
bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"])
sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4)
sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1)
sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5)
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"])
feSlits = slits(name="FE-SLITS", center=(0, 5290, sourceHeight), maxDivH=1.8e-3, maxDivV=0.8e-3)
# Filters
filt = namedtuple(
"filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"]
)
feWindow = filt(
name="FE-WINDOW",
center=(0.0, 6158, sourceHeight),
pitch=np.pi / 2,
limPhysX=(-6, 6),
limPhysY=(-3.0, 3.0),
surface="None",
material=filterDiamond,
thickness=0.1,
)
feWindow = feWindow._replace(
surface="CVD Diamond window {0:0.0f} $\mu$m".format(feWindow.thickness * 1e3)
)
feFilt = filt(
name="FE-FI",
center=(0.0, 6590, sourceHeight),
pitch=np.pi / 2,
limPhysX=(-15, 15),
limPhysY=(-10, 10),
surface="None",
material=filterGraphite,
thickness=0.25,
)
feFilt = feFilt._replace(surface="Graphite filter {0:0.0f} $\mu$m".format(feFilt.thickness * 1e3))
# Collimating mirror
collimatingMirror = namedtuple(
"collimatingMirror",
[
"name",
"center",
"surface",
"material",
"limPhysX",
"limPhysY",
"limOptX",
"limOptY",
"R",
"pitch",
"jack1",
"jack2",
"jack3",
"tx1",
"tx2",
],
)
cm = collimatingMirror(
name="FE-CM",
center=[0, 7618, sourceHeight],
surface=("Si", "Pt", "Rh"),
material=(stripeSi, stripePt, stripeRh),
limPhysX=(-30, 30),
limPhysY=(-600, 600),
limOptX=((-21, -8, 5), (-11, 2, 21)),
limOptY=((-500, -500, -500), (500, 500, 500)),
R=[3e6, 15e6],
pitch=[1.4e-3, 4.5e-3],
jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global)
jack2=[-210.0, 8310.0, 0.0],
jack3=[210.0, 8310.0, 0.0],
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
tx2=[0.0, 575],
) # X-Stage 2
apertures = namedtuple("apertures", ["name", "center", "opening"])
fePS = apertures(
name="FE-PS", center=[0, 8760, sourceHeight], opening=[-39 / 2, 39 / 2, -10, 29]
) # left, right, bottom, top
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]
)
# Monochromator
monochromator = namedtuple(
"monochromator",
[
"name",
"center",
"xtal",
"material1",
"material2",
"xtalWidth",
"xtalOffsetX",
"xtalLength1",
"xtalLength2",
"xtalGap",
"rotOffset",
"heightOffset",
"braggLim",
"jack1",
"jack2",
"jack3",
"tx",
],
)
mo1 = monochromator(
name="OP-CCM1",
center=[0.0, 11670 - 135, sourceHeight],
xtal=("Si311", "Si111"),
material1=(si311_1, si111_1),
material2=(si311_2, si111_2),
xtalWidth=(20, 20),
xtalOffsetX=(-19.2, 19.2),
xtalLength1=(60, 60),
xtalLength2=(60, 60),
xtalGap=(8, 8),
rotOffset=6, # not sure what it is
heightOffset=8.5, # not sure what it is
braggLim=[4, 35],
jack1=[0.0, 11350.0, 0.0], # Tripod not available!
jack2=[-400.0, 12350.0, 0.0],
jack3=[400.0, 12350.0, 0.0],
tx=0.0,
) # X-Stage [x]
# Focusing mirror
focusingMirror = namedtuple(
"focusingMirror",
[
"name",
"center",
"surfaceToroid",
"materialToroid",
"limPhysXToroid",
"limPhysYToroid",
"limOptXToroid",
"limOptYToroid",
"R",
"pitch",
"r",
"xToroid",
"hToroid",
"jack1",
"jack2",
"jack3",
"tx1",
"tx2",
],
)
fm = focusingMirror(
name="OP-FM",
center=[0.0, 15580 - 135, sourceHeight],
surfaceToroid=("Rh (toroid)", "Pt (toroid)"),
materialToroid=(stripeRh, stripePt),
limPhysXToroid=(-54.0, 54.0),
limPhysYToroid=(-565.0, 565.0),
limOptXToroid=((4.865, -40.882), (43.388, -4.865)),
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
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],
jack3=[75.0, 16180.0, 0.0],
tx1=[0.0, -575.0], # X-Stage 1 [x, y]
tx2=[0.0, 575.0],
) # X-Stage 2 [x, y]
ehWindow = filt(
name="EH-WINDOW",
center=(0.0, 22225 - 135, sourceHeight),
pitch=np.pi / 2,
limPhysX=(-10.0, 10.0),
limPhysY=(17.5, 92.5),
surface="None",
material=filterBe,
thickness=0.25,
)
ehWindow = ehWindow._replace(
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="EH-ES1", center=[0, 24000, sourceHeight])
ES2 = sample(name="EH-ES2", center=[0, 25000, sourceHeight])