From a9b9275350e6d9cbed7adf46f040df4ea54c7bed Mon Sep 17 00:00:00 2001 From: Vincent Esposito Date: Tue, 27 Feb 2024 14:25:51 -0800 Subject: [PATCH 1/4] working setup for Yano exp Feb 2024 --- pcdsdevices/lens.py | 93 +++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/pcdsdevices/lens.py b/pcdsdevices/lens.py index 09cc8b43fa5..430a42c08fb 100644 --- a/pcdsdevices/lens.py +++ b/pcdsdevices/lens.py @@ -6,7 +6,6 @@ from collections import defaultdict from datetime import date -import numpy as np from ophyd.device import Component as Cpt from ophyd.device import FormattedComponent as FCpt from pcdscalc import be_lens_calcs as calcs @@ -132,15 +131,16 @@ class LensStackBase(BaseInterface, PseudoPositioner): tab_component_names = True def __init__(self, x_prefix, y_prefix, z_prefix, lens_set, - z_offset, z_dir, E, att_obj, lcls_obj=None, - mono_obj=None, *args, **kwargs): + z_offset, z_dir, E, att_obj, + lcls_obj=None, mono_obj=None, *args, **kwargs): self.x_prefix = x_prefix self.y_prefix = y_prefix self.z_prefix = z_prefix self.z_dir = z_dir self.z_offset = z_offset - self._E = E + self._E = None + self._which_E = None self._att_obj = att_obj self._lcls_obj = lcls_obj self._mono_obj = mono_obj @@ -150,6 +150,33 @@ def __init__(self, x_prefix, y_prefix, z_prefix, lens_set, super().__init__(x_prefix, *args, **kwargs) + @property + def energy(self): + if self._E is not None: + self._which_E = "User" + return self._E + elif self._mono_obj is not None: + if self._mono_obj.inserted: + self._which_E = 'ccm' + return self._mono_obj.energy.energy.get().readback + else: + print('CCM not in, defaulting to LCLS energy') + + if self._lcls_obj is not None: + self._which_E = 'lcls' + return self._lcls_obj.hard_e_energy.get() + + @energy.setter + def energy(self, energy): + self._E = energy + + def get_current_beam_info(self): + dist_m = self.z.position / 1000 * self.z_dir + self.z_offset + beamsize = calcs.calc_beam_fwhm(self.energy, self.lens_set, + distance=dist_m) + print(f"Distance for beamsize {beamsize}: {dist_m}m") + return + def tweak(self): """ Call the tweak function from `pcdsdevice.interface`. @@ -186,11 +213,13 @@ def forward(self, pseudo_pos): AttributeError If pseudo motor is not setup for use. """ - if not np.isclose(pseudo_pos.beam_size, self.beam_size.position): + # if not np.isclose(pseudo_pos.beam_size, self.beam_size.position): + if True: beam_size = pseudo_pos.beam_size dist = calcs.calc_distance_for_size(beam_size, self.lens_set, - self._E)[0] + self.energy)[0] z_pos = (dist - self.z_offset) * self.z_dir * 1000 + z_pos = z_pos - 100 # dirty trick to get z in the range else: z_pos = pseudo_pos.calib_z try: @@ -229,8 +258,9 @@ def inverse(self, real_pos): """ dist_m = real_pos.z / 1000 * self.z_dir + self.z_offset logger.info('dist_m %s', dist_m) - beamsize = calcs.calc_beam_fwhm(self._E, self.lens_set, - distance=dist_m) + beamsize = calcs.calc_beam_fwhm(self.energy, self.lens_set, + distance=dist_m, + printsummary=False) return self.PseudoPosition(calib_z=real_pos.z, beam_size=beamsize) def align(self, z_position=None, edge_offset=20): @@ -302,8 +332,9 @@ def move(self, position, wait=True, timeout=None, moved_cb=None): positioner instance. """ if self._make_safe() is True: - return super().move(position, wait=wait, timeout=timeout, - moved_cb=moved_cb) + st = super().move(position, wait=wait, timeout=timeout, + moved_cb=moved_cb) + return st else: logger.warning('Aborting moving for safety.') return @@ -322,20 +353,30 @@ def _make_safe(self): logger.warning('Cannot do safe moveZ, no attenuator' ' object provided.') return False - filt, thk = self._att_obj.filters[0], 0 - for f in self._att_obj.filters: - t = f.thickness.get() - if t > thk: - filt, thk = f, t - if not filt.inserted: - filt.insert() + if 'Attenuator' in self._att_obj.__class__.__name__: + filt, thk = self._att_obj.filters[0], 0 + for f in self._att_obj.filters: + t = f.thickness.get() + if t > thk: + filt, thk = f, t + if not filt.inserted: + filt.insert(wait=True) + time.sleep(0.01) + if filt.inserted: + logger.info('Beam stop attenuator moved in!') + safe = True + else: + logger.warning('Beam stop attenuator did not move in!') + safe = False + elif 'PulsePicker' in self._att_obj.__class__.__name__: + self._att_obj.close(wait=True) time.sleep(0.01) - if filt.inserted: - logger.info('Beam stop attenuator moved in!') - safe = True - else: - logger.warning('Beam stop attenuator did not move in!') - safe = False + if self._att_obj.inserted: + logger.info('Pulse picker closed!') + safe = True + else: + logger.warning('Pulse picker did not close!') + safe = False return safe @@ -530,11 +571,13 @@ def __init__(self, x_prefix, y_prefix, z_prefix, z_offset, z_dir, E, if lens_set is None: # Defaulting this a the first set in the file for now - lens_set = calcs.get_lens_set(1, self.path) + self.lens_set = calcs.get_lens_set(1, self.path) + else: + self.lens_set = lens_set super().__init__( x_prefix, y_prefix, z_prefix, lens_set, z_offset, z_dir, E, - att_obj, *args, **kwargs + att_obj, lcls_obj, mono_obj, *args, **kwargs ) def read_lens(self, print_only=False): From 13802a21a80371d2670a55ffe94c59e1bd2ec5b0 Mon Sep 17 00:00:00 2001 From: Vincent Esposito Date: Tue, 27 Feb 2024 18:24:52 -0800 Subject: [PATCH 2/4] lens_for_escan not working --- pcdsdevices/lens.py | 15 ++++- pcdsdevices/lens_for_escan.py | 90 +++++++++++++++++++++++++++ pcdsdevices/tests/test_lens_sets/test | 2 +- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 pcdsdevices/lens_for_escan.py diff --git a/pcdsdevices/lens.py b/pcdsdevices/lens.py index 430a42c08fb..7b56205710c 100644 --- a/pcdsdevices/lens.py +++ b/pcdsdevices/lens.py @@ -152,6 +152,14 @@ def __init__(self, x_prefix, y_prefix, z_prefix, lens_set, @property def energy(self): + """ + Get the energy from various sources based on current situation: + - If self.energy = has been used, it will return the + the user-defined energy + - If self._mono_obj is defined and inserted, it will return the + current mono energy + - Return the LCLS energy otherwise + """ if self._E is not None: self._which_E = "User" return self._E @@ -171,6 +179,10 @@ def energy(self, energy): self._E = energy def get_current_beam_info(self): + """ + Calculates the beam size (FWHM) at the IP for the current lens + positin (distance to the interaction point). + """ dist_m = self.z.position / 1000 * self.z_dir + self.z_offset beamsize = calcs.calc_beam_fwhm(self.energy, self.lens_set, distance=dist_m) @@ -400,7 +412,8 @@ class LensStack(LensStackBase): 1 or -1, represents beam direction wrt z direction. E: number, optional Beam energy - att_obj : attenuator object, optional + att_obj : attenuator object, optional. Can be the beamline attenuator or + the pulse-picker. Used to block the beam while lens stack is moving. lcls_obj Object that gets PVs from lcls (for energy) mono_obj diff --git a/pcdsdevices/lens_for_escan.py b/pcdsdevices/lens_for_escan.py new file mode 100644 index 00000000000..21d6964d8e7 --- /dev/null +++ b/pcdsdevices/lens_for_escan.py @@ -0,0 +1,90 @@ +from ophyd.device import Component as Cpt +from ophyd.pseudopos import PseudoSingle + +from pcdsdevices.device import ObjectComponent as OCpt +from pcdsdevices.interface import FltMvInterface +from pcdsdevices.pseudopos import (PseudoPositioner, PseudoSingleInterface, + pseudo_position_argument, + real_position_argument) + + +class Lens4EScan(FltMvInterface, PseudoPositioner): + _real = ("mono_energy", "beam_size") + _pseudo = ("energy", ) + + mono_energy = OCpt('{mono_energy}') + beam_size = OCpt('{lens_stack.beam_size}') + + energy = Cpt(PseudoSingleInterface) + + tab_whitelist = ['calc_lens_pos', 'mono_energy', 'stack'] + tab_component_names = True + + def __init__(self, mono_energy, lens_stack, + desired_beamsize=5e-6, + *args, **kwargs): + self.mono = mono_energy + self.stack = lens_stack + self.desired_beamsize = desired_beamsize + super().__init__(*args, **kwargs) + + @pseudo_position_argument + def forward(self, pseudo_pos): + return self.RealPosition(mono_energy=pseudo_pos.energy, + beam_size=self.desired_beamsize) + + @real_position_argument + def inverse(self, real_pos): + return self.PseudoPosition(energy=real_pos.mono_energy) + + @pseudo_position_argument + def move(self, position, *args, **kwargs): + self.stack.energy = position.energy + st = super().move(position, *args, + moved_cb=self.cb_open_beamstop, + **kwargs) + return st + + def calc_lens_pos(self, energy): + """ + Return the expected lens position (x,y,z) for a given energy. + """ + stack_current_energy = self.stack.energy + if self.stack._which_E != 'User': + stack_current_energy = None + self.stack.energy = energy + lens_pos = self.stack.forward(beam_size=self.desired_beamsize) + self.stack.energy = stack_current_energy + return lens_pos + + def cb_open_beamstop(self, obj): + """ + Callback function to open the attenuator or pulse picker at the end of a + move. If the atttenuator is used, this function assumes that the blade #9 + is used to block the beam. + """ + if 'Attenuator' in self.stack._att_obj.__class__.__name__: + self.stack._att_obj.filters[9].remove() + elif 'PulsePicker' in self.stack._att_obj.__class__.__name__: + self.stack._att_obj.open() + return + + def _concurrent_move(self, real_pos, **kwargs): + """ + Try done fix: override the waiting list with the pseudopos parents, + not pseudosingle, since cb is called on the parent in this case. + + This is needed because PseudoSingle does not report the right status, + as they defer the motion to to their parent class (PseudoPositioner). + When building a pseudo-positioner made of PseudoSingles, one needs to + point the _real_waiting to the PseudoSingles' parent class. + """ + for real_axis in self._real: + if isinstance(real_axis, PseudoSingle): + self._real_waiting.append(real_axis.parent) + else: + self._real_waiting_append(real_axis) + + for real, value in zip(self._real, real_pos): + self.log.debug("[concurrent] Moving %s to %s", real.name, value) + real.move(value, wait=False, moved_cb=self._real_finished, **kwargs) diff --git a/pcdsdevices/tests/test_lens_sets/test b/pcdsdevices/tests/test_lens_sets/test index 728f59c60da..19765bd501b 100644 --- a/pcdsdevices/tests/test_lens_sets/test +++ b/pcdsdevices/tests/test_lens_sets/test @@ -1 +1 @@ -[2, 0.0002, 4, 0.0005] +null From de2990e1925abb6e94bc1131c83a35b12a744165 Mon Sep 17 00:00:00 2001 From: Vincent Esposito Date: Thu, 29 Feb 2024 23:34:36 -0800 Subject: [PATCH 3/4] atol=5 is way too small for self-seeding --- pcdsdevices/beam_stats.py | 2 +- pcdsdevices/ccm.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pcdsdevices/beam_stats.py b/pcdsdevices/beam_stats.py index 7bb48c2a561..52d7cf8917b 100644 --- a/pcdsdevices/beam_stats.py +++ b/pcdsdevices/beam_stats.py @@ -177,7 +177,7 @@ class BeamEnergyRequestNoWait(BeamEnergyRequest, PVPositionerDone): It will report done immediately and ignore moves that are smaller than atol. """ - atol = 5 + atol = 0.5 # All done-related functionality is inherited from PVPositionerDone # Just implement skip_small_moves's default diff --git a/pcdsdevices/ccm.py b/pcdsdevices/ccm.py index ab6136c55f4..ec6395160a4 100644 --- a/pcdsdevices/ccm.py +++ b/pcdsdevices/ccm.py @@ -9,6 +9,7 @@ from ophyd.device import Component as Cpt from ophyd.device import Device from ophyd.device import FormattedComponent as FCpt +from ophyd.pseudopos import pseudo_position_argument, real_position_argument from ophyd.signal import EpicsSignal, EpicsSignalRO, Signal from ophyd.status import MoveStatus @@ -589,6 +590,7 @@ def _update_intermediates( ref2 = self.alio_to_energy(value + res_delta/2) self.resolution.put(abs((ref1 - ref2) / res_delta), force=True) + @pseudo_position_argument def forward(self, pseudo_pos: namedtuple) -> namedtuple: """ PseudoPositioner interface function for calculating the setpoint. @@ -600,6 +602,7 @@ def forward(self, pseudo_pos: namedtuple) -> namedtuple: alio = self.energy_to_alio(energy) return self.RealPosition(alio=alio) + @real_position_argument def inverse(self, real_pos: namedtuple) -> namedtuple: """ PseudoPositioner interface function for calculating the readback. @@ -747,6 +750,7 @@ def __init__( self.hutch = 'TST' super().__init__(prefix, **kwargs) + @pseudo_position_argument def forward(self, pseudo_pos: namedtuple) -> namedtuple: """ PseudoPositioner interface function for calculating the setpoint. @@ -761,6 +765,7 @@ def forward(self, pseudo_pos: namedtuple) -> namedtuple: vernier = energy * 1000 return self.RealPosition(alio=alio, acr_energy=vernier) + @real_position_argument def inverse(self, real_pos: namedtuple) -> namedtuple: """ PseudoPositioner interface function for calculating the readback. From ee3c0b7f184208984401c6f6ea4d4c8dd61b3ee9 Mon Sep 17 00:00:00 2001 From: Vincent Esposito Date: Wed, 21 Aug 2024 16:03:48 -0700 Subject: [PATCH 4/4] check in small changes --- pcdsdevices/lens.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pcdsdevices/lens.py b/pcdsdevices/lens.py index 7b56205710c..4c657435c9d 100644 --- a/pcdsdevices/lens.py +++ b/pcdsdevices/lens.py @@ -66,6 +66,9 @@ class Prefocus(CombinedInOutRecordPositioner, LightpathInOutMixin): # In should be everything except state 0 (Unknown) and state 1 (Out) _in_if_not_out = True + tab_whitelist = ['x', 'y'] + tab_component_names = True + def __init__(self, prefix, *, name, **kwargs): # Set default transmission # Done this way because states are still unknown at this point @@ -75,6 +78,9 @@ def __init__(self, prefix, *, name, **kwargs): else (1 if state in self.out_states else 0)) super().__init__(prefix, name=name, **kwargs) + # motor aliases + self.x = self.x_motor + self.y = self.y_motor class LensStackBase(BaseInterface, PseudoPositioner): @@ -123,21 +129,22 @@ class LensStackBase(BaseInterface, PseudoPositioner): y = FCpt(IMS, '{self.y_prefix}') z = FCpt(IMS, '{self.z_prefix}') - calib_z = Cpt(PseudoSingleInterface) - beam_size = Cpt(PseudoSingleInterface) + calib_z = Cpt(PseudoSingleInterface, egu='m') + beam_size = Cpt(PseudoSingleInterface, egu='m') tab_whitelist = ['tweak', 'align', 'calib_z', 'beam_size', 'create_lens', 'read_lens'] tab_component_names = True def __init__(self, x_prefix, y_prefix, z_prefix, lens_set, - z_offset, z_dir, E, att_obj, + z_offset, z_dir, E, att_obj, fwhm_unfocused=None, lcls_obj=None, mono_obj=None, *args, **kwargs): self.x_prefix = x_prefix self.y_prefix = y_prefix self.z_prefix = z_prefix self.z_dir = z_dir self.z_offset = z_offset + self.fwhm_unfocused = fwhm_unfocused or 500e-6 self._E = None self._which_E = None @@ -184,8 +191,8 @@ def get_current_beam_info(self): positin (distance to the interaction point). """ dist_m = self.z.position / 1000 * self.z_dir + self.z_offset - beamsize = calcs.calc_beam_fwhm(self.energy, self.lens_set, - distance=dist_m) + beamsize = calcs.calc_beam_fwhm(self.energy, self.lens_set, distance=dist_m, + fwhm_unfocused=self.fwhm_unfocused) print(f"Distance for beamsize {beamsize}: {dist_m}m") return @@ -231,7 +238,7 @@ def forward(self, pseudo_pos): dist = calcs.calc_distance_for_size(beam_size, self.lens_set, self.energy)[0] z_pos = (dist - self.z_offset) * self.z_dir * 1000 - z_pos = z_pos - 100 # dirty trick to get z in the range + # z_pos = z_pos - 100 # dirty trick to get z in the range else: z_pos = pseudo_pos.calib_z try: @@ -269,7 +276,6 @@ def inverse(self, real_pos): PseudoPosition """ dist_m = real_pos.z / 1000 * self.z_dir + self.z_offset - logger.info('dist_m %s', dist_m) beamsize = calcs.calc_beam_fwhm(self.energy, self.lens_set, distance=dist_m, printsummary=False)