From 6d5adc42053f8fc7f23f86a80b6f52a6e35b4bb6 Mon Sep 17 00:00:00 2001 From: hitz_s Date: Tue, 8 Sep 2026 11:35:40 +0200 Subject: [PATCH] wip --- .../plugins/digital_twin/digital_twin.py | 389 ------ .../__init__.py | 0 .../beamline.py | 0 .../digital_twin_core/digital_twin_core.py | 1119 +++++++++++++++++ .../types.py | 0 .../x01da_offsets.yaml | 0 .../x01da_parameters.py | 0 .../x10da_offsets.yaml | 0 .../x10da_parameters.py | 592 ++++----- .../startup/post_startup.py | 2 +- .../widgets/digital_twin/__init__.py | 3 - .../digital_twin/calculations/__init__.py | 0 .../calculations/calc_sideview.py | 70 -- .../calculations/calc_surfaces.py | 159 --- .../digital_twin/calculations/calc_varia.py | 519 -------- .../widgets/digital_twin/digital_twin.py | 96 +- .../widgets/digital_twin/offsets.py | 83 -- .../digital_twin/panels/input_panel.py | 2 +- .../digital_twin/panels/mover_panel.py | 2 +- .../widgets/digital_twin/panels/plots.py | 30 +- .../bec_widgets/widgets/digital_twin/types.py | 83 -- .../digital_twin/widgets/move_widget.py | 3 +- .../widgets/digital_twin/x01da_offsets.yaml | 50 - .../widgets/digital_twin/x01da_parameters.py | 323 ----- .../widgets/digital_twin/x10da_offsets.yaml | 59 - .../widgets/digital_twin/x10da_parameters.py | 296 ----- .../widgets/scheduler/item_dialog.py | 2 +- .../widgets/scheduler/scheduler.py | 2 +- 28 files changed, 1477 insertions(+), 2407 deletions(-) delete mode 100644 debye_bec/bec_ipython_client/plugins/digital_twin/digital_twin.py rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/__init__.py (100%) rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/beamline.py (100%) create mode 100644 debye_bec/bec_ipython_client/plugins/digital_twin_core/digital_twin_core.py rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/types.py (100%) rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/x01da_offsets.yaml (100%) rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/x01da_parameters.py (100%) rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/x10da_offsets.yaml (100%) rename debye_bec/bec_ipython_client/plugins/{digital_twin => digital_twin_core}/x10da_parameters.py (96%) delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/offsets.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/types.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml delete mode 100644 debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/digital_twin.py b/debye_bec/bec_ipython_client/plugins/digital_twin/digital_twin.py deleted file mode 100644 index 28019d2..0000000 --- a/debye_bec/bec_ipython_client/plugins/digital_twin/digital_twin.py +++ /dev/null @@ -1,389 +0,0 @@ -from pathlib import Path - -import numpy as np -import yaml -from bec_lib import bec_logger -from bec_lib.logger import bec_logger - -from . import parameters as bl -from .beamline import get_beamline_id -from .types import BeamlineId, ConfigDict - -OFFSET_FILE_X01DA = Path(__file__).with_name("x01da_offsets.yaml") -OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml") - -logger = bec_logger.logger - -""" -The idea is to move the core logic of digital_twin to this file. Only keep the gui elements in the widget. -This way, the scheduler widget can access digital twin without loading the GUI (GUI is still needed for the item creation, but not the item execution) - Scheduler will extract assistant inputs (get_assistant_config) during item creation - Scheduler will use digital_twin.calculate_positons to calculate positions and digital_twin.move_all to move the motors -""" - - -class DigitalTwinCore: - - def __init__(self): - logger.info("This is the digital twin from the ipython client!") - self.beamline = get_beamline_id() - self.offset_file = Path() - match self.beamline: - case "x01da": - self.offset_file = OFFSET_FILE_X01DA - case "x10da": - self.offset_file = OFFSET_FILE_X10DA - self.offsets = {} - self.load_offsets() - - def move_with_config(self, config): - positions = self.calc_positions(self.beamline, config) - positions = self.apply_offsets(positions, nested_config=True) - logger.info(f"Would now move to these positions: {positions}") - - def load_offsets(self): - if self.offsets == {}: - logger.info("Load beamline offsets") - if not self.offset_file.exists(): - raise FileNotFoundError(f"Offset file not found: {self.offset_file}") - - with self.offset_file.open("r", encoding="utf-8") as f: - data = yaml.safe_load(f) - - if not isinstance(data, dict): - raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}") - - self.offsets = data - else: - logger.info("Unload beamline offsets") - self.offsets = {} - - def apply_offsets(self, config, nested_config=False): - for axis, axis_data in config.items(): - if axis in self.offsets: - axis_offsets = self.offsets[axis] - if "modifier" in axis_offsets and "offset" in axis_offsets: - for idx, rng in enumerate(axis_offsets["modifier"]["range"]): - modifier_axis = axis_offsets["modifier"]["axis"] - modifier_value = ( - config[modifier_axis]["value"] - if nested_config - else config[modifier_axis] - ) - if rng[0] < modifier_value < rng[1]: - if nested_config: - axis_data["value"] += axis_offsets["offset"][idx] - else: - config[axis] += axis_offsets["offset"][idx] - break - elif "offset" in axis_offsets: - if nested_config: - axis_data["value"] += axis_offsets["offset"] - else: - config[axis] += axis_offsets["offset"] - return config - - def remove_offsets(self, config): - for axis, _ in config.items(): - if axis in self.offsets: - axis_offsets = self.offsets[axis] - if "modifier" in axis_offsets and "offset" in axis_offsets: - for idx, rng in enumerate(axis_offsets["modifier"]["range"]): - if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]: - config[axis] -= axis_offsets["offset"][idx] - break - elif "offset" in axis_offsets: - config[axis] -= axis_offsets["offset"] - return config - - @staticmethod - def calc_positions(beamline: BeamlineId, cfg: ConfigDict) -> dict[str, dict[str, float]]: - """ - Calculates the positions of axes based on a beamline config. - - Args: - cfg(ConfigDict): Dictionary with beamline config - - Returns: - dict[str, dict[str, float]]: Dictionary mapping device names to dictionaries - containing a "value" key with the corresponding float value (position). - """ - - pos = {} - - ## FE slits - trxr = -np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1] - trxw = ( - (np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1]) - / bl.feSlits.center1[1] - * bl.feSlits.center2[1] - ) - tryb = -np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1] - tryt = ( - (np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1]) - / bl.feSlits.center1[1] - * bl.feSlits.center2[1] - ) - - xgap = trxw - trxr - ygap = tryt - tryb - - pos["sldi_gapx"] = {"value": xgap} - pos["sldi_gapy"] = {"value": ygap} - - ## Collimating Mirror - obj_dist = bl.cm.center[1] # object distance - beam_vs = 2 * obj_dist * np.tan(cfg["v_acc"]) # vertical size of beam after CM - - # TRX - if cfg["cm_stripe"] in bl.cm.surface: - index = bl.cm.surface.index(cfg["cm_stripe"]) - else: - raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!") - cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 - pos["cm_trx"] = {"value": cm_trx} - - # TRY - height = obj_dist * np.tan(cfg["v_acc"]) ** 2 * 1 / np.tan(cfg["cm_pitch"]) - pos["cm_try"] = {"value": height} - - # Pitch - pos["cm_rotx"] = { - "value": -cfg["cm_pitch"] * 1e3 - } # invert and convert to mrad (same as EGU of rotx axis) - - # Bending Radius - radius = ( - 2.0 * obj_dist / np.sin(cfg["cm_pitch"]) - ) # Elements of modern X-ray Physics, page 108 ff. - pos["cm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km - - ## Monochromator - if cfg["mo1_mode"] == "Monochromatic": - # Add 2x CM pitch to the bragg angle - bragg = cfg["mo1_bragg"] - elif cfg["mo1_mode"] == "Pinkbeam": - # Align xtal surfaces parallel to beam - bragg = 0 - else: - raise ValueError("Monochromator mode not supported") - pos["mo1_bragg_angle"] = {"value": bragg / np.pi * 180} # Bragg angle in deg - - # TRY, Height - l = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) - yhor = l * np.cos(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver = yhor * np.tan(2.0 * cfg["cm_pitch"]) - - if cfg["mo1_mode"] == "Monochromatic": - beam_offset_mo1 = ( - l * np.sin(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver - ) # Resultat ist korrekt! - elif cfg["mo1_mode"] == "Pinkbeam": - beam_offset_mo1 = 0 - else: - raise ValueError("Monochromator mode not supported") - - def csc(a): - return 1 / np.sin(a) - - def cot(a): - return 1 / np.tan(a) - - # calculate height of center of first crystal surface - f = bl.mo1.rotOffset # rotation offset, mm - d = bl.mo1.heightOffset # xtal height offset, mm - c = d * csc(cfg["mo1_bragg"]) - f * cot(cfg["mo1_bragg"]) - - # Calculate height of center of rotation - b = np.sqrt( - d**2 * csc(cfg["mo1_bragg"]) ** 2 - - 2 * d * f * cot(cfg["mo1_bragg"]) * csc(cfg["mo1_bragg"]) - + f**2 * cot(cfg["mo1_bragg"]) ** 2 - + f**2 - ) - h = np.cos(np.pi / 2 - np.arctan(f / c) - cfg["mo1_bragg"] - 2 * cfg["cm_pitch"]) * b - h2 = ((bl.mo1.center[1] - bl.cm.center[1]) - np.sqrt(b**2 - h**2)) * np.tan( - 2 * cfg["cm_pitch"] - ) - height_mo1_real = ( - h + h2 - ) # per design, the height should not change if the pitch of the CM is not changed! - if cfg["mo1_mode"] == "Monochromatic": - pass - elif cfg["mo1_mode"] == "Pinkbeam": - height_mo1_real = ( - height_mo1_real - 13 - ) # Move down to let beam pass between both crystal without touching copper cooler - else: - raise ValueError("Monochromator mode not supported") - pos["mo1_try"] = {"value": height_mo1_real} - - # TRX, Crystal selection - if cfg["mo1_mode"] == "Monochromatic": - xtal = cfg["mo1_xtal"].translate( - str.maketrans("", "", "()") - ) # Remove brackets from xtal name to conform with parameters - if xtal in bl.mo1.xtal: - index = bl.mo1.xtal.index(xtal) - else: - raise ValueError(f"Requested xtal {xtal} not found in parameters!") - pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]} - else: - pos["mo1_trx"] = {"value": 0} - - diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono - dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) - - ## Slits 1 - 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} - - ## Beam Monitor 1 - d = bl.opBM1.center[1] - bl.cm.center[1] - dz - bm1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - pos["bm1_try"] = {"value": bm1_beam_height} - - ## Focusing Mirror - p = bl.fm.center[1] - q = cfg["smpl"] - bl.fm.center[1] - f = (p * q) / (p + q) # focal length - - # Bender radius - if cfg["fm_qy"] is None: - radius = 2 * q / np.sin(cfg["fm_rotx"]) # ideal bending radius for focused beam - else: - radius = ( - 2 * cfg["fm_qy"] / np.sin(cfg["fm_rotx"]) - ) # ideal bending radius for unfocused beam - pos["fm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km - - # Pitch - d = bl.fm.center[1] - bl.cm.center[1] - dz - fm_rotx = ( - 2 * cfg["cm_pitch"] - cfg["fm_rotx"] - ) # calculate pitch in absolute values (according to horizontal plane) - pos["fm_rotx"] = { - "value": -fm_rotx * 1e3 - } # invert and convert to mrad (same as EGU of rotx axis) - - if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): - - # TRY - if cfg["fm_stripe"] == "Rh (toroid)": - r = bl.fm.r[0] - h_cyl = bl.fm.hToroid[0] - else: # PT toroid - r = bl.fm.r[1] - h_cyl = bl.fm.hToroid[1] - width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"] * 1e-3) - alpha = np.arccos(1 - width_beam**2 / (2 * r**2)) - h = r - (r * np.cos(alpha / 2)) - fm_beam_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg[ - "fm_gain_height" - ] - fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - h_cyl + h / 2) * cfg[ - "fm_gain_height" - ] - pos["fm_try"] = {"value": fm_height} - - # TRX - if cfg["fm_stripe"] == "Rh (toroid)": - x_cyl = -bl.fm.xToroid[0] - else: - x_cyl = -bl.fm.xToroid[1] - pos["fm_trx"] = {"value": x_cyl} - - elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"): - - # TRY - fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"] - fm_beam_height = fm_height - pos["fm_try"] = {"value": fm_height} - - # TRX - if cfg["fm_stripe"] == "Rh (flat)": - x_flat = -bl.fm.xFlat[0] - else: - x_flat = -bl.fm.xFlat[1] - pos["fm_trx"] = {"value": x_flat} - - else: - raise ValueError("FM Stripe selection not valid") - - pos["fm_roty"] = {"value": 0} - pos["fm_rotz"] = {"value": 0} - - ## Slits 2 - 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] - bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - pos["bm2_try"] = {"value": bm2_beam_height} - - ## Optical Table - - 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} - - # 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) - 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 diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/__init__.py b/debye_bec/bec_ipython_client/plugins/digital_twin_core/__init__.py similarity index 100% rename from debye_bec/bec_ipython_client/plugins/digital_twin/__init__.py rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/__init__.py diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/beamline.py b/debye_bec/bec_ipython_client/plugins/digital_twin_core/beamline.py similarity index 100% rename from debye_bec/bec_ipython_client/plugins/digital_twin/beamline.py rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/beamline.py diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin_core/digital_twin_core.py b/debye_bec/bec_ipython_client/plugins/digital_twin_core/digital_twin_core.py new file mode 100644 index 0000000..8acc1b5 --- /dev/null +++ b/debye_bec/bec_ipython_client/plugins/digital_twin_core/digital_twin_core.py @@ -0,0 +1,1119 @@ +import re +from pathlib import Path +from typing import Literal, cast + +import numpy as np +import yaml +from bec_lib import bec_logger +from scipy.interpolate import UnivariateSpline +from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm + +from . import parameters as bl +from .beamline import get_beamline_id +from .types import BeamlineId, ConfigDict, DataDict, SurfaceDict + +H = 6.62606957e-34 +E = 1.602176634e-19 +C = 299792458 +RE = 2.8179e-15 + +OFFSET_FILE_X01DA = Path(__file__).with_name("x01da_offsets.yaml") +OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml") + +logger = bec_logger.logger + +""" +The idea is to move the core logic of digital_twin to this file. Only keep the gui elements in the widget. +This way, the scheduler widget can access digital twin without loading the GUI (GUI is still needed for the item creation, but not the item execution) + Scheduler will extract assistant inputs (get_assistant_config) during item creation + Scheduler will use digital_twin.calculate_positons to calculate positions and digital_twin.move_all to move the motors +""" + + +class DigitalTwinCore: + + def __init__(self): + logger.info("This is the digital twin from the ipython client!") + self.beamline = get_beamline_id() + self.offset_file = Path() + match self.beamline: + case "x01da": + self.offset_file = OFFSET_FILE_X01DA + case "x10da": + self.offset_file = OFFSET_FILE_X10DA + self.offsets = {} + self.load_offsets() + + def move_with_config(self, config): + positions = self.calc_positions(self.beamline, config) + positions = self.apply_offsets(positions, nested_config=True) + logger.info(f"Would now move to these positions: {positions}") + + def load_offsets(self): + if self.offsets == {}: + logger.info("Load beamline offsets") + if not self.offset_file.exists(): + raise FileNotFoundError(f"Offset file not found: {self.offset_file}") + + with self.offset_file.open("r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + if not isinstance(data, dict): + raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}") + + self.offsets = data + else: + logger.info("Unload beamline offsets") + self.offsets = {} + + def apply_offsets(self, config, nested_config=False): + for axis, axis_data in config.items(): + if axis in self.offsets: + axis_offsets = self.offsets[axis] + if "modifier" in axis_offsets and "offset" in axis_offsets: + for idx, rng in enumerate(axis_offsets["modifier"]["range"]): + modifier_axis = axis_offsets["modifier"]["axis"] + modifier_value = ( + config[modifier_axis]["value"] + if nested_config + else config[modifier_axis] + ) + if rng[0] < modifier_value < rng[1]: + if nested_config: + axis_data["value"] += axis_offsets["offset"][idx] + else: + config[axis] += axis_offsets["offset"][idx] + break + elif "offset" in axis_offsets: + if nested_config: + axis_data["value"] += axis_offsets["offset"] + else: + config[axis] += axis_offsets["offset"] + return config + + def remove_offsets(self, config): + for axis, _ in config.items(): + if axis in self.offsets: + axis_offsets = self.offsets[axis] + if "modifier" in axis_offsets and "offset" in axis_offsets: + for idx, rng in enumerate(axis_offsets["modifier"]["range"]): + if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]: + config[axis] -= axis_offsets["offset"][idx] + break + elif "offset" in axis_offsets: + config[axis] -= axis_offsets["offset"] + return config + + @staticmethod + def calc_positions(beamline: BeamlineId, cfg: ConfigDict) -> dict[str, dict[str, float]]: + """ + Calculates the positions of axes based on a beamline config. + + Args: + cfg(ConfigDict): Dictionary with beamline config + + Returns: + dict[str, dict[str, float]]: Dictionary mapping device names to dictionaries + containing a "value" key with the corresponding float value (position). + """ + + pos = {} + + ## FE slits + trxr = -np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1] + trxw = ( + (np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1]) + / bl.feSlits.center1[1] + * bl.feSlits.center2[1] + ) + tryb = -np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1] + tryt = ( + (np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1]) + / bl.feSlits.center1[1] + * bl.feSlits.center2[1] + ) + + xgap = trxw - trxr + ygap = tryt - tryb + + pos["sldi_gapx"] = {"value": xgap} + pos["sldi_gapy"] = {"value": ygap} + + ## Collimating Mirror + obj_dist = bl.cm.center[1] # object distance + beam_vs = 2 * obj_dist * np.tan(cfg["v_acc"]) # vertical size of beam after CM + + # TRX + if cfg["cm_stripe"] in bl.cm.surface: + index = bl.cm.surface.index(cfg["cm_stripe"]) + else: + raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!") + cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2 + pos["cm_trx"] = {"value": cm_trx} + + # TRY + height = obj_dist * np.tan(cfg["v_acc"]) ** 2 * 1 / np.tan(cfg["cm_pitch"]) + pos["cm_try"] = {"value": height} + + # Pitch + pos["cm_rotx"] = { + "value": -cfg["cm_pitch"] * 1e3 + } # invert and convert to mrad (same as EGU of rotx axis) + + # Bending Radius + radius = ( + 2.0 * obj_dist / np.sin(cfg["cm_pitch"]) + ) # Elements of modern X-ray Physics, page 108 ff. + pos["cm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km + + ## Monochromator + if cfg["mo1_mode"] == "Monochromatic": + # Add 2x CM pitch to the bragg angle + bragg = cfg["mo1_bragg"] + elif cfg["mo1_mode"] == "Pinkbeam": + # Align xtal surfaces parallel to beam + bragg = 0 + else: + raise ValueError("Monochromator mode not supported") + pos["mo1_bragg_angle"] = {"value": bragg / np.pi * 180} # Bragg angle in deg + + # TRY, Height + l = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) + yhor = l * np.cos(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) + yver = yhor * np.tan(2.0 * cfg["cm_pitch"]) + + if cfg["mo1_mode"] == "Monochromatic": + beam_offset_mo1 = ( + l * np.sin(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver + ) # Resultat ist korrekt! + elif cfg["mo1_mode"] == "Pinkbeam": + beam_offset_mo1 = 0 + else: + raise ValueError("Monochromator mode not supported") + + def csc(a): + return 1 / np.sin(a) + + def cot(a): + return 1 / np.tan(a) + + # calculate height of center of first crystal surface + f = bl.mo1.rotOffset # rotation offset, mm + d = bl.mo1.heightOffset # xtal height offset, mm + c = d * csc(cfg["mo1_bragg"]) - f * cot(cfg["mo1_bragg"]) + + # Calculate height of center of rotation + b = np.sqrt( + d**2 * csc(cfg["mo1_bragg"]) ** 2 + - 2 * d * f * cot(cfg["mo1_bragg"]) * csc(cfg["mo1_bragg"]) + + f**2 * cot(cfg["mo1_bragg"]) ** 2 + + f**2 + ) + h = np.cos(np.pi / 2 - np.arctan(f / c) - cfg["mo1_bragg"] - 2 * cfg["cm_pitch"]) * b + h2 = ((bl.mo1.center[1] - bl.cm.center[1]) - np.sqrt(b**2 - h**2)) * np.tan( + 2 * cfg["cm_pitch"] + ) + height_mo1_real = ( + h + h2 + ) # per design, the height should not change if the pitch of the CM is not changed! + if cfg["mo1_mode"] == "Monochromatic": + pass + elif cfg["mo1_mode"] == "Pinkbeam": + height_mo1_real = ( + height_mo1_real - 13 + ) # Move down to let beam pass between both crystal without touching copper cooler + else: + raise ValueError("Monochromator mode not supported") + pos["mo1_try"] = {"value": height_mo1_real} + + # TRX, Crystal selection + if cfg["mo1_mode"] == "Monochromatic": + xtal = cfg["mo1_xtal"].translate( + str.maketrans("", "", "()") + ) # Remove brackets from xtal name to conform with parameters + if xtal in bl.mo1.xtal: + index = bl.mo1.xtal.index(xtal) + else: + raise ValueError(f"Requested xtal {xtal} not found in parameters!") + pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]} + else: + pos["mo1_trx"] = {"value": 0} + + diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono + dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) + + ## Slits 1 + 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} + + ## Beam Monitor 1 + d = bl.opBM1.center[1] - bl.cm.center[1] - dz + bm1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 + pos["bm1_try"] = {"value": bm1_beam_height} + + ## Focusing Mirror + p = bl.fm.center[1] + q = cfg["smpl"] - bl.fm.center[1] + f = (p * q) / (p + q) # focal length + + # Bender radius + if cfg["fm_qy"] is None: + radius = 2 * q / np.sin(cfg["fm_rotx"]) # ideal bending radius for focused beam + else: + radius = ( + 2 * cfg["fm_qy"] / np.sin(cfg["fm_rotx"]) + ) # ideal bending radius for unfocused beam + pos["fm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km + + # Pitch + d = bl.fm.center[1] - bl.cm.center[1] - dz + fm_rotx = ( + 2 * cfg["cm_pitch"] - cfg["fm_rotx"] + ) # calculate pitch in absolute values (according to horizontal plane) + pos["fm_rotx"] = { + "value": -fm_rotx * 1e3 + } # invert and convert to mrad (same as EGU of rotx axis) + + if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): + + # TRY + if cfg["fm_stripe"] == "Rh (toroid)": + r = bl.fm.r[0] + h_cyl = bl.fm.hToroid[0] + else: # PT toroid + r = bl.fm.r[1] + h_cyl = bl.fm.hToroid[1] + width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"] * 1e-3) + alpha = np.arccos(1 - width_beam**2 / (2 * r**2)) + h = r - (r * np.cos(alpha / 2)) + fm_beam_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg[ + "fm_gain_height" + ] + fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - h_cyl + h / 2) * cfg[ + "fm_gain_height" + ] + pos["fm_try"] = {"value": fm_height} + + # TRX + if cfg["fm_stripe"] == "Rh (toroid)": + x_cyl = -bl.fm.xToroid[0] + else: + x_cyl = -bl.fm.xToroid[1] + pos["fm_trx"] = {"value": x_cyl} + + elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"): + + # TRY + fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"] + fm_beam_height = fm_height + pos["fm_try"] = {"value": fm_height} + + # TRX + if cfg["fm_stripe"] == "Rh (flat)": + x_flat = -bl.fm.xFlat[0] + else: + x_flat = -bl.fm.xFlat[1] + pos["fm_trx"] = {"value": x_flat} + + else: + raise ValueError("FM Stripe selection not valid") + + pos["fm_roty"] = {"value": 0} + pos["fm_rotz"] = {"value": 0} + + ## Slits 2 + 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] + bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) + pos["bm2_try"] = {"value": bm2_beam_height} + + ## Optical Table + + 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} + + # 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) + 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 + + @staticmethod + def sldi_gap_to_acc(sldi_gapx: float, sldi_gapy: float) -> tuple[float, float]: + """ + Calculate the slits acceptance based on the gap values + + Args: + sldi_gapx(float): GAPX value of the slits in mm + sldi_gapy(float): GAPY value of the slits in mm + + Returns: + tuple[float, float]: Horizontal and vertical acceptance in rad + """ + d1 = bl.feSlits.center1[1] + d2 = bl.feSlits.center2[1] + h_acc = np.tan(sldi_gapx / (d2 + d1)) + v_acc = np.tan(sldi_gapy / (d2 + d1)) + return h_acc, v_acc + + @staticmethod + def cm_trx_to_stripe(cm_trx: float) -> str | None: + """ + Based on the trx value of the collimating mirror, return + the correct stripe + + Args: + cm_trx(float): Collimating mirror trx value + + Returns + str | None: Stripe of the mirror, None if not found + """ + cm_stripe = None + for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): + if low <= cm_trx <= high: + cm_stripe = name + return cm_stripe + + @staticmethod + def cm_stripe_to_trx(cm_stripe: str) -> float | None: + """ + Based on the stripe of the collimating mirror, return + the trx value + + Args: + cm_stripe(str): Stripe of the collimating mirror + + Returns: + float | None: TRX value of the stripe. None if not found + """ + for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): + if cm_stripe == name: + return -(low + high) / 2 + return None + + @staticmethod + def fm_trx_to_stripe(fm_trx: float) -> str | None: + """ + Based on the trx value of the focusing mirror, return + the correct stripe + + Args: + fm_trx(float): focusing mirror trx value + + Returns + str | None: Stripe of the mirror, None if not found + """ + fm_stripe = None + 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)" + return fm_stripe + + @staticmethod + def fm_stripe_to_trx(fm_stripe: str) -> float | None: + """ + Based on the stripe of the focusing mirror, return + the trx value + + Args: + fm_stripe(str): Stripe of the focusing mirror + + Returns: + float | None: TRX value of the stripe. None if not found + """ + 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 + return None + + @staticmethod + def mo1_energy_resolution(xtal: Literal["Si111", "Si311"], energy: float) -> float: + """ + Calculate the energy resolution of the monochromator + + Args: + xtal(str): Xtal name. "Si111" or "Si311" + energy(float): Energy in eV + + Returns: + float: Energy resolution in eV + """ + index = bl.mo1.xtal.index(xtal) + crystal = bl.mo1.material1[index] + + dtheta = np.linspace(-30, 90, 601) + theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6 + refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0]) ** 2 # single crystal + + refl2 = refl**2 # DCM with parallel crystals + + # FWHM of the DCM curve + spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0) + roots = cast(np.ndarray, spline.roots()) + r1, r2 = float(roots[0]), float(roots[1]) + fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad + + # Energy resolution + theta_b = crystal.get_Bragg_angle(energy) + de_over_e = fwhm_rad / np.tan(theta_b) + de = de_over_e * energy + + # logger.info(f"DCM FWHM : {r2-r1:.2f} µrad") + # logger.info(f"ΔE/E : {dE_over_E:.2e}") + # logger.info(f"ΔE : {dE:.3f} eV at {E} eV") + + return de + + @staticmethod + def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float: + """ + Calculate the reflectivity of the mirror stripe based + on the pitch and energy. + + Args: + cm_stripe(str): Mirror stripe + cm_pitch(float): Pitch of the mirror (beam incidence angle) + energy(float): Energy of the beam in eV + + 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 + return refl + + @staticmethod + def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float: + """ + Calculate the reflectivity of the mirror stripe based + on the pitch and energy. + + Args: + cm_stripe(str): Mirror stripe + cm_pitch(float): Pitch of the mirror (beam incidence angle) + energy(float): Energy of the beam in eV + + 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 + stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() + index = surface.index(stripe) + else: + surface = bl.fm.surfaceFlat + material = bl.fm.materialFlat + stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() + index = surface.index(stripe) + rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2] + refl = abs(rs) ** 2 + return refl + + @staticmethod + def mo1_bragg_angle( + mo_mode: Literal["Monochromatic", "Pinkbeam"], + d_spacing: float, + energy: float, + cm_pitch: float, + ) -> tuple[float, float]: + """ + Calculate the bragg angle of the monochromator. + Corrects for the collimating mirror pitch. + + Args: + mo_mode(str): Monochromator mode. "Monochromatic" or "Pinkbeam" + d_spacing(float): D-spacing of the crystal in Angstrom + energy(float): Energy of the beam in eV + cm_pitch(float): Pitch of collimating mirror in rad + + Returns: + tuple[float, float]: Bragg angle and corrected bragg angle + """ + wl = C * H / (E * energy) + val = wl / (2 * d_spacing * 1e-10) + bragg_angle = 0 + if val > -1 and val < 1: + bragg_angle = np.asin(val) + if mo_mode == "Monochromatic": + # Add 2x CM pitch to the bragg angle + bragg_angle_cor = (2 * cm_pitch) + bragg_angle + else: + # Align xtal surfaces parallel to beam + bragg_angle_cor = 2 * cm_pitch + return bragg_angle, bragg_angle_cor + + @staticmethod + def fm_ideal_pitch( + fm_focus: Literal["Defocused", "Focused", "Manual"], + fm_stripe: str, + smpl: float, + sldi_hacc: float | None = None, + sldi_vacc: float | None = None, + fm_focx: float | None = None, + fm_focy: float | None = None, + ) -> tuple[float, float | None]: + """ + Calculates the ideal pitch for the focusing mirror depending on the + focusing strategy. + If "Defocused" is chosed, sldi_hacc, sldi_vacc, fm_focx and fm_focy + must be provided. + + Args: + fm_focus(str): Focus strategy. "Defocused", "Focused" or "Manual + fm_stripe(str): Mirror stripe + smpl(float): Sample position in mm from source + sldi_hacc(float): Horizontal acceptance of frontend slits. Defaults to None + sldi_vacc(float): Vertical acceptance of frontend slits. Defaults to None + fm_focx(float): Requested horizontal spot size in mm. Defaults to None + fm_focy(float): Requested vertical spot size in mm. Defaults to None + + Returns: + tuple[float, float | None]: Pitch of mirror in rad, qy in mm + """ + + # logger.info("Calculate pitch and qy now...") + # logger.info(f"sldi_hacc: {sldi_hacc}") + # logger.info(f"sldi_vacc: {sldi_vacc}") + # logger.info(f"fm_stripe: {fm_stripe}") + # logger.info(f"smpl: {smpl}") + p_cm = bl.cm.center[1] # posCM + p = bl.fm.center[1] # posFM + q = smpl - bl.fm.center[1] # dist posFM to posEX + if fm_focus == "Defocused": + assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode" + assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode" + assert fm_focx is not None, "fm_focx must be provided for Defocused mode" + assert fm_focy is not None, "fm_focy must be provided for Defocused mode" + a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror + # logger.info(f"a: {a}") + # logger.info(f"sldi_hacc: {sldi_hacc}") + # logger.info(f"bl.fm.center[1]: {bl.fm.center[1]}") + # logger.info(f"p: {p}") + # logger.info(f"q: {q}") + b = ( + 2 * np.tan(sldi_vacc) * bl.cm.center[1] + ) # Beam height at focusing mirror (collimated beam) + x = fm_focx + # logger.info(f"x: {x}") + x = 0.098821 * x**2 + 0.512344 * x # polynom to correct for spot size + # logger.info(f"x (corrected): {x}") + y = fm_focy + y = 3.183562 * y**2 + 1.258364 * y # polynom to correct for spot size + qx = q + x * p / a + qy = q + y * p_cm / b + f = (p * qx) / (p + qx) # focal length + # logger.info(f"qx: {qx}") + # logger.info(f"f: {f}") + else: # Calculate for focused beam on sample in "manual" and "focused" mode + qy = None + f = (p * q) / (p + q) # focal length + pitch = 0 + if "Rh" in fm_stripe: + pitch = np.arcsin(bl.fm.r[0] / (2 * f)) # ideal pitch for FM + if "Pt" in fm_stripe: + pitch = np.arcsin(bl.fm.r[1] / (2 * f)) # ideal pitch for FM + # logger.info(f"fm_pitch: {pitch}") + # logger.info(f"qy: {qy}") + return pitch, qy + + @staticmethod + def calc_beamsize( + sldi_hacc: float, + sldi_vacc: float, + fm_stripe: str, + fm_pitch: float, + fm_radius: float, + smpl: float, + ) -> tuple[float, float | None]: + """ + Calculate the resulting beamsize according to the input parameters + + Args: + sldi_hacc(float): Horizontal acceptance of frontend slits + sldi_vacc(float): Vertical acceptance of frontend slits + fm_stripe(str): Mirror stripe + fm_pitch(float): Focusing mirror pitch in rad + fm_radius(float): Focusing mirror bender radius in m + smpl(float): Sample position in mm from source + + Returns: + tuple[float, float | None]: horizontal spot size, vertical spot size, both in mm + """ + + # logger.info("Calculate beamsize now...") + # logger.info(f"sldi_hacc: {sldi_hacc}") + # logger.info(f"sldi_vacc: {sldi_vacc}") + # logger.info(f"fm_stripe: {fm_stripe}") + # logger.info(f"fm_pitch: {fm_pitch}") + # logger.info(f"fm_radius: {fm_radius}") + # logger.info(f"smpl: {smpl}") + p_cm = bl.cm.center[1] # posCM + p = bl.fm.center[1] # posFM + q = smpl - bl.fm.center[1] # dist posFM to posEX + qy = fm_radius * np.sin(fm_pitch) / 2 + a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror + b = ( + 2 * np.tan(sldi_vacc) * bl.cm.center[1] + ) # Beam height at focusing mirror (collimated beam) + f = 0 + if "Rh" in fm_stripe: + f = bl.fm.r[0] / (2 * np.sin(fm_pitch)) + if "Pt" in fm_stripe: + f = bl.fm.r[1] / (2 * np.sin(fm_pitch)) + qx = p * f / (p - f) + x = a * (qx - q) / p + y = b * (qy - q) / p_cm + # Change this | to a plus if calculation is not correct + fm_focx = -4 * (64043 - 125000 * np.sqrt(0.26249637 + 0.395284 * x)) / 98821 + # Change this | to a plus if calculation is not correct + fm_focy = -1 * (314591 - 250000 * np.sqrt(1.58347995 + 12.734248 * y)) / 1591781 + # logger.info(f"f: {f}") + # logger.info(f"qx: {qx}") + # logger.info(f"qy: {qy}") + # logger.info(f"fm_focx: {fm_focx}") + # logger.info(f"fm_focy: {fm_focy}") + return fm_focx, fm_focy + + @staticmethod + def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float: + """ + Calculate the critical angle of the mirror stripe + + Args: + cm_stripe(str): Mirror stripe. "Si", "Pt" or "Rh" + energy(float): Energy in eV + + Returns: + float: Critical angle in rad + """ + if cm_stripe == "Si": + stripe = bl.stripeSi + elif cm_stripe == "Pt": + stripe = bl.stripePt + else: + stripe = bl.stripeRh + w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m] + f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy)) + number_density = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3) + critical_angle = np.sqrt(number_density * RE * w**2 * f1 / np.pi) + return critical_angle + + @staticmethod + def mirror_surface_geometries( + mirror: Literal["cm", "fm_toroid", "fm_flat"], + ) -> dict[str, tuple[float, float, float, float]]: + """ + Return the mirror stripe geometries + + Args: + mirror(str): Mirror. "cm", "fm_toroid" or "fm_flat" + + Returns: + dict[str, tuple[float, float, float, float]]: Dictionary mapping surface + names to tuples of (x, y, width, height). + """ + if mirror == "cm": + surface = bl.cm.surface + lim_opt_x = bl.cm.limOptX + lim_opt_y = bl.cm.limOptY + elif mirror == "fm_toroid": + surface = bl.fm.surfaceToroid + lim_opt_x = bl.fm.limOptXToroid + lim_opt_y = bl.fm.limOptYToroid + elif mirror == "fm_flat": + surface = bl.fm.surfaceFlat + lim_opt_x = bl.fm.limOptXFlat + lim_opt_y = bl.fm.limOptYFlat + else: + raise ValueError(f"Requested mirror {mirror} not available!") + geom = {} + for sf, lx, hx, ly, hy in zip( + surface, lim_opt_x[0], lim_opt_x[1], lim_opt_y[0], lim_opt_y[1] + ): + geom[sf] = (lx, ly, hx - lx, hy - ly) + return geom + + @staticmethod + def mo_surface_geometries( + mo: Literal["mo1"], plane: Literal[0, 1] + ) -> dict[str, tuple[float, float, float, float]]: + """ + Return the monochromator xtal geometries + + Args: + mo(str): Monochromator. Only "mo1" implemented + plane(int): Surface of xtal. 0 and 1 (First and second) + + Returns: + dict[str, tuple[float, float, float, float]]: Dictionary mapping surface + names to tuples of (x, y, width, height). + """ + if mo == "mo1": + xtal = bl.mo1.xtal + xtal_width = bl.mo1.xtalWidth + xtal_offset_x = bl.mo1.xtalOffsetX + if plane == 0: + xtal_length = bl.mo1.xtalLength1 + else: + xtal_length = bl.mo1.xtalLength2 + else: + return {} + geom = {} + for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length): + geom[sf] = (offx - w / 2, -length / 2, w, length) + return geom + + @staticmethod + def wall_geometries() -> list[list[float]]: + """ + Return the wall geometries + + Returns: + 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( + [ + bl.walls.start[i], + bl.walls.height[i][0], + bl.walls.end[i] - bl.walls.start[i], + bl.walls.height[i][1] - bl.walls.height[i][0], + ] + ) + return geom + + @staticmethod + def pipe_geometries() -> list[dict[str, np.ndarray]]: + """ + Return the wall geometries + + Returns: + list[dict[str, np.ndarray]]: List of dictionaries with keys "x" and "y", + each containing a numpy array of two float values representing + 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 + pipes.append( + { + "x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), + "y": np.array([top, top]), + } + ) + pipes.append( + { + "x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), + "y": np.array([bottom, bottom]), + } + ) + return pipes + + @staticmethod + 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 == 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") + + @staticmethod + def calc_sideview(cfg: ConfigDict) -> DataDict: + """ + Calculates the sideview coordinates based on a beamline config. + + Args: + cfg(ConfigDict): Dictionary with beamline config + + Returns: + DataDict: Sideview data + """ + + beam: DataDict = {"x": [], "y": []} + + beam["x"] = [] + beam["y"] = [] + beam["x"].append(0) # Source + beam["y"].append(bl.sourceHeight) + beam["x"].append(bl.cm.center[1]) # CM + beam["y"].append(bl.sourceHeight) + if cfg["mo1_mode"] == "Monochromatic": + diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono + dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) + dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) + beam["x"].append(bl.mo1.center[1] - dz / 2) # Mono 1.1 + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1]) + ) + beam["x"].append(bl.mo1.center[1] + dz / 2) # Mono 1.2 + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1]) + + dy + ) + beam["x"].append(bl.fm.center[1]) # FM + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz) + + dy + ) + beam["x"].append(cfg["smpl"]) # Experiment + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz) + + dy + + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) + ) + elif cfg["mo1_mode"] == "Pinkbeam": + beam["x"].append(bl.fm.center[1]) # FM + beam["y"].append( + bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1]) + ) + beam["x"].append(cfg["smpl"]) # Experiment + beam["y"].append( + bl.sourceHeight + + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1]) + + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) + ) + + return beam + + @staticmethod + def calc_surfaces(cfg: ConfigDict) -> SurfaceDict: + """ + Calculates the surface coordinates based on a beamline config. + + Args: + cfg(ConfigDict): Dictionary with beamline config + + Returns: + SurfaceDict: Surface data + """ + + out: SurfaceDict = { + "cm": {"x": [], "y": []}, + "mo1_1": {"x": [], "y": []}, + "mo1_2": {"x": [], "y": []}, + "fm": {"x": [], "y": []}, + } + + # Collimating mirror + l = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) / np.sin(cfg["cm_pitch"]) + + 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"]) + + cen = -cfg["cm_trx"] + + out["cm"]["x"] = [cen - w1 / 2, cen - w2 / 2, cen + w2 / 2, cen + w1 / 2] + out["cm"]["y"] = [-l / 2, l / 2, l / 2, -l / 2] + + # Monochromator + # calculate height of center of first crystal surface + c = bl.mo1.heightOffset * 1 / np.sin(cfg["mo1_bragg"]) - bl.mo1.rotOffset * 1 / np.tan( + cfg["mo1_bragg"] + ) + e = bl.mo1.xtalGap[0] / np.tan(cfg["mo1_bragg"]) - c + + xtal = cfg["mo1_xtal"].translate( + str.maketrans("", "", "()") + ) # Remove brackets from xtal name to conform with parameters + index = bl.mo1.xtal.index(xtal) + + xtal_pos = bl.mo1.xtalOffsetX[index] + xtal_length_1 = bl.mo1.xtalLength1[index] + xtal_length_2 = bl.mo1.xtalLength2[index] + + width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"]) + + height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) + w = height_beam / np.sin(cfg["mo1_bragg"]) + + if cfg["mo1_mode"] == "Monochromatic": + out["mo1_1"]["x"] = [ + xtal_pos - width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos - width_beam / 2, + ] + out["mo1_1"]["y"] = [ + xtal_length_1 / 2 - c - w / 2, + xtal_length_1 / 2 - c - w / 2, + xtal_length_1 / 2 - c + w / 2, + xtal_length_1 / 2 - c + w / 2, + ] + out["mo1_2"]["x"] = [ + xtal_pos - width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos + width_beam / 2, + xtal_pos - width_beam / 2, + ] + out["mo1_2"]["y"] = [ + -xtal_length_2 / 2 + e - w / 2, + -xtal_length_2 / 2 + e - w / 2, + -xtal_length_2 / 2 + e + w / 2, + -xtal_length_2 / 2 + e + w / 2, + ] + else: # Pinkbeam + out["mo1_1"]["x"] = [] + out["mo1_1"]["y"] = [] + 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 + stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip() + index = surface.index(stripe) + r = bl.fm.r[index] + else: + surface = bl.fm.surfaceFlat + stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip() + index = surface.index(stripe) + r = bl.fm.r[index] + off = -cfg["fm_trx"] + + width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"]) + + if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): + + l = height_beam / np.sin(cfg["fm_rotx"]) + alpha = np.arccos(1 - width_beam**2 / (2 * r**2)) + h = r - (r * np.cos(alpha / 2)) + z = h / np.tan(cfg["fm_rotx"]) + + x = [off - width_beam / 2, off - width_beam / 2] + y = [l / 2 - z / 2, -l / 2 - z / 2] + + res = 20 + x_elipse = np.linspace(0, np.pi, res) + y_elipse = np.linspace(0, np.pi, res) + x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse] + y_elipse = [width_beam * np.sin(i) * z / width_beam - l / 2 - z / 2 for i in y_elipse] + + x.extend(x_elipse) + y.extend(y_elipse) + + x.extend([off + width_beam / 2, off + width_beam / 2]) + y.extend([-l / 2 - z / 2, l / 2 - z / 2]) + + res = 50 + x_elipse = np.linspace(np.pi, 0, res) + y_elipse = np.linspace(np.pi, 0, res) + x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse] + y_elipse = [width_beam * np.sin(i) * z / width_beam + l / 2 - z / 2 for i in y_elipse] + + x.extend(x_elipse) + y.extend(y_elipse) + + out["fm"]["x"] = x + out["fm"]["y"] = y + + else: # flat surface, no toroid + l = height_beam / np.sin(cfg["fm_rotx"]) + + w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"]) + w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"]) + + out["fm"]["x"] = [off - w1 / 2, off + w1 / 2, off + w2 / 2, off - w2 / 2] + out["fm"]["y"] = [-l / 2, -l / 2, l / 2, l / 2] + + return out diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/types.py b/debye_bec/bec_ipython_client/plugins/digital_twin_core/types.py similarity index 100% rename from debye_bec/bec_ipython_client/plugins/digital_twin/types.py rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/types.py diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/x01da_offsets.yaml b/debye_bec/bec_ipython_client/plugins/digital_twin_core/x01da_offsets.yaml similarity index 100% rename from debye_bec/bec_ipython_client/plugins/digital_twin/x01da_offsets.yaml rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/x01da_offsets.yaml diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/x01da_parameters.py b/debye_bec/bec_ipython_client/plugins/digital_twin_core/x01da_parameters.py similarity index 100% rename from debye_bec/bec_ipython_client/plugins/digital_twin/x01da_parameters.py rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/x01da_parameters.py diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/x10da_offsets.yaml b/debye_bec/bec_ipython_client/plugins/digital_twin_core/x10da_offsets.yaml similarity index 100% rename from debye_bec/bec_ipython_client/plugins/digital_twin/x10da_offsets.yaml rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/x10da_offsets.yaml diff --git a/debye_bec/bec_ipython_client/plugins/digital_twin/x10da_parameters.py b/debye_bec/bec_ipython_client/plugins/digital_twin_core/x10da_parameters.py similarity index 96% rename from debye_bec/bec_ipython_client/plugins/digital_twin/x10da_parameters.py rename to debye_bec/bec_ipython_client/plugins/digital_twin_core/x10da_parameters.py index b7b40a5..c0bd4e0 100644 --- a/debye_bec/bec_ipython_client/plugins/digital_twin/x10da_parameters.py +++ b/debye_bec/bec_ipython_client/plugins/digital_twin_core/x10da_parameters.py @@ -1,296 +1,296 @@ -""" -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 -fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"]) - -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( - "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, 7560.8, sourceHeight], - surface=("Pt", "Si", "Rh"), - material=(stripePt, stripeSi, stripeRh), - limPhysX=(-30, 30), - limPhysY=(-600, 600), - 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], - 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 - -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", - [ - "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", - ], -) - -OFFSET_TRX = 46.8735 - -fm = focusingMirror( - name="OP-FM", - center=[0.0, 15580 - 135, sourceHeight], - surfaceToroid=("Rh", "Pt"), - materialToroid=(stripeRh, stripePt), - limPhysXToroid=(-54.0, 54.0), - limPhysYToroid=(-565.0, 565.0), - 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 + 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], - 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] - -# Entry wall experimental hutch: 21593 mm from source (SLS2) - -# Exit window -ehWindow = filt( - name="EH-WINDOW", - center=(0.0, 22063, 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"]) - -es1 = sample(name="ES1", center=[0, 23823, sourceHeight]) -es2 = sample(name="ES2", center=[0, 25843, 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]) +""" +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 +fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"]) + +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( + "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, 7560.8, sourceHeight], + surface=("Pt", "Si", "Rh"), + material=(stripePt, stripeSi, stripeRh), + limPhysX=(-30, 30), + limPhysY=(-600, 600), + 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], + 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 + +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", + [ + "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", + ], +) + +OFFSET_TRX = 46.8735 + +fm = focusingMirror( + name="OP-FM", + center=[0.0, 15580 - 135, sourceHeight], + surfaceToroid=("Rh", "Pt"), + materialToroid=(stripeRh, stripePt), + limPhysXToroid=(-54.0, 54.0), + limPhysYToroid=(-565.0, 565.0), + 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 + 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], + 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] + +# Entry wall experimental hutch: 21593 mm from source (SLS2) + +# Exit window +ehWindow = filt( + name="EH-WINDOW", + center=(0.0, 22063, 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"]) + +es1 = sample(name="ES1", center=[0, 23823, sourceHeight]) +es2 = sample(name="ES2", center=[0, 25843, 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]) diff --git a/debye_bec/bec_ipython_client/startup/post_startup.py b/debye_bec/bec_ipython_client/startup/post_startup.py index d1778bb..0fd9d82 100644 --- a/debye_bec/bec_ipython_client/startup/post_startup.py +++ b/debye_bec/bec_ipython_client/startup/post_startup.py @@ -41,7 +41,7 @@ logger = bec_logger.logger logger.info("Using the Debye startup script.") -from debye_bec.bec_ipython_client.plugins.digital_twin.digital_twin import DigitalTwinCore +from debye_bec.bec_ipython_client.plugins.digital_twin_core.digital_twin_core import DigitalTwinCore from debye_bec.bec_ipython_client.plugins.auto_gain import AutoGain digital_twin = DigitalTwinCore() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/__init__.py index a42cb09..e69de29 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/__init__.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/__init__.py @@ -1,3 +0,0 @@ -from .beamline import get_parameters - -parameters = get_parameters() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py deleted file mode 100644 index 135c76d..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Calculates the sideview coordinates based on a beamline config. -""" - -import numpy as np - -from .. import parameters as bl -from ..types import ConfigDict, DataDict - - -def calc_sideview(cfg: ConfigDict) -> DataDict: - """ - Calculates the sideview coordinates based on a beamline config. - - Args: - cfg(ConfigDict): Dictionary with beamline config - - Returns: - DataDict: Sideview data - """ - - beam: DataDict = {"x": [], "y": []} - - beam["x"] = [] - beam["y"] = [] - beam["x"].append(0) # Source - beam["y"].append(bl.sourceHeight) - beam["x"].append(bl.cm.center[1]) # CM - beam["y"].append(bl.sourceHeight) - if cfg["mo1_mode"] == "Monochromatic": - diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono - dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) - dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) - beam["x"].append(bl.mo1.center[1] - dz / 2) # Mono 1.1 - beam["y"].append( - bl.sourceHeight - + np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1]) - ) - beam["x"].append(bl.mo1.center[1] + dz / 2) # Mono 1.2 - beam["y"].append( - bl.sourceHeight - + np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1]) - + dy - ) - beam["x"].append(bl.fm.center[1]) # FM - beam["y"].append( - bl.sourceHeight - + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz) - + dy - ) - beam["x"].append(cfg["smpl"]) # Experiment - beam["y"].append( - bl.sourceHeight - + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz) - + dy - + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) - ) - elif cfg["mo1_mode"] == "Pinkbeam": - beam["x"].append(bl.fm.center[1]) # FM - beam["y"].append( - bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1]) - ) - beam["x"].append(cfg["smpl"]) # Experiment - beam["y"].append( - bl.sourceHeight - + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1]) - + np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1]) - ) - - return beam diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py deleted file mode 100644 index 2ef4a59..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Calculates the surface coordinates based on a beamline config. -""" - -import re - -import numpy as np -from bec_lib import bec_logger - -from .. import parameters as bl -from ..types import ConfigDict, SurfaceDict - -logger = bec_logger.logger - - -def calc_surfaces(cfg: ConfigDict) -> SurfaceDict: - """ - Calculates the surface coordinates based on a beamline config. - - Args: - cfg(ConfigDict): Dictionary with beamline config - - Returns: - SurfaceDict: Surface data - """ - - out: SurfaceDict = { - "cm": {"x": [], "y": []}, - "mo1_1": {"x": [], "y": []}, - "mo1_2": {"x": [], "y": []}, - "fm": {"x": [], "y": []}, - } - - # Collimating mirror - l = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) / np.sin(cfg["cm_pitch"]) - - 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"]) - - cen = -cfg["cm_trx"] - - out["cm"]["x"] = [cen - w1 / 2, cen - w2 / 2, cen + w2 / 2, cen + w1 / 2] - out["cm"]["y"] = [-l / 2, l / 2, l / 2, -l / 2] - - # Monochromator - # calculate height of center of first crystal surface - c = bl.mo1.heightOffset * 1 / np.sin(cfg["mo1_bragg"]) - bl.mo1.rotOffset * 1 / np.tan( - cfg["mo1_bragg"] - ) - e = bl.mo1.xtalGap[0] / np.tan(cfg["mo1_bragg"]) - c - - xtal = cfg["mo1_xtal"].translate( - str.maketrans("", "", "()") - ) # Remove brackets from xtal name to conform with parameters - index = bl.mo1.xtal.index(xtal) - - xtal_pos = bl.mo1.xtalOffsetX[index] - xtal_length_1 = bl.mo1.xtalLength1[index] - xtal_length_2 = bl.mo1.xtalLength2[index] - - width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"]) - - height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) - w = height_beam / np.sin(cfg["mo1_bragg"]) - - if cfg["mo1_mode"] == "Monochromatic": - out["mo1_1"]["x"] = [ - xtal_pos - width_beam / 2, - xtal_pos + width_beam / 2, - xtal_pos + width_beam / 2, - xtal_pos - width_beam / 2, - ] - out["mo1_1"]["y"] = [ - xtal_length_1 / 2 - c - w / 2, - xtal_length_1 / 2 - c - w / 2, - xtal_length_1 / 2 - c + w / 2, - xtal_length_1 / 2 - c + w / 2, - ] - out["mo1_2"]["x"] = [ - xtal_pos - width_beam / 2, - xtal_pos + width_beam / 2, - xtal_pos + width_beam / 2, - xtal_pos - width_beam / 2, - ] - out["mo1_2"]["y"] = [ - -xtal_length_2 / 2 + e - w / 2, - -xtal_length_2 / 2 + e - w / 2, - -xtal_length_2 / 2 + e + w / 2, - -xtal_length_2 / 2 + e + w / 2, - ] - else: # Pinkbeam - out["mo1_1"]["x"] = [] - out["mo1_1"]["y"] = [] - 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 - stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip() - index = surface.index(stripe) - r = bl.fm.r[index] - else: - surface = bl.fm.surfaceFlat - stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip() - index = surface.index(stripe) - r = bl.fm.r[index] - off = -cfg["fm_trx"] - - width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"]) - - if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): - - l = height_beam / np.sin(cfg["fm_rotx"]) - alpha = np.arccos(1 - width_beam**2 / (2 * r**2)) - h = r - (r * np.cos(alpha / 2)) - z = h / np.tan(cfg["fm_rotx"]) - - x = [off - width_beam / 2, off - width_beam / 2] - y = [l / 2 - z / 2, -l / 2 - z / 2] - - res = 20 - x_elipse = np.linspace(0, np.pi, res) - y_elipse = np.linspace(0, np.pi, res) - x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse] - y_elipse = [width_beam * np.sin(i) * z / width_beam - l / 2 - z / 2 for i in y_elipse] - - x.extend(x_elipse) - y.extend(y_elipse) - - x.extend([off + width_beam / 2, off + width_beam / 2]) - y.extend([-l / 2 - z / 2, l / 2 - z / 2]) - - res = 50 - x_elipse = np.linspace(np.pi, 0, res) - y_elipse = np.linspace(np.pi, 0, res) - x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse] - y_elipse = [width_beam * np.sin(i) * z / width_beam + l / 2 - z / 2 for i in y_elipse] - - x.extend(x_elipse) - y.extend(y_elipse) - - out["fm"]["x"] = x - out["fm"]["y"] = y - - else: # flat surface, no toroid - l = height_beam / np.sin(cfg["fm_rotx"]) - - w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"]) - w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"]) - - out["fm"]["x"] = [off - w1 / 2, off + w1 / 2, off + w2 / 2, off - w2 / 2] - out["fm"]["y"] = [-l / 2, -l / 2, l / 2, l / 2] - - return out diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py deleted file mode 100644 index 5c80afb..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py +++ /dev/null @@ -1,519 +0,0 @@ -""" -Various calculations for the digital twin -""" - -import re -from typing import Literal, cast - -import numpy as np -from bec_lib import bec_logger -from scipy.interpolate import UnivariateSpline -from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm - -from .. import parameters as bl - -logger = bec_logger.logger - -H = 6.62606957e-34 -E = 1.602176634e-19 -C = 299792458 -RE = 2.8179e-15 - - -def sldi_gap_to_acc(sldi_gapx: float, sldi_gapy: float) -> tuple[float, float]: - """ - Calculate the slits acceptance based on the gap values - - Args: - sldi_gapx(float): GAPX value of the slits in mm - sldi_gapy(float): GAPY value of the slits in mm - - Returns: - tuple[float, float]: Horizontal and vertical acceptance in rad - """ - d1 = bl.feSlits.center1[1] - d2 = bl.feSlits.center2[1] - h_acc = np.tan(sldi_gapx / (d2 + d1)) - v_acc = np.tan(sldi_gapy / (d2 + d1)) - return h_acc, v_acc - - -def cm_trx_to_stripe(cm_trx: float) -> str | None: - """ - Based on the trx value of the collimating mirror, return - the correct stripe - - Args: - cm_trx(float): Collimating mirror trx value - - Returns - str | None: Stripe of the mirror, None if not found - """ - cm_stripe = None - for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): - if low <= cm_trx <= high: - cm_stripe = name - return cm_stripe - - -def cm_stripe_to_trx(cm_stripe: str) -> float | None: - """ - Based on the stripe of the collimating mirror, return - the trx value - - Args: - cm_stripe(str): Stripe of the collimating mirror - - Returns: - float | None: TRX value of the stripe. None if not found - """ - for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]): - if cm_stripe == name: - return -(low + high) / 2 - return None - - -def fm_trx_to_stripe(fm_trx: float) -> str | None: - """ - Based on the trx value of the focusing mirror, return - the correct stripe - - Args: - fm_trx(float): focusing mirror trx value - - Returns - str | None: Stripe of the mirror, None if not found - """ - fm_stripe = None - 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)" - return fm_stripe - - -def fm_stripe_to_trx(fm_stripe: str) -> float | None: - """ - Based on the stripe of the focusing mirror, return - the trx value - - Args: - fm_stripe(str): Stripe of the focusing mirror - - Returns: - float | None: TRX value of the stripe. None if not found - """ - 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 - return None - - -def mo1_energy_resolution(xtal: Literal["Si111", "Si311"], energy: float) -> float: - """ - Calculate the energy resolution of the monochromator - - Args: - xtal(str): Xtal name. "Si111" or "Si311" - energy(float): Energy in eV - - Returns: - float: Energy resolution in eV - """ - index = bl.mo1.xtal.index(xtal) - crystal = bl.mo1.material1[index] - - dtheta = np.linspace(-30, 90, 601) - theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6 - refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0]) ** 2 # single crystal - - refl2 = refl**2 # DCM with parallel crystals - - # FWHM of the DCM curve - spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0) - roots = cast(np.ndarray, spline.roots()) - r1, r2 = float(roots[0]), float(roots[1]) - fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad - - # Energy resolution - theta_b = crystal.get_Bragg_angle(energy) - de_over_e = fwhm_rad / np.tan(theta_b) - de = de_over_e * energy - - # logger.info(f"DCM FWHM : {r2-r1:.2f} µrad") - # logger.info(f"ΔE/E : {dE_over_E:.2e}") - # logger.info(f"ΔE : {dE:.3f} eV at {E} eV") - - return de - - -def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float: - """ - Calculate the reflectivity of the mirror stripe based - on the pitch and energy. - - Args: - cm_stripe(str): Mirror stripe - cm_pitch(float): Pitch of the mirror (beam incidence angle) - energy(float): Energy of the beam in eV - - 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 - return refl - - -def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float: - """ - Calculate the reflectivity of the mirror stripe based - on the pitch and energy. - - Args: - cm_stripe(str): Mirror stripe - cm_pitch(float): Pitch of the mirror (beam incidence angle) - energy(float): Energy of the beam in eV - - 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 - stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() - index = surface.index(stripe) - else: - surface = bl.fm.surfaceFlat - material = bl.fm.materialFlat - stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip() - index = surface.index(stripe) - rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2] - refl = abs(rs) ** 2 - return refl - - -def mo1_bragg_angle( - mo_mode: Literal["Monochromatic", "Pinkbeam"], d_spacing: float, energy: float, cm_pitch: float -) -> tuple[float, float]: - """ - Calculate the bragg angle of the monochromator. - Corrects for the collimating mirror pitch. - - Args: - mo_mode(str): Monochromator mode. "Monochromatic" or "Pinkbeam" - d_spacing(float): D-spacing of the crystal in Angstrom - energy(float): Energy of the beam in eV - cm_pitch(float): Pitch of collimating mirror in rad - - Returns: - tuple[float, float]: Bragg angle and corrected bragg angle - """ - wl = C * H / (E * energy) - val = wl / (2 * d_spacing * 1e-10) - bragg_angle = 0 - if val > -1 and val < 1: - bragg_angle = np.asin(val) - if mo_mode == "Monochromatic": - # Add 2x CM pitch to the bragg angle - bragg_angle_cor = (2 * cm_pitch) + bragg_angle - else: - # Align xtal surfaces parallel to beam - bragg_angle_cor = 2 * cm_pitch - return bragg_angle, bragg_angle_cor - - -def fm_ideal_pitch( - fm_focus: Literal["Defocused", "Focused", "Manual"], - fm_stripe: str, - smpl: float, - sldi_hacc: float | None = None, - sldi_vacc: float | None = None, - fm_focx: float | None = None, - fm_focy: float | None = None, -) -> tuple[float, float | None]: - """ - Calculates the ideal pitch for the focusing mirror depending on the - focusing strategy. - If "Defocused" is chosed, sldi_hacc, sldi_vacc, fm_focx and fm_focy - must be provided. - - Args: - fm_focus(str): Focus strategy. "Defocused", "Focused" or "Manual - fm_stripe(str): Mirror stripe - smpl(float): Sample position in mm from source - sldi_hacc(float): Horizontal acceptance of frontend slits. Defaults to None - sldi_vacc(float): Vertical acceptance of frontend slits. Defaults to None - fm_focx(float): Requested horizontal spot size in mm. Defaults to None - fm_focy(float): Requested vertical spot size in mm. Defaults to None - - Returns: - tuple[float, float | None]: Pitch of mirror in rad, qy in mm - """ - - # logger.info("Calculate pitch and qy now...") - # logger.info(f"sldi_hacc: {sldi_hacc}") - # logger.info(f"sldi_vacc: {sldi_vacc}") - # logger.info(f"fm_stripe: {fm_stripe}") - # logger.info(f"smpl: {smpl}") - p_cm = bl.cm.center[1] # posCM - p = bl.fm.center[1] # posFM - q = smpl - bl.fm.center[1] # dist posFM to posEX - if fm_focus == "Defocused": - assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode" - assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode" - assert fm_focx is not None, "fm_focx must be provided for Defocused mode" - assert fm_focy is not None, "fm_focy must be provided for Defocused mode" - a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror - # logger.info(f"a: {a}") - # logger.info(f"sldi_hacc: {sldi_hacc}") - # logger.info(f"bl.fm.center[1]: {bl.fm.center[1]}") - # logger.info(f"p: {p}") - # logger.info(f"q: {q}") - b = ( - 2 * np.tan(sldi_vacc) * bl.cm.center[1] - ) # Beam height at focusing mirror (collimated beam) - x = fm_focx - # logger.info(f"x: {x}") - x = 0.098821 * x**2 + 0.512344 * x # polynom to correct for spot size - # logger.info(f"x (corrected): {x}") - y = fm_focy - y = 3.183562 * y**2 + 1.258364 * y # polynom to correct for spot size - qx = q + x * p / a - qy = q + y * p_cm / b - f = (p * qx) / (p + qx) # focal length - # logger.info(f"qx: {qx}") - # logger.info(f"f: {f}") - else: # Calculate for focused beam on sample in "manual" and "focused" mode - qy = None - f = (p * q) / (p + q) # focal length - pitch = 0 - if "Rh" in fm_stripe: - pitch = np.arcsin(bl.fm.r[0] / (2 * f)) # ideal pitch for FM - if "Pt" in fm_stripe: - pitch = np.arcsin(bl.fm.r[1] / (2 * f)) # ideal pitch for FM - # logger.info(f"fm_pitch: {pitch}") - # logger.info(f"qy: {qy}") - return pitch, qy - - -def calc_beamsize( - sldi_hacc: float, - sldi_vacc: float, - fm_stripe: str, - fm_pitch: float, - fm_radius: float, - smpl: float, -) -> tuple[float, float | None]: - """ - Calculate the resulting beamsize according to the input parameters - - Args: - sldi_hacc(float): Horizontal acceptance of frontend slits - sldi_vacc(float): Vertical acceptance of frontend slits - fm_stripe(str): Mirror stripe - fm_pitch(float): Focusing mirror pitch in rad - fm_radius(float): Focusing mirror bender radius in m - smpl(float): Sample position in mm from source - - Returns: - tuple[float, float | None]: horizontal spot size, vertical spot size, both in mm - """ - - # logger.info("Calculate beamsize now...") - # logger.info(f"sldi_hacc: {sldi_hacc}") - # logger.info(f"sldi_vacc: {sldi_vacc}") - # logger.info(f"fm_stripe: {fm_stripe}") - # logger.info(f"fm_pitch: {fm_pitch}") - # logger.info(f"fm_radius: {fm_radius}") - # logger.info(f"smpl: {smpl}") - p_cm = bl.cm.center[1] # posCM - p = bl.fm.center[1] # posFM - q = smpl - bl.fm.center[1] # dist posFM to posEX - qy = fm_radius * np.sin(fm_pitch) / 2 - a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror - b = 2 * np.tan(sldi_vacc) * bl.cm.center[1] # Beam height at focusing mirror (collimated beam) - f = 0 - if "Rh" in fm_stripe: - f = bl.fm.r[0] / (2 * np.sin(fm_pitch)) - if "Pt" in fm_stripe: - f = bl.fm.r[1] / (2 * np.sin(fm_pitch)) - qx = p * f / (p - f) - x = a * (qx - q) / p - y = b * (qy - q) / p_cm - # Change this | to a plus if calculation is not correct - fm_focx = -4 * (64043 - 125000 * np.sqrt(0.26249637 + 0.395284 * x)) / 98821 - # Change this | to a plus if calculation is not correct - fm_focy = -1 * (314591 - 250000 * np.sqrt(1.58347995 + 12.734248 * y)) / 1591781 - # logger.info(f"f: {f}") - # logger.info(f"qx: {qx}") - # logger.info(f"qy: {qy}") - # logger.info(f"fm_focx: {fm_focx}") - # logger.info(f"fm_focy: {fm_focy}") - return fm_focx, fm_focy - - -def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float: - """ - Calculate the critical angle of the mirror stripe - - Args: - cm_stripe(str): Mirror stripe. "Si", "Pt" or "Rh" - energy(float): Energy in eV - - Returns: - float: Critical angle in rad - """ - if cm_stripe == "Si": - stripe = bl.stripeSi - elif cm_stripe == "Pt": - stripe = bl.stripePt - else: - stripe = bl.stripeRh - w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m] - f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy)) - number_density = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3) - critical_angle = np.sqrt(number_density * RE * w**2 * f1 / np.pi) - return critical_angle - - -def mirror_surface_geometries( - mirror: Literal["cm", "fm_toroid", "fm_flat"], -) -> dict[str, tuple[float, float, float, float]]: - """ - Return the mirror stripe geometries - - Args: - mirror(str): Mirror. "cm", "fm_toroid" or "fm_flat" - - Returns: - dict[str, tuple[float, float, float, float]]: Dictionary mapping surface - names to tuples of (x, y, width, height). - """ - if mirror == "cm": - surface = bl.cm.surface - lim_opt_x = bl.cm.limOptX - lim_opt_y = bl.cm.limOptY - elif mirror == "fm_toroid": - surface = bl.fm.surfaceToroid - lim_opt_x = bl.fm.limOptXToroid - lim_opt_y = bl.fm.limOptYToroid - elif mirror == "fm_flat": - surface = bl.fm.surfaceFlat - lim_opt_x = bl.fm.limOptXFlat - lim_opt_y = bl.fm.limOptYFlat - else: - raise ValueError(f"Requested mirror {mirror} not available!") - geom = {} - for sf, lx, hx, ly, hy in zip(surface, lim_opt_x[0], lim_opt_x[1], lim_opt_y[0], lim_opt_y[1]): - geom[sf] = (lx, ly, hx - lx, hy - ly) - return geom - - -def mo_surface_geometries( - mo: Literal["mo1"], plane: Literal[0, 1] -) -> dict[str, tuple[float, float, float, float]]: - """ - Return the monochromator xtal geometries - - Args: - mo(str): Monochromator. Only "mo1" implemented - plane(int): Surface of xtal. 0 and 1 (First and second) - - Returns: - dict[str, tuple[float, float, float, float]]: Dictionary mapping surface - names to tuples of (x, y, width, height). - """ - if mo == "mo1": - xtal = bl.mo1.xtal - xtal_width = bl.mo1.xtalWidth - xtal_offset_x = bl.mo1.xtalOffsetX - if plane == 0: - xtal_length = bl.mo1.xtalLength1 - else: - xtal_length = bl.mo1.xtalLength2 - else: - return {} - geom = {} - for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length): - geom[sf] = (offx - w / 2, -length / 2, w, length) - return geom - - -def wall_geometries() -> list[list[float]]: - """ - Return the wall geometries - - Returns: - 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( - [ - bl.walls.start[i], - bl.walls.height[i][0], - bl.walls.end[i] - bl.walls.start[i], - bl.walls.height[i][1] - bl.walls.height[i][0], - ] - ) - return geom - - -def pipe_geometries() -> list[dict[str, np.ndarray]]: - """ - Return the wall geometries - - Returns: - list[dict[str, np.ndarray]]: List of dictionaries with keys "x" and "y", - each containing a numpy array of two float values representing - 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 - pipes.append( - { - "x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), - "y": np.array([top, top]), - } - ) - pipes.append( - { - "x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]), - "y": np.array([bottom, bottom]), - } - ) - 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 == 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") diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py index 3fb7ca7..2a01e02 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -34,32 +34,14 @@ from qtpy.QtWidgets import ( QWidget, ) -from ....bec_ipython_client.plugins.digital_twin.digital_twin import DigitalTwinCore +from ....bec_ipython_client.plugins.digital_twin_core.beamline import get_beamline_id +from ....bec_ipython_client.plugins.digital_twin_core.digital_twin_core import DigitalTwinCore +from ....bec_ipython_client.plugins.digital_twin_core.types import ConfigDict from ..edge_selector import EdgeSelector -from .beamline import get_beamline_id -from .calculations.calc_sideview import calc_sideview -from .calculations.calc_surfaces import calc_surfaces -from .calculations.calc_varia import ( - calc_beamsize, - cm_critical_angle, - cm_reflectivity, - cm_stripe_to_trx, - cm_trx_to_stripe, - fm_ideal_pitch, - fm_reflectivity, - fm_stripe_to_trx, - fm_trx_to_stripe, - mo1_bragg_angle, - mo1_energy_resolution, - sldi_gap_to_acc, - table_to_smpl_pos, -) -from .offsets import Offsets from .panels.input_panel import InputPanel from .panels.mover_panel import MoverPanel from .panels.plots import SideviewPlot, SurfacePlots from .panels.settings_panel import SettingsPanel -from .types import ConfigDict from .widgets.qt_widgets import ComboBox, InputNumberField logger = bec_logger.logger @@ -88,8 +70,6 @@ class DigitalTwin(BECWidget, QWidget): # Debugging, override beamline! # self.beamline = BeamlineId.X10DA - self.offsets = Offsets() - # Check if devices are all in config self.check_bec_config() self.bec_dispatcher.connect_slot( @@ -117,8 +97,8 @@ class DigitalTwin(BECWidget, QWidget): self.plot_layout = QVBoxLayout(self.plot_widget) self.plot_layout.setContentsMargins(4, 4, 4, 4) self.plot_layout.setSpacing(6) - self.sideview_plot = SideviewPlot() - self.surface_plots = SurfacePlots(self.beamline) + self.sideview_plot = SideviewPlot(self.core) + self.surface_plots = SurfacePlots(self.beamline, self.core) 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) @@ -393,9 +373,9 @@ class DigitalTwin(BECWidget, QWidget): fm_qy = self.qy cm_stripe = self.input.cm_stripe.currentText() - cm_trx = cm_stripe_to_trx(cm_stripe) + cm_trx = self.core.cm_stripe_to_trx(cm_stripe) fm_stripe = self.input.fm_stripe.currentText() - fm_trx = fm_stripe_to_trx(fm_stripe) + fm_trx = self.core.fm_stripe_to_trx(fm_stripe) 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}!" @@ -405,7 +385,7 @@ class DigitalTwin(BECWidget, QWidget): smpl = self.input.smpl.value() case ComboBox(): table = self.input.smpl.currentText() - smpl = table_to_smpl_pos(table) + smpl = self.core.table_to_smpl_pos(table) config: ConfigDict = { "energy": self.input.energy.value(), @@ -427,7 +407,7 @@ class DigitalTwin(BECWidget, QWidget): # Apply offsets if apply_offset: - config = self.offsets.apply_offsets(config, nested_config=False) + config = self.core.apply_offsets(config, nested_config=False) # Convert to SI units! config["h_acc"] *= 1e-3 @@ -453,12 +433,12 @@ class DigitalTwin(BECWidget, QWidget): mo1_bragg = self.dev.mo1_bragg.read(cached=True) sldi_gapx = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"] sldi_gapy = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"] - h_acc, v_acc = sldi_gap_to_acc(sldi_gapx, sldi_gapy) + h_acc, v_acc = self.core.sldi_gap_to_acc(sldi_gapx, sldi_gapy) cm_trx = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"] - cm_stripe = cm_trx_to_stripe(-cm_trx) + cm_stripe = self.core.cm_trx_to_stripe(-cm_trx) cm_pitch = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] fm_trx = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"] - fm_stripe = fm_trx_to_stripe(-fm_trx) + fm_stripe = self.core.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 @@ -467,7 +447,7 @@ class DigitalTwin(BECWidget, QWidget): 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) + smpl = self.core.table_to_smpl_pos(table) raw = { # Config in SI units! "energy": mo1_bragg["mo1_bragg"]["value"], @@ -576,13 +556,13 @@ class DigitalTwin(BECWidget, QWidget): pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"] # Removing offsets - pos = self.offsets.remove_offsets(pos) + pos = self.core.remove_offsets(pos) self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)["mo1_bragg"]["value"]) - h_acc, v_acc = sldi_gap_to_acc(pos["sldi_gapx"], pos["sldi_gapy"]) + h_acc, v_acc = self.core.sldi_gap_to_acc(pos["sldi_gapx"], pos["sldi_gapy"]) self.input.sldi_hacc.set_number(h_acc * 1e3) self.input.sldi_vacc.set_number(v_acc * 1e3) - self.input.cm_stripe.set_current_text(cm_trx_to_stripe(-pos["cm_trx"])) + self.input.cm_stripe.set_current_text(self.core.cm_trx_to_stripe(-pos["cm_trx"])) self.input.cm_pitch.set_number(pos["cm_rotx"]) if abs(pos["mo1_trx"]) > 5: mo1_mode = "Monochromatic" @@ -592,7 +572,7 @@ class DigitalTwin(BECWidget, QWidget): self.input.mo1_xtal.set_current_text( self.dev.mo1_bragg.read(cached=True)["mo1_bragg_crystal_current_xtal_string"]["value"] ) - fm_stripe = fm_trx_to_stripe(-pos["fm_trx"]) + fm_stripe = self.core.fm_trx_to_stripe(-pos["fm_trx"]) self.input.fm_stripe.set_current_text(fm_stripe) fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"] self.input.fm_rotx.set_number(fm_rotx_real) @@ -603,10 +583,10 @@ class DigitalTwin(BECWidget, QWidget): self.input.smpl.set_number(pos["ot_es1_trz"]) case ComboBox(): table = self.ask_table_selection(self.input.smpl.currentText()) - smpl = table_to_smpl_pos(table) + smpl = self.core.table_to_smpl_pos(table) self.input.smpl.set_current_text(table) - fm_focx, fm_focy = calc_beamsize( + fm_focx, fm_focy = self.core.calc_beamsize( h_acc, v_acc, fm_stripe, -fm_rotx_real * 1e-3, pos["fm_bnd_radius"] * 1e6, smpl ) if fm_focx < 0.08 and fm_focy < 0.08: @@ -665,8 +645,8 @@ class DigitalTwin(BECWidget, QWidget): Defaults to True """ - self.offsets.load_offsets() - if self.offsets.offsets != {}: + self.core.load_offsets() + if self.core.offsets != {}: # Offsets were loaded if recalculate: self.calc_assistant(identifier="init") @@ -700,7 +680,7 @@ class DigitalTwin(BECWidget, QWidget): intro_label.setWordWrap(True) layout.addWidget(intro_label) - file = QLabel(str(self.offsets.offset_file)) + file = QLabel(str(self.core.offset_file)) file.setWordWrap(True) font = QFont() font.setItalic(True) @@ -718,7 +698,7 @@ class DigitalTwin(BECWidget, QWidget): return super().represent_sequence(tag, sequence, flow_style=True) text_edit.setPlainText( - yaml.dump(self.offsets.offsets, Dumper=InlineListDumper, sort_keys=False) + yaml.dump(self.core.offsets, Dumper=InlineListDumper, sort_keys=False) ) layout.addWidget(text_edit) @@ -759,9 +739,9 @@ class DigitalTwin(BECWidget, QWidget): Updates the plots for the reality scene """ config = self.get_reality_config() - data = calc_sideview(config) + data = self.core.calc_sideview(config) self.sideview_plot.update_curves("reality", data=data) - surfaces = calc_surfaces(config) + surfaces = self.core.calc_surfaces(config) self.surface_plots.update_surfaces(scene="reality", data=surfaces) @SafeSlot() @@ -791,7 +771,7 @@ class DigitalTwin(BECWidget, QWidget): ) # Remove brackets from xtal name to conform with parameters xtal = cast(Literal["Si111", "Si311"], xtal) energy = self.input.energy.value() - self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy)) + self.input.mo1_eres.setValue(self.core.mo1_energy_resolution(xtal, energy)) def calc_cm_reflectivity(self): """ @@ -800,9 +780,11 @@ class DigitalTwin(BECWidget, QWidget): cm_stripe = self.input.cm_stripe.currentText() cm_pitch = -self.input.cm_pitch.value() * 1e-3 energy = self.input.energy.value() - self.input.cm_refl.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, energy)) + self.input.cm_refl.setValue(100 * self.core.cm_reflectivity(cm_stripe, cm_pitch, energy)) self.input.cm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV") - self.input.cm_refl_harm.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, 3 * energy)) + self.input.cm_refl_harm.setValue( + 100 * self.core.cm_reflectivity(cm_stripe, cm_pitch, 3 * energy) + ) self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") def calc_fm_reflectivity(self): @@ -816,9 +798,11 @@ class DigitalTwin(BECWidget, QWidget): else: fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3 energy = self.input.energy.value() - self.input.fm_refl.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, energy)) + self.input.fm_refl.setValue(100 * self.core.fm_reflectivity(fm_stripe, fm_rotx, energy)) self.input.fm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV") - self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, 3 * energy)) + self.input.fm_refl_harm.setValue( + 100 * self.core.fm_reflectivity(fm_stripe, fm_rotx, 3 * energy) + ) self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV") def calc_cm_fm_harm_suppr(self): @@ -838,14 +822,14 @@ class DigitalTwin(BECWidget, QWidget): Updates the sideview plot based on the assistant values """ config = self.get_assistant_config(apply_offset=True) - data = calc_sideview(config) + data = self.core.calc_sideview(config) self.sideview_plot.update_curves("assistant", data) def calc_assistant_surfaces(self): """ Updates the surface plot based on the assistant values """ - surfaces = calc_surfaces(self.get_assistant_config()) + surfaces = self.core.calc_surfaces(self.get_assistant_config()) self.surface_plots.update_surfaces(scene="assistant", data=surfaces) def calc_positions(self): @@ -908,7 +892,7 @@ class DigitalTwin(BECWidget, QWidget): cm_pitch = -self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] * 1e-3 mo1_mode = cast(Literal["Monochromatic", "Pinkbeam"], self.input.mo1_mode.currentText()) energy = self.input.energy.value() - theta, _ = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch) + theta, _ = self.core.mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch) self.bragg_angle = theta self.input.mo1_bragg_angle.setValue(theta / np.pi * 180) @@ -939,12 +923,12 @@ class DigitalTwin(BECWidget, QWidget): smpl = self.input.smpl.value() case ComboBox(): table = self.input.smpl.currentText() - smpl = table_to_smpl_pos(table) + smpl = self.core.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() fm_focy = self.input.fm_focy.value() - fm_rotx, qy = fm_ideal_pitch( + fm_rotx, qy = self.core.fm_ideal_pitch( fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy ) self.qy = qy @@ -956,7 +940,7 @@ class DigitalTwin(BECWidget, QWidget): """ cm_stripe = cast(Literal["Si", "Pt", "Rh"], self.input.cm_stripe.currentText()) energy = self.input.energy.value() - self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3) + self.input.cm_pitch_critical.setValue(-self.core.cm_critical_angle(cm_stripe, energy) * 1e3) if __name__ == "__main__": diff --git a/debye_bec/bec_widgets/widgets/digital_twin/offsets.py b/debye_bec/bec_widgets/widgets/digital_twin/offsets.py deleted file mode 100644 index e905afc..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/offsets.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Offset class to load or unload offsets from a file -""" - -from pathlib import Path - -import yaml -from bec_lib import bec_logger - -from .beamline import get_beamline_id - -OFFSET_FILE_X01DA = Path(__file__).with_name("x01da_offsets.yaml") -OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml") - -logger = bec_logger.logger - - -class Offsets: - - def __init__(self, *arg, **kwargs): - self.beamline = get_beamline_id() - self.offset_file = Path() - match self.beamline: - case "x01da": - self.offset_file = OFFSET_FILE_X01DA - case "x10da": - self.offset_file = OFFSET_FILE_X10DA - self.offsets = {} - - def load_offsets(self): - if self.offsets == {}: - logger.info("Load beamline offsets") - if not self.offset_file.exists(): - raise FileNotFoundError(f"Offset file not found: {self.offset_file}") - - with self.offset_file.open("r", encoding="utf-8") as f: - data = yaml.safe_load(f) - - if not isinstance(data, dict): - raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}") - - self.offsets = data - else: - logger.info("Unload beamline offsets") - self.offsets = {} - - def apply_offsets(self, config, nested_config=False): - for axis, axis_data in config.items(): - if axis in self.offsets: - axis_offsets = self.offsets[axis] - if "modifier" in axis_offsets and "offset" in axis_offsets: - for idx, rng in enumerate(axis_offsets["modifier"]["range"]): - modifier_axis = axis_offsets["modifier"]["axis"] - modifier_value = ( - config[modifier_axis]["value"] - if nested_config - else config[modifier_axis] - ) - if rng[0] < modifier_value < rng[1]: - if nested_config: - axis_data["value"] += axis_offsets["offset"][idx] - else: - config[axis] += axis_offsets["offset"][idx] - break - elif "offset" in axis_offsets: - if nested_config: - axis_data["value"] += axis_offsets["offset"] - else: - config[axis] += axis_offsets["offset"] - return config - - def remove_offsets(self, config): - for axis, _ in config.items(): - if axis in self.offsets: - axis_offsets = self.offsets[axis] - if "modifier" in axis_offsets and "offset" in axis_offsets: - for idx, rng in enumerate(axis_offsets["modifier"]["range"]): - if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]: - config[axis] -= axis_offsets["offset"][idx] - break - elif "offset" in axis_offsets: - config[axis] -= axis_offsets["offset"] - return config diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py index 8bebec6..5c4cd1b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py @@ -7,7 +7,7 @@ from typing import Union # pylint: disable=E0611 from qtpy.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from ..types import BeamlineId +from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId from ..widgets.qt_widgets import ( Button, ComboBox, diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py index 4b43a3d..148d0ef 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py @@ -7,7 +7,7 @@ from typing import Literal # pylint: disable=E0611 from qtpy.QtWidgets import QVBoxLayout, QWidget -from ..types import BeamlineId +from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId from ..widgets.move_widget import AbsorberWidget, MoveWidget from ..widgets.qt_widgets import Group diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py index b7d7131..0d8f383 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py @@ -15,13 +15,11 @@ from qtpy.QtGui import QBrush, QColor # pylint: disable=E0611 from qtpy.QtWidgets import QApplication, QGraphicsRectItem, QHBoxLayout, QVBoxLayout, QWidget -from ..calculations.calc_varia import ( - mirror_surface_geometries, - mo_surface_geometries, - pipe_geometries, - wall_geometries, +from .....bec_ipython_client.plugins.digital_twin_core.types import ( + BeamlineId, + DataDict, + SurfaceDict, ) -from ..types import BeamlineId, DataDict, SurfaceDict from ..widgets.qt_widgets import Group logger = bec_logger.logger @@ -30,9 +28,10 @@ logger = bec_logger.logger class SurfacePlots(QWidget): """Plot widget with two curves and legend.""" - def __init__(self, beamline: BeamlineId, parent=None): + def __init__(self, beamline: BeamlineId, core, parent=None): super().__init__(parent=parent) self.beamline = beamline + self.core = core self._layout = QHBoxLayout(self) self._layout.setContentsMargins(4, 4, 4, 4) self._layout.setSpacing(6) @@ -169,15 +168,15 @@ class SurfacePlots(QWidget): for name, plot in self.plots.items(): if name == "cm": - plot_surface(plot["widget"], mirror_surface_geometries("cm")) + plot_surface(plot["widget"], self.core.mirror_surface_geometries("cm")) elif name == "mo1_1": - plot_surface(plot["widget"], mo_surface_geometries("mo1", 0)) + plot_surface(plot["widget"], self.core.mo_surface_geometries("mo1", 0)) elif name == "mo1_2": - plot_surface(plot["widget"], mo_surface_geometries("mo1", 1)) + plot_surface(plot["widget"], self.core.mo_surface_geometries("mo1", 1)) elif name == "fm": if self.beamline == "x01da": - plot_surface(plot["widget"], mirror_surface_geometries("fm_flat")) - plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid")) + plot_surface(plot["widget"], self.core.mirror_surface_geometries("fm_flat")) + plot_surface(plot["widget"], self.core.mirror_surface_geometries("fm_toroid")) else: raise ValueError(f"Plot {name} not found!") for name, plot in self.plots.items(): @@ -203,8 +202,9 @@ class SurfacePlots(QWidget): class SideviewPlot(QWidget): """Plot widget with two curves and legend.""" - def __init__(self, parent=None): + def __init__(self, core, parent=None): super().__init__(parent=parent) + self.core = core self._layout = QVBoxLayout(self) self._layout.setContentsMargins(4, 4, 4, 4) self._layout.setSpacing(0) @@ -303,7 +303,7 @@ class SideviewPlot(QWidget): def plot_vacuum_pipes(self): """Plot vacuum pipes""" - pipes = pipe_geometries() + pipes = self.core.pipe_geometries() for pipe in pipes: self.pipes.append( self.plot_widget.plot( @@ -313,7 +313,7 @@ class SideviewPlot(QWidget): def plot_walls(self): """Plot walls""" - walls = wall_geometries() + walls = self.core.wall_geometries() for wall in walls: rect = QGraphicsRectItem(wall[0], wall[1], wall[2], wall[3]) rect.setBrush(QBrush(QColor(*self.color_impenetrable))) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/types.py b/debye_bec/bec_widgets/widgets/digital_twin/types.py deleted file mode 100644 index d5c0393..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/types.py +++ /dev/null @@ -1,83 +0,0 @@ -"""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. - - Attributes: - energy (float): Beam energy. - h_acc (float): Horizontal acceptance. - v_acc (float): Vertical acceptance. - cm_pitch (float): CM pitch angle. - cm_stripe (str): CM stripe name. - cm_trx (float): CM translation x. - mo1_mode (str): MO1 mode. - mo1_xtal (str): MO1 crystal. - mo1_bragg (float): MO1 Bragg angle. - fm_rotx (float): FM rotation x. - fm_stripe (str): FM stripe name. - fm_trx (float): FM translation x. - fm_qy (float): FM qy value. - fm_gain_height (int): FM gain height. - smpl (float): Sample value. - """ - - energy: float - h_acc: float - v_acc: float - cm_pitch: float - cm_stripe: str - cm_trx: float - mo1_mode: str - mo1_xtal: str - mo1_bragg: float - fm_rotx: float - fm_stripe: str - fm_trx: float - fm_qy: None | float - fm_gain_height: int - smpl: float - - -class DataDict(TypedDict): - """ - Typed dictionary representing plot data. - - Attributes: - x (list[float]): List of x-axis values. - y (list[float]): List of y-axis values. - """ - - x: list - y: list - - -class SurfaceDict(TypedDict): - """ - Typed dictionary representing the surfaces of a scene, - grouping plot data by surface type. - - Attributes: - cm (DataDict): Data for the cm surface. - mo1_1 (DataDict): Data for the mo1_1 surface. - mo1_2 (DataDict): Data for the mo1_2 surface. - fm (DataDict): Data for the fm surface. - """ - - cm: DataDict - mo1_1: DataDict - mo1_2: DataDict - fm: DataDict diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py index 0aa6f24..6df4065 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py @@ -15,9 +15,10 @@ from qtpy.QtCore import QObject, QPropertyAnimation, Qt, QThread from qtpy.QtGui import QTransform from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidget +from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId + # pylint: disable=E0402 from .....devices.absorber import STATUS as ABS_STATUS -from ..types import BeamlineId logger = bec_logger.logger diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml b/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml deleted file mode 100644 index f475d96..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml +++ /dev/null @@ -1,50 +0,0 @@ -cm_try: - offset: 0.15 - -mo1_trx: - modifier: - axis: mo1_trx - range: [[-30, -0.1], [0.1, 30]] - offset: [-2.3, 1.31] - -mo1_try: - modifier: - axis: mo1_trx - range: [[-30, -0.1], [0.1, 30]] - offset: [-1.78, -1.78] - -sl1_centery: - offset: -1.2 - -fm_trx: - modifier: - axis: fm_trx - range: [[-66, -31], [-24, 7], [11, 31], [38, 66]] - offset: [-0.61, 0, 0, -0.16] - -fm_try: - modifier: - axis: fm_trx - range: [[-66, -31], [-24, 7], [11, 31], [38, 66]] - offset: [0.028, 0, 0, -0.45] - -fm_rotx: - modifier: - axis: fm_trx - range: [[-66, -31], [-24, 7], [11, 31], [38, 66]] - offset: [0.027, 0, 0, 0.045] - -fm_roty: - modifier: - axis: fm_trx - range: [[-66, -31], [-24, 7], [11, 31], [38, 66]] - offset: [-0.038, 0, 0, -0.053] - -sl2_centery: - offset: -0.7 - -ot_try: - offset: -0.49 - -ot_rotx: - offset: 0 \ No newline at end of file diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py deleted file mode 100644 index 214cb7d..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py +++ /dev/null @@ -1,323 +0,0 @@ -""" -X01DA / Debye Beamline Parameters. -This file describes the parameter of each component of the Debye 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 -fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"]) - -feSlits = fe_slits( - name="FE-SLITS", - center=(0, 6117, sourceHeight), - center1=(0, 5045, sourceHeight), - center2=(0, 5289.5, sourceHeight), - maxDivH=1.8e-3, - maxDivV=0.8e-3, -) - -# FE Window -filt = namedtuple( - "filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"] -) - -feWindow = filt( - name="FE-WINDOW", - center=(0.0, 7020, sourceHeight), - pitch=np.pi / 2, - limPhysX=(-6, 6), - limPhysY=(-3.0, 3.0), - surface="None", - material=filterDiamond, - thickness=0.1, -) -feWindow = feWindow._replace(surface=f"CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m") - -# 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, 6890, sourceHeight], - surface=("Si", "Pt", "Rh"), - material=(stripeSi, stripePt, stripeRh), - limPhysX=(-34, 34), - limPhysY=(-600, 600), - limOptX=((-21, -7, 14), (-11, 11, 23)), - limOptY=((-500, -500, -500), (500, 500, 500)), - R=[3e6, 15e6], - pitch=[-5.0e-3, -0.0e-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, 8815, sourceHeight], opening=[-20.0, 20.0, -20.0 + 12.5, 20.0 + 12.5] -) # left, right, bottom, top - -opWbBsBlock = apertures( - name="OP-WB-BS-BLOCK", center=[0.0, 13860, sourceHeight], opening=[-18.0, 18.0, 25, 85.5] -) # left, right, bottom, top -# opening=[-18., 18., 42, 76], # X10DA - -# 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-MO1", - center=[0.0, 11750, sourceHeight], - xtal=("Si311", "Si111"), - material1=(si311_1, si111_1), - material2=(si311_2, si111_2), - xtalWidth=(24, 24), - xtalOffsetX=(-21.2, 21.2), - xtalLength1=(55, 55), - xtalLength2=(105, 105), - xtalGap=(8, 8), - rotOffset=6, - heightOffset=8.5, - braggLim=[3.6, 33], - jack1=[0.0, 11350.0, 0.0], # Tripod maybe not available! - jack2=[-400.0, 12350.0, 0.0], - jack3=[400.0, 12350.0, 0.0], - tx=0.0, -) # X-Stage [x] - -mo2 = monochromator( - name="OP-CCM2", - center=[0.0, 13250, sourceHeight], - xtal=("Si311", "Si111"), - material1=(si311_1, si111_1), - material2=(si311_2, si111_2), - xtalWidth=(24, 24), - xtalOffsetX=(-21, 21), - xtalLength1=(55, 55), - xtalLength2=(105, 105), - xtalGap=(8, 8), - rotOffset=6, - heightOffset=8.5, - braggLim=[3.6, 33], - jack1=[0.0, 13350.0, 0.0], # Tripod maybe not available! - jack2=[-400.0, 14350.0, 0.0], - jack3=[400.0, 14350.0, 0.0], - tx=0.0, -) # X-Stage [x] - -# OP Slits -op_slits = namedtuple("op_slits", ["name", "center"]) - -opSlits1 = op_slits(name="OP-SLITS 1", center=(0, 14349.6, sourceHeight)) - -opSlits2 = op_slits(name="OP-SLITS 2", center=(0, 18134.8, sourceHeight)) - -# OP Beam Monitors -op_bm = namedtuple("op_bm", ["name", "center"]) - -opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14599.6, sourceHeight)) - -opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 18384.8, sourceHeight)) - -# Focusing mirror -focusingMirror = namedtuple( - "focusingMirror", - [ - "name", - "center", - "surfaceToroid", - "materialToroid", - "surfaceFlat", - "materialFlat", - "limPhysXToroid", - "limPhysYToroid", - "limPhysXFlat", - "limPhysYFlat", - "limOptXToroid", - "limOptYToroid", - "limOptXFlat", - "limOptYFlat", - "R", - "pitch", - "r", - "xToroid", - "xFlat", - "hToroid", - "jack1", - "jack2", - "jack3", - "tx1", - "tx2", - ], -) - -fm = focusingMirror( - name="OP-FM", - center=[0.0, 15670, sourceHeight], # nominal height 58 mm above ring, SLS1! - surfaceToroid=("Rh", "Pt"), - materialToroid=(stripeRh, stripePt), - surfaceFlat=("Rh", "Pt"), - materialFlat=(stripeRh, stripePt), - limPhysXToroid=(-79.0, 79.0), - limPhysYToroid=(-575.0, 575.0), - limPhysXFlat=(-79.0, 79.0), - limPhysYFlat=(-575.0, 575.0), - limOptXToroid=((-38, 66), (-66, 31)), - limOptYToroid=((-500.0, -500.0), (500.0, 500.0)), - limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)), - limOptYFlat=((-500.0, -500.0), (500.0, 500.0)), - R=[3e6, 15e6], - pitch=[-5.0e-3, 0e-3], - r=[35.510, 24.986], - xToroid=[-52, 48.5], # offset in local x - xFlat=[-20.95, 8.55], - hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2. - jack1=[-130.0, 15535 - 538.0, 0.0], - jack2=[130.0, 15535 + 538.0, 0.0], - jack3=[0.0, 15535 + 538.0, 0.0], - tx1=[0.0, -575.0], # X-Stage 1 [x, y] - tx2=[0.0, 575.0], -) # X-Stage 2 [x, y] - -# EH Window -ehWindow = filt( - name="EH-WINDOW", - center=(0.0, 19998.3, sourceHeight), - pitch=np.pi / 2, - limPhysX=(-20.0, 20.0), - limPhysY=(-4, 4), - surface="None", - material=filterSi3N4, - thickness=0.002, -) -ehWindow = ehWindow._replace(surface=f"Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m") - -# Sample -sample = namedtuple("sample", ["name", "center"]) - -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 -# DN63CF ID = 60.2 mm oder 66 mm -# DN100CF ID = 97.4 mm oder 104 mm -pipe = namedtuple("pipes", ["center", "diameter", "start", "end"]) -vacuum_pipes = pipe( - center=[27.5, (37.5 + 27.5) / 2, 37.5, 62.5, 72.5], - diameter=[97.4, 97.4, 97.4, 97.4, 97.4], - start=[10952.88, 11750 + 250, mo2.center[1] + 250, 14000, fm.center[1]], - end=[11750 - 250, mo2.center[1] - 250, 14000, fm.center[1], ehWindow.center[1]], -) - -Walls = namedtuple("walls", ["start", "end", "height"]) -walls = Walls(start=[13999.30], end=[13999 + 75.5 + 30], height=[[-20, 25]]) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml b/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml deleted file mode 100644 index f0fe9a8..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml +++ /dev/null @@ -1,59 +0,0 @@ - -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 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py deleted file mode 100644 index b7b40a5..0000000 --- a/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py +++ /dev/null @@ -1,296 +0,0 @@ -""" -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 -fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"]) - -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( - "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, 7560.8, sourceHeight], - surface=("Pt", "Si", "Rh"), - material=(stripePt, stripeSi, stripeRh), - limPhysX=(-30, 30), - limPhysY=(-600, 600), - 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], - 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 - -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", - [ - "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", - ], -) - -OFFSET_TRX = 46.8735 - -fm = focusingMirror( - name="OP-FM", - center=[0.0, 15580 - 135, sourceHeight], - surfaceToroid=("Rh", "Pt"), - materialToroid=(stripeRh, stripePt), - limPhysXToroid=(-54.0, 54.0), - limPhysYToroid=(-565.0, 565.0), - 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 + 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], - 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] - -# Entry wall experimental hutch: 21593 mm from source (SLS2) - -# Exit window -ehWindow = filt( - name="EH-WINDOW", - center=(0.0, 22063, 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"]) - -es1 = sample(name="ES1", center=[0, 23823, sourceHeight]) -es2 = sample(name="ES2", center=[0, 25843, 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]) diff --git a/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py b/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py index 79d6bc3..02066bf 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py +++ b/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py @@ -109,7 +109,7 @@ class ScheduleItemDialog(QDialog): self.beamline = beamline if self.beamline in ["x01da", "x10da"]: logger.info(f"Loading bl-specific modules for beamline {self.beamline}") - from ....bec_ipython_client.plugins.digital_twin.digital_twin import DigitalTwinCore + from ....bec_ipython_client.plugins.digital_twin_core.digital_twin_core import DigitalTwinCore from ..digital_twin.digital_twin import DigitalTwin from ..edge_selector import EdgeSelector from ..scan_control_xas.scan_control_xas import ScanControlXAS diff --git a/debye_bec/bec_widgets/widgets/scheduler/scheduler.py b/debye_bec/bec_widgets/widgets/scheduler/scheduler.py index b398d16..97e25a2 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/scheduler.py +++ b/debye_bec/bec_widgets/widgets/scheduler/scheduler.py @@ -156,7 +156,7 @@ class Scheduler(BECWidget, QWidget): logger.info( 'Scheduler running at X01DA or X10DA, import and load digital twin and auto-gain' ) - from ....bec_ipython_client.plugins.digital_twin.digital_twin import DigitalTwinCore + from ....bec_ipython_client.plugins.digital_twin_core.digital_twin_core import DigitalTwinCore from ....bec_ipython_client.plugins.auto_gain import AutoGain self.digital_twin = DigitalTwinCore() self.auto_gain = AutoGain()