From b0a7d6905cd2edef01d25770cdf8666af12762f1 Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 6 May 2026 14:53:06 +0200 Subject: [PATCH] wip: digital twin --- .../digital_twin/calculate_positions.py | 5 +- .../widgets/digital_twin/digital_twin.py | 78 +++++++++++++++++-- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculate_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calculate_positions.py index 8012baf..971b7e1 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculate_positions.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculate_positions.py @@ -146,7 +146,10 @@ def calc_positions(cfg): f = (p*q)/(p+q) # focal length # Bender radius - radius = 2 * q / np.sin(cfg['fm_pitch']) # ideal bending radius + if cfg['fm_qy'] is None: + radius = 2 * q / np.sin(cfg['fm_pitch']) # ideal bending radius for focused beam + else: + radius = 2 * cfg['fm_qy'] / np.sin(cfg['fm_pitch']) # ideal bending radius for unfocused beam pos['fm_bnd_radius'] = {'value': radius * 1e-6} # Convert to km # Pitch 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 3146a34..0c739d1 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -88,10 +88,14 @@ class DigitalTwin(BECWidget, QWidget): self.input.mo1_mode.activated_connect(self.calc_assistant) self.input.mo1_xtal.activated_connect(self.calc_assistant) self.input.fm_stripe.activated_connect(self.calc_assistant) + self.input.fm_focus.activated_connect(self.calc_assistant) self.input.fm_pitch.value_changed_connect(self.calc_assistant) + self.input.fm_focx.value_changed_connect(self.calc_assistant) + self.input.fm_focy.value_changed_connect(self.calc_assistant) self.input.smpl.value_changed_connect(self.calc_assistant) self.bragg_angle = 0 + self.qy = 0 # Initialize all values self.calc_assistant(identifier='init') @@ -111,6 +115,7 @@ class DigitalTwin(BECWidget, QWidget): self.calc_mo1_bragg_angle() self.calc_cm_crit_pitch() self.calc_cm_reflectivity() + self.update_fm_mode() self.calc_fm_reflectivity() self.calc_cm_fm_harm_suppr() self.calc_fm_ideal_pitch() @@ -134,6 +139,13 @@ class DigitalTwin(BECWidget, QWidget): case 'mo1_xtal': self.calc_mo1_bragg_angle() self.calc_mo1_energy_resolution() + case 'fm_focus': + self.update_fm_mode() + self.calc_fm_ideal_pitch() + case 'fm_focx': + self.calc_fm_ideal_pitch() + case 'fm_focy': + self.calc_fm_ideal_pitch() case 'fm_stripe': self.calc_fm_reflectivity() self.calc_cm_fm_harm_suppr() @@ -144,6 +156,24 @@ class DigitalTwin(BECWidget, QWidget): self.calc_assistant_sideview() self.calc_assistant_surfaces() + def update_fm_mode(self): + fm_focus = self.input.fm_focus.currentText() + if fm_focus in 'Manual': + self.input.fm_pitch.setVisible(True) + self.input.fm_pitch_ideal.setVisible(True) + self.input.fm_focx.setVisible(False) + self.input.fm_focy.setVisible(False) + elif fm_focus in 'Focused': + self.input.fm_pitch.setVisible(False) + self.input.fm_pitch_ideal.setVisible(True) + self.input.fm_focx.setVisible(False) + self.input.fm_focy.setVisible(False) + else: # Defocused + self.input.fm_pitch.setVisible(False) + self.input.fm_pitch_ideal.setVisible(True) + self.input.fm_focx.setVisible(True) + self.input.fm_focy.setVisible(True) + @SafeSlot() def calc_reality(self): config = self.get_reality_config() @@ -226,6 +256,18 @@ class DigitalTwin(BECWidget, QWidget): self.input.cm_fm_harm_suppr.setLabel(f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV") def get_assistant_config(self): + + fm_focus = self.input.fm_focus.currentText() + if fm_focus in 'Manual': + fm_pitch = self.input.fm_pitch.value() + fm_qy = None + elif fm_focus in 'Focused': + fm_pitch = self.input.fm_pitch_ideal.value() + fm_qy = None + else: # Focused + fm_pitch = self.input.fm_pitch_ideal.value() + fm_qy = self.qy + config = { # Config in SI units! 'energy' : self.input.energy.value(), 'h_acc' : self.input.sldi_hacc.value() * 1e-3, @@ -236,9 +278,10 @@ class DigitalTwin(BECWidget, QWidget): 'mo1_mode' : self.input.mo1_mode.currentText(), 'mo1_xtal' : self.input.mo1_xtal.currentText(), 'mo1_bragg' : self.bragg_angle, - 'fm_pitch' : -self.input.fm_pitch.value() * 1e-3, + 'fm_pitch' : -fm_pitch * 1e-3, 'fm_stripe' : self.input.fm_stripe.currentText(), 'fm_trx' : None, + 'fm_qy' : fm_qy, 'fm_gain_height' : 1, 'smpl' : self.input.smpl.value(), } @@ -364,15 +407,28 @@ class DigitalTwin(BECWidget, QWidget): @SafeSlot() def update_mo1_mode(self): if self.input.mo1_mode.currentText() in 'Monochromatic': - self.input.mo1_xtal.setDisabled(False) + self.input.mo1_xtal.setVisible(True) + self.input.mo1_bragg_angle.setVisible(True) + self.input.mo1_eres.setVisible(True) else: - self.input.mo1_xtal.setDisabled(True) + self.input.mo1_xtal.setVisible(False) + self.input.mo1_bragg_angle.setVisible(False) + self.input.mo1_eres.setVisible(False) @SafeSlot() def calc_fm_ideal_pitch(self): p = bl.fm.center[1] # posFM q = self.input.smpl.value() - bl.fm.center[1] # dist posFM to posEX - f = (p * q) / (p + q) # focal length + if self.input.fm_focus.currentText() in 'Defocused': + a = 2 * np.tan(self.input.sldi_hacc.value() * 1e-3) * bl.fm.center[1] # Beam width at focusing mirror + b = 2 * np.tan(self.input.sldi_vacc.value() * 1e-3) * bl.cm.center[1] # Beam height at focusing mirror (collimated beam) + x = self.input.fm_focx.value() + y = self.input.fm_focy.value() + qx = q + x * p / a + self.qy = q + y * p / b + f = (p * qx) / (p + qx) # focal length + else: # Calculate for focused beam on sample in "manual" and "focused" mode + f = (p * q) / (p + q) # focal length pitch = 0 if 'Rh' in self.input.fm_stripe.currentText(): pitch = np.arcsin(bl.fm.r[0]/(2*f))# ideal pitch for FM @@ -423,16 +479,16 @@ class InputPanel(QWidget): # Collimating mirror self.cm_stripe = ComboBox('cm_stripe', 'Stripe', ['Si', 'Rh', 'Pt']) - self.cm_pitch_critical = NumberIndicator('Critical Pitch', 'mrad', decimals=3) self.cm_pitch = InputNumberField('cm_pitch', 'Pitch [mrad]', init=-2.391, decimals=3, single_step=0.01, ll=-4.6, hl=-1.2) + self.cm_pitch_critical = NumberIndicator('Critical Pitch', 'mrad', decimals=3) self.cm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0) self.cm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0) self.cm_ass_group = Group( 'Collimating Mirror', [ self.cm_stripe, - self.cm_pitch_critical, self.cm_pitch, + self.cm_pitch_critical, self.cm_refl, self.cm_refl_harm, ] @@ -455,16 +511,22 @@ class InputPanel(QWidget): # Focusing Mirror self.fm_stripe = ComboBox('fm_stripe', 'Stripe', ['Rh (toroid)', 'Rh (flat)', 'Pt (toroid)', 'Pt (flat)']) - self.fm_pitch_ideal = NumberIndicator('Ideal Incidence Angle', 'mrad', decimals=3) + self.fm_focus = ComboBox('fm_focus', 'Focus Type', ['Manual', 'Focused', 'Defocused']) self.fm_pitch = InputNumberField('fm_pitch', 'Incidence Angle [mrad]', init=-2.391, decimals=3, single_step=0.01, ll=-10, hl=2) + self.fm_focx = InputNumberField('fm_focx', 'Beam Size Horizontal [mm]', init=1, decimals=1, single_step=0.1, ll=0, hl=30) + self.fm_focy = InputNumberField('fm_focy', 'Beam Size Vertical [mm]', init=1, decimals=1, single_step=0.1, ll=0, hl=10) + self.fm_pitch_ideal = NumberIndicator('Incidence Angle for focused beam', 'mrad', decimals=3) self.fm_refl = NumberIndicator('Reflectivity at x eV', '%', decimals=0) self.fm_refl_harm = NumberIndicator('Reflectivity at x eV', '%', decimals=0) self.fm_ass_group = Group( 'Focusing Mirror', [ self.fm_stripe, - self.fm_pitch_ideal, + self.fm_focus, self.fm_pitch, + self.fm_focx, + self.fm_focy, + self.fm_pitch_ideal, self.fm_refl, self.fm_refl_harm, ]