diff --git a/docs/position_instances.md b/docs/position_instances.md new file mode 100644 index 0000000000000000000000000000000000000000..191dfd0e4f2b82ccc7af0cb9d1305719cc40c4aa --- /dev/null +++ b/docs/position_instances.md @@ -0,0 +1,109 @@ +# PositionInstances ID26 BLISS Class +Class provinding tools to store/retrieve positions of a set of axis along other user actions. + +The positions are made persistant in time, so that they can be retrieved even in case of quitting bliss session. + +They belong to one particular bliss session and cannot be shared between sessions. +## Usage: +- **from id26.scripts.position_instances import PositionInstances** +- **user_instance = PositionInstances ('instance_name' , mot1, mot2, mot3 ...)** + loads as 'user_instance' session object any previously initialised PositionInstances with the name 'instance_name', or create it if it is the first time it is instantiated. + - if a list of axis is passed as arguments, slots are prepared for that set of axis in redis database. + - if no axis is passed, slots are prepared for all the motors of the session. + +- **user_instance.update ()** + stores in 'user_instance' object (attached to 'instance_name' in redis) the current positions of the axis in the set. +- **user_instance.update (mot1, mot2)** + stores in 'user_instance' object the current positions of the listed axis only. +- **user_instance.assign (mot3, 3.21)** + stores in 'user_instance' the specified position value for the specified axis. value can be None. +- **user_instance.setup ()** + launches a dialog window to enter positions to be stored in 'user_instance'. Position can be None +- **user_instance.move (mot3, mot4)** + move all axis listed to the position that was stored (if any) for them in 'user_instance' +- **user_instance.move_all ()** + move all axis of the set to the position that was stored (if any) for them in 'user_instance'. +- **user_instance.load (directory, suffix='')** + stores positions read from a previously saved file (see .save() below) + - directory (path) must be specified + - a suffix can be given, the filename is : {session}_positions{suffix}.yml + +The following functions act on all the position instances existing in redis database. The behaviour will be the same whatever is the session object calling it. +- **user_instance.show ()** + shows all positions sets stored among their different existing instances in redis database. It might happen that some instances are no longer useful. **note that this is the user responsibility to clear unused instances** to free redis memory with the next command. +- **user_instance.remove_instance ('unwanted_instance_name')** +remove specified instance from the database. + +- **user_instance.add (mot3, mot4, mot5)** + add some axis slots. do not forget to assign them some positions after. (.update(), .assign() or .setup()). +- **user_instance.remove (mot1, mot2)** + remove axis slots from the instances. + +- **user_instance.save (directory, suffix = '')** + saves to disk a yml files with all instances content. + - a directory must be specified. + - a suffix can be given, + - the filename is : {session}_positions{suffix}.yml + +## Example: +
+ MCL [5]: pos0 = PositionInstances ('oldpos0', gap) + Initialising oldpos0 + axis names: ['gap'] + + MCL [6]: pos0.update() + updating oldpos0 with {'gap': 3.0} + + MCL [7]: #gap position is stored + + MCL [8]: user_script_aligning_mot1() + Moving gap from 3 to 10 + Aligning mot1 bla bla + + MCL [9]: pos1 = PositionInstances ('oldpos1',mot1) + Initialising oldpos1 + axis names: ['mot1'] + + MCL [10]: #a slot for mot1 is created, gap is kept + + MCL [11]: pos1.show() + + oldpos1 (SELECTED) oldpos0 oldpos2 default + gap None 3.0 3.0 None + mot1 None None None None + creation_date # 2020-03-23-14:40 # 2020-03-23-14:38 # 2020-03-19-15:29 # 2020-03-19-15:29 + last_accessed # 2020-03-23-14:40 # 2020-03-23-14:39 # 2020-03-23-14:38 # 2020-03-19-15:29 + + MCL [12]: pos1.update() + updating oldpos1 with {'gap': 10.0, 'mot1': 29.0} + + MCL [13]: pos1.show() + + oldpos1 (SELECTED) oldpos0 oldpos2 default + gap 10.0 3.0 3.0 None + mot1 29.0 None None None + creation_date # 2020-03-23-14:40 # 2020-03-23-14:38 # 2020-03-19-15:29 # 2020-03-19-15:29 + last_accessed # 2020-03-23-14:42 # 2020-03-23-14:39 # 2020-03-23-14:38 # 2020-03-19-15:29 + + MCL [14]: pos0.move(gap) + Moving gap from 10 to 3 + + MCL [15]: #gap moved back to pos0 stored position + + MCL [16]: #As I dont need oldpos2 instance, I will clear it + + MCL [17]: pos0.remove_instance('oldpos2') + oldpos2 removed. + + MCL [18]: pos0.show() + + oldpos0 (SELECTED) oldpos1 default + gap 10.0 10.0 None + mot1 29.0 29.0 None + creation_date # 2020-03-23-14:38 # 2020-03-23-14:40 # 2020-03-19-15:29 + last_accessed # 2020-03-23-14:53 # 2020-03-23-14:52 # 2020-03-19-15:29 + + MCL [19]: user_script_aligning_c[ontinue() ++ + diff --git a/id26/controllers/BM23CalcCounters.py b/id26/controllers/BM23CalcCounters.py new file mode 100644 index 0000000000000000000000000000000000000000..5511bc4227da0106c884ad65324a1273bb4bedba --- /dev/null +++ b/id26/controllers/BM23CalcCounters.py @@ -0,0 +1,80 @@ + +from bliss.config import settings + +from bliss.common.scans import ct +from bliss.controllers.counter import CalcCounterController +from bliss.common.counter import IntegratingCounter + +class DarkCounter(CalcCounterController): + + def __init__(self, name, config): + + self.dark_object = config.get("openclose") + + self.background = 0.0 + CalcCounterController.__init__(self, name, config) + + self.dark_setting_name = f"{self.name}_dark_setting" + self.dark_setting = settings.HashSetting(self.dark_setting_name) + + + def get_default_chain_parameters(self, scan_params, acq_params): + self.scan_params = scan_params + return acq_params + + def is_integrating_counter(self, cnt): + info_str = cnt.__info__() + for info in info_str.split(): + if info.find("counter type") != -1: + if info.find("integrating") != -1: + return True + else: + return False + + return False + + def get_input_counter_from_tag(self, tag): + for cnt in self.inputs: + if self.tags[cnt.name] == tag: + return cnt + + return None + + def take_background(self, time=1.0): + + # Close beam + dark_object_state = self.dark_object.state() + self.dark_object.close() + + if self.dark_object.is_closed(): + scan_ct = ct(time, self.inputs, run=False) + scan_ct._data_watch_callback = None + scan_ct.run() + + data_background = scan_ct.get_data() + + for cnt in self.inputs: + tag = self.tags[cnt.name] + self.dark_setting[tag] = data_background[cnt.name][0] + + self.dark_setting['dark_time'] = time + + if dark_object_state == "OPEN": + self.dark_object.open() + + else: + print("Close functions did not succeed, Backgrounds have not been changed !!!") + + def calc_function(self, input_dict): + #print(f"\n\n---------DARK") + value = {} + for tag in input_dict.keys(): + cnt = self.get_input_counter_from_tag(tag) + if isinstance(cnt, IntegratingCounter): + #print(f"\n---- {cnt.name} -- INTEGRATING") + value[tag] = input_dict[tag] - self.dark_setting[tag] * self.scan_params["count_time"] / self.dark_setting['dark_time'] + else: + #print(f"\n---- {cnt.name} -- NOT INTEGRATING") + value[tag] = input_dict[tag] - self.dark_setting[tag] + + return value diff --git a/id26/controllers/BM23CalcCounters.py.mercredi b/id26/controllers/BM23CalcCounters.py.mercredi new file mode 100644 index 0000000000000000000000000000000000000000..b9fdf02875e94a69fc22c569d5d1438132c93313 --- /dev/null +++ b/id26/controllers/BM23CalcCounters.py.mercredi @@ -0,0 +1,78 @@ + +from bliss.config import settings +from bliss.common.scans import ct +from bliss.controllers.counter import CalcCounterController +from bliss.common.counter import IntegratingCounter + +class DarkCounter(CalcCounterController): + + def __init__(self, name, config): + + self.dark_object = config.get("openclose") + + self.background = 0.0 + CalcCounterController.__init__(self, name, config) + + self.dark_setting_name = f"{self.name}_dark_setting" + self.dark_setting = settings.HashSetting(self.dark_setting_name) + + def get_default_chain_parameters(self, scan_params, acq_params): + self.scan_params = scan_params + return acq_params + + def is_integrating_counter(self, cnt): + info_str = cnt.__info__() + for info in info_str.split(): + if info.find("counter type") != -1: + if info.find("integrating") != -1: + return True + else: + return False + + return False + + def get_input_counter_from_tag(self, tag): + for cnt in self.inputs: + if self.tags[cnt.name] == tag: + return cnt + + return None + + def take_background(self, time=1.0): + + # Close beam + dark_object_state = self.dark_object.state() + self.dark_object.close() + + if self.dark_object.is_closed(): + scan_ct = ct(time, self.inputs, run=False) + scan_ct._data_watch_callback = None + scan_ct.run() + + data_background = scan_ct.get_data() + + for cnt in self.inputs: + tag = self.tags[cnt.name] + self.dark_setting[tag] = data_background[cnt.name][0] + + self.dark_time = time + + if dark_object_state == "OPEN": + self.dark_object.open() + + else: + print("Close functions did not succeed, Backgrounds have not been changed !!!") + + def calc_function(self, input_dict): + print(f"{self.get_current_parameters()}") + value = {} + for tag in input_dict.keys(): + cnt = self.get_input_counter_from_tag(tag) + if isinstance(cnt, IntegratingCounter): + print(f"---- {cnt.name} -- INTEGRATING") + value[tag] = input_dict[tag] - self.dark_setting[tag] * self.scan_params["count_time"] / self.dark_time + else: + print(f"---- {cnt.name} -- NOT INTEGRATING") + value[tag] = input_dict[tag] - self.dark_setting[tag] + + return value diff --git a/id26/scripts/attenuator.py b/id26/controllers/attenuator.py similarity index 97% rename from id26/scripts/attenuator.py rename to id26/controllers/attenuator.py index e0fea16789467ac5e134f51b0ddb7d163209d3bb..a72b2566acd23275e68aa53473a515d1d30f32f4 100644 --- a/id26/scripts/attenuator.py +++ b/id26/controllers/attenuator.py @@ -2,7 +2,7 @@ from bliss import setup_globals from id26.scripts.beamline_parameters import ID26Parameters, switch_instance ''' -package: id26.scripts.attenuator +package: id26.controllers.attenuator name: att class: ID26attenuator wago: $wcid26c @@ -33,7 +33,8 @@ class Id26Attenuator (ID26Parameters): def __init__ (self, name, config_dict): self.wago = config_dict['wago'] self.filters = [] - + self.name = name + for f in config_dict['filters']: wagokey, desc, attenuation = f.keys() self.filters.append([f[wagokey],f[attenuation],f[desc]]) diff --git a/id26/calc_controllers/counter_offsets.py b/id26/controllers/auto_offsets.py similarity index 63% rename from id26/calc_controllers/counter_offsets.py rename to id26/controllers/auto_offsets.py index 4ca5e8d9b0336d70ed3ee0760edd714ae9baf975..0ae215218810c93bfacb156fdd227b0f6a6536fe 100644 --- a/id26/calc_controllers/counter_offsets.py +++ b/id26/controllers/auto_offsets.py @@ -1,4 +1,5 @@ from bliss import setup_globals + from id26.scripts.beamline_parameters import ID26Parameters, switch_instance @@ -6,77 +7,138 @@ from id26.scripts.beamline_parameters import ID26Parameters, switch_instance # Out [10]: {'ACQ_TRIG': ['COUNT', 'ZAP'], 'SOFT_SHUTTER': ['CLOSE', 'OPEN']} -''' -package: id26.calc_controllers.counter_offsets -class: ID26FastShutter -name: fsh -opiom: $opiom_cc1 -multiplexer: multiplexer_opiom_cc1 -''' - -class ID26FastShutter (ID26Parameters): +class ID26OffsetShutter (ID26Parameters): def __init__ (self, name, config_dict): - self.opiom = config_dict['opiom'] - self.multiplexer = config_dict['multiplexer'] + param_name = name + _def = {} + _def ['shutter'] = 'fshutter' + _cb = {} + _cb ['shutter'] = self._cb_toggle + super ().__init__(param_name, _def, callbacks = _cb) + + self.chopper = config_dict ['chopper'] + self.fshutter = config_dict ['fshutter'] + + self._cb_toggle (self.parameters.shutter) def __info__(self): - msg = "Shutter is " + self._fstate() - msg += "\nAutomatic opening during counting: " -# msg += 'ON' if self.opiom.registers()['IMA']&0xe else 'OFF' - msg += 'ON' if self.opiom.registers()['IMA']&0x2 else 'OFF' - return(msg) + return "\n"+self.parameters.__info__() + + def _cb_toggle (self, shu): + + #print ('in cb', shu) + if shu == 'chopper': + self._off_shu = ID26OffsetShutterChopper (self.chopper) + elif shu == 'fshutter': + self._off_shu = ID26OffsetShutterFShAtt (self.fshutter) + else: + print ('shutter nust be one of : [\'chopper\', \'fshutter\']') + return self.parameters.shutter + return shu - def _fstate(self): - return (self.multiplexer.getOutputStat('SOFT_SHUTTER')) + def open (self): + self._off_shu.open() + + def close (self): + self._off_shu.close() + + def state (self): + return self._off_shu.state() + #return 'OPEN' or 'CLOSE' + + def is_open (self): + return True if self._off_shu.state() == 'OPEN' else False + + def is_closed (self): + return True if self._off_shu.state() == 'CLOSE' else False + + +class ID26OffsetShutterChopper: + def __init__(self, chopper): + self.chopp = chopper + def open (self): - self.multiplexer.switch('soft_shutter','open') + print ('openning chopper ',self.chopp.name) + self.chopp.move(1) + for i in range(3): + if self.state() is not 'OPEN': + print ('openning chopper ',self.chopp.name) + self.chopp.move(1) def close (self): - self.multiplexer.switch('soft_shutter','close') - - def open_fast (self): - if self.opiom.comm_ack('IMA 0x01 0x01') != 'OK': - raise RuntimeError ('Bad answer from opiom') - - def close_fast (self): - if self.opiom.comm_ack('IMA 0x00 0x01') != 'OK': - raise RuntimeError ('Bad answer from opiom') - - def on (self): - ''' - Activates the open/close of shutter before/after counting - "IMA 0x%02x 0x0e", mode << 1 ; mode = 1 - "IMA 0x%02x 0x02", mode << 1 ; mode = 1 is enough - the opiom is not programmed for that yet !!! - ''' - if 1: - print ("Would activate the open/close of shutter before/after counting") - print ("But the OPIOM IS NOT READY FOR THAT ...") - elif self.opiom.comm_ack('IMA 0x{0:02x} 0x0e'.format(1<<1)) != 'OK': - raise RuntimeError ('Bad answer from opiom') - - def off (self): - ''' - Activates the open/close of shutter before/after counting - "IMA 0x%02x 0x0e", mode << 1 ; mode = 0 - the opiom is not programmed for that yet !!! - ''' - if 0: - if self.opiom.comm_ack('IMA 0x{0:02x} 0x0e'.format(0<<1)) != 'OK': - raise RuntimeError ('Bad answer from opiom') + + print ('closing chopper ',self.chopp.name) + self.chopp.move(0) + for i in range(3): + if self.state() is not 'CLOSE': + print ('closing chopper ',self.chopp.name) + self.chopp.move(0) + + def state (self): + #print ('chopper used') + return 'CLOSE' if self.chopp.position < 0.2 else 'OPEN' + -class ID26OffsetShutter (ID26Parameters): +class ID26OffsetShutterFShAtt: + + def __init__(self, shutter_info): + + for f in shutter_info: + self.f=f + keys = f.keys() + if 'fshutter' in keys: + self.shutter = f['fshutter'] + if 'attenuator' in keys: + self.atten = f['attenuator'] + + + def open (self): + print ('opening', self.shutter.name) + self.shutter.open() + print ('opening', self.atten.name) + self.atten.factor(63) + + def close (self): + print ('closing', self.shutter.name) + self.shutter.close() + print ('closing', self.atten.name) + self.atten.factor(0) + + def state (self): + status1 = self.shutter._fstate() + + status2 = 'CLOSE' if self.atten.factor() == 0 else 'OPEN' + status2 = 'OPEN' if self.atten.factor() == 63 else 'CLOSE' + + print (self.shutter.name, 'status:', status1) + print (self.atten.name, 'status:', status2) + + if status1 == 'CLOSE' and status2 == 'CLOSE': + return 'CLOSE' + if status1 == 'OPEN' and status2 == 'OPEN': + return 'OPEN' + return 'FAULT' + + + +class ID26OffsetShutter3: - def __init__ (self): + def __init__(self, *args, **kwargs): pass def open (self): + print ('not implemented') pass def close (self): + print ('not implemented') + pass + + def state (self): + print ('not implemented') pass @@ -97,7 +159,7 @@ config example: outputs: - name: out1 -package: id26.calc_controllers.counter_offsets +package: id26.controllers.counter_offsets class: ID26CounterOffsets name: counter_offset inputs: diff --git a/id26/controllers/calc_counters.py b/id26/controllers/calc_counters.py new file mode 100644 index 0000000000000000000000000000000000000000..e623acc30973c5d6f77a14b2be80511558346b5d --- /dev/null +++ b/id26/controllers/calc_counters.py @@ -0,0 +1,33 @@ +from bliss.controllers.counter import CalcCounterController + + +class ID26SumCounter (CalcCounterController):#, ID26Parameters): + + def __init__ (self, name, config_dict): + + CalcCounterController.__init__(self, name, config_dict) + + ''' + param_name = 'calc_counters_parameters' + defaults = {} + + ID26Parameters.__init__(self, + param_name, + defaults, + ) + ''' + + def calc_function(self, input_dict): + csum=0 + for cnt in self.inputs: + csum += input_dict[self.tags[cnt.name]] + + #csum = csum / float(len(self.inputs)) + + + return {self.tags[self.outputs[0].name]: csum} + #print ('inputs',self.inputs) + #print ('tagss',self.tags) + #print ('counter',self.counter) + #print ('outputs',self.outputs) + diff --git a/id26/controllers/fast_shutter.py b/id26/controllers/fast_shutter.py new file mode 100644 index 0000000000000000000000000000000000000000..27c51625329267e99d0441aa5c971a940b4e5d65 --- /dev/null +++ b/id26/controllers/fast_shutter.py @@ -0,0 +1,75 @@ +from bliss import setup_globals + +from id26.scripts.beamline_parameters import ID26Parameters, switch_instance + + +#TEST_ZAP [10]: multiplexer_opiom_cc1.getAllPossibleValues() +# Out [10]: {'ACQ_TRIG': ['COUNT', 'ZAP'], 'SOFT_SHUTTER': ['CLOSE', 'OPEN']} + + +''' +package: id26.controllers.fast_shutter +class: ID26FastShutter +name: fsh +opiom: $opiom_cc1 +multiplexer: multiplexer_opiom_cc1 +''' + +class ID26FastShutter (ID26Parameters): + + def __init__ (self, name, config_dict): + self.opiom = config_dict['opiom'] + self.multiplexer = config_dict['multiplexer'] + self.name = name + + + def __info__(self): + msg = "Shutter is " + self._fstate() + msg += "\nAutomatic opening during counting: " +# msg += 'ON' if self.opiom.registers()['IMA']&0xe else 'OFF' + msg += 'ON' if self.opiom.registers()['IMA']&0x2 else 'OFF' + return(msg) + + def _fstate(self): + print ('.multiplexer.getOutputStat(\'SOFT_SHUTTER\')',self.multiplexer.getOutputStat('SOFT_SHUTTER')) + return (self.multiplexer.getOutputStat('SOFT_SHUTTER')) + + def open (self): + self.multiplexer.switch('soft_shutter','open') + + def close (self): + self.multiplexer.switch('soft_shutter','close') + + def open_fast (self): + if self.opiom.comm_ack('IMA 0x01 0x01') != 'OK': + raise RuntimeError ('Bad answer from opiom') + + def close_fast (self): + if self.opiom.comm_ack('IMA 0x00 0x01') != 'OK': + raise RuntimeError ('Bad answer from opiom') + + def on (self): + ''' + Activates the open/close of shutter before/after counting + "IMA 0x%02x 0x0e", mode << 1 ; mode = 1 + "IMA 0x%02x 0x02", mode << 1 ; mode = 1 is enough + the opiom is not programmed for that yet !!! + ''' + if 1: + print ("Would activate the open/close of shutter before/after counting") + print ("But the OPIOM IS NOT READY FOR THAT ...") + elif self.opiom.comm_ack('IMA 0x{0:02x} 0x0e'.format(1<<1)) != 'OK': + raise RuntimeError ('Bad answer from opiom') + + def off (self): + ''' + Activates the open/close of shutter before/after counting + "IMA 0x%02x 0x0e", mode << 1 ; mode = 0 + the opiom is not programmed for that yet !!! + ''' + if 0: + if self.opiom.comm_ack('IMA 0x{0:02x} 0x0e'.format(0<<1)) != 'OK': + raise RuntimeError ('Bad answer from opiom') + + + diff --git a/id26/controllers/id26moco.py b/id26/controllers/id26moco.py new file mode 100644 index 0000000000000000000000000000000000000000..ce75af7f1f5301486317cfefe742044103148e09 --- /dev/null +++ b/id26/controllers/id26moco.py @@ -0,0 +1,118 @@ +import gevent +import math + +from bliss.controllers.moco import Moco +from bliss.config import static +from bliss import setup_globals + +class id26moco(Moco): + + def __init__(self, name, config_tree): + + Moco.__init__(self, name, config_tree) + self.MOCO_MAX = 2 + self.PERCENTAGE = 5 + + self.set_voltage() + + def set_voltage(self): + + self.outbeam("VOLT", "NORM", "UNIP", 10, "NOAUTO", silent=True) + self.slope = 1 + self.phase = 150 + self.tau = 0.1 + + print("MoCo box outbeam set to voltage.") + print(f"Slope set to {self.slope} and phase {self.phase}. You may check this using moco_scan(phase).") + + self.MOCO_MAX = 2 + + def moco_set_current(self): + + self.outbeam("CURR", "NORM", "UNIP", 5e-6, "NOAUTO") + self.slope = 1e-6 + self.phase = 120 + self.tau = 0.1 + + print("MoCo box outbeam set to current.") + print(f"Slope set to {self.slope} and phase {self.phase}. You may check this using moco_scan(phase).") + + self.MOCO_MAX = 1e-6 + + def start_regul(self, percent=None): + + if percent is None: + percent = self.PERCENTAGE + + self.stop() + + moco_amp = self.set_amp(percent) + + _aux = (moco_amp/(percent/100)) # this is the FWHM of the RC in units of qgths + + if _aux == 0: + print("Error in moco_set, _aux ==0 moco_slope not properly calculated") + return + else: + moco_slope = (self.MOCO_MAX/ _aux) /10 + + self.slope = moco_slope + print(f"Moco slope set to {moco_slope}.\n") + + self.go() + + print("MoCo feedback started.") + print("Waiting for feedback to reach maximum...") + + gevent.sleep(5) + + def set_amp(self, percent=None): + + if percent is None: + percent = self.PERCENTAGE + + moco_amp = self.calc_amp(percent) + self.amplitude = moco_amp + print(f"MoCo box amplitude set. (moco_amp={moco_amp}) ") + + return moco_amp + + def calc_amp(self, percent=None): + + if percent is None: + percent = self.PERCENTAGE + + energy = setup_globals.energy + g_mo_d = energy.controller.parameters.g_mo_d + + if g_mo_d > 3.0: + # energy resolution + resolution = 7000.0 + elif (1.5 < g_mo_d) and (g_mo_d < 3): + # energy resolution + resolution=35000.0 + elif g_mo_d < 1.5: + # energy resolution + resolution = 70000.0 + + ene_pos = energy.position + bragg_pos = energy.controller.calc_to_real({"calc": ene_pos})["theta"] + winkel = math.radians(bragg_pos) + + try: + delta_bragg = ((1.0 / resolution) * math.sin(winkel)) / math.cos(winkel) + print(f"delta_bragg = {delta_bragg}") + except: + raise RuntimeError(f"MOCO ({self.name}: in calc_amp: Division by 0") + + # conversion factor for qgth2 to microrad + delta_qgth2 = delta_bragg * 1000000.0 / 68.2875 + rc_fwhm = delta_bragg * 1000000.0 * math.sqrt(2.0) + moco_amp = delta_qgth2 * math.sqrt(2.0) * percent / 100.0 + + print(f"I assume a resolving power of {resolution}") + print(f"At {ene_pos} KeV ({bragg_pos} degrees) the rc FWHM is {rc_fwhm} microrad and {delta_qgth2*math.sqrt(2)} in units of qgth2.") + print(f"I determine a MoCo amplitude at {percent} percent of the FWHM, i.e. around {moco_amp}") + + return moco_amp + diff --git a/id26/scripts/mono_encoders.py b/id26/controllers/mono_encoders.py similarity index 100% rename from id26/scripts/mono_encoders.py rename to id26/controllers/mono_encoders.py diff --git a/id26/calc_controllers/monochromator.py b/id26/controllers/monochromator.py similarity index 84% rename from id26/calc_controllers/monochromator.py rename to id26/controllers/monochromator.py index 5baac389d77671d055c52d3c65fbfe556f6afd8f..042956c53344764f176408d7958b4d3ad6077376 100644 --- a/id26/calc_controllers/monochromator.py +++ b/id26/controllers/monochromator.py @@ -22,6 +22,7 @@ controller: """ import math +import numpy from bliss import setup_globals from bliss.config import settings @@ -96,6 +97,7 @@ class ID26Monochromator(CalcController, ID26Parameters): def calc_from_real(self, positions_dict): + #print (positions_dict) theta = positions_dict["theta"] self._add ('monox_oldpos', positions_dict["monox"], nosetup = True) self.parameters.monox_oldpos = positions_dict["monox"] @@ -106,11 +108,18 @@ class ID26Monochromator(CalcController, ID26Parameters): def calc_to_real(self, positions_dict): + #print (positions_dict) + #positions_dict can be an array ...so energy, theta and monox as well ... + #energy = positions_dict["calc"] + + #energy = numpy.array(positions_dict["calc"]) energy = positions_dict["calc"] + theta = self.crystal_plane.bragg_angle(energy * ur.keV).to(ur.deg).magnitude - monox = self._monox_position(energy) + #monox = self._monox_position(energy) - return {"theta": theta, "monox": monox} + #return {"theta": theta, "monox": monox} + return {"theta": theta} def _monox_dpos(self): return (self.parameters.monox_de / ( @@ -120,17 +129,33 @@ class ID26Monochromator(CalcController, ID26Parameters): def _monox_position(self, energy): # the position of monox is defined as the center of the second crystal - + # as positions_dict can be an array ...energy can as well, so _newpos and _new_pos ... + # energy IS ALWAYS AN ARRAY NOW HERE + _newpos = (self.mono_vertical_shift * energy * self.g_mo_d) / HC_OVER_E _limits = self._tagged["monox"][0].limits - if abs(self.parameters.monox_oldpos - _newpos) > self._monox_dpos(): - _new_pos = _limits[1] if _newpos > _limits[1] else _newpos - _new_pos = _limits[0] if _newpos < _limits[0] else _new_pos - return _new_pos + #print ('energy',energy) + #print ('newpos',_newpos) + + _newpos = list(map (lambda x:min(_limits[1],x),_newpos)) + _newpos = list(map (lambda x:max(_limits[0],x),_newpos)) + + _val = self._monox_dpos() + _old = self.parameters.monox_oldpos + + _newpos = list(map (lambda x:x if abs(_old-x)>_val else _old,_newpos)) + + + return numpy.array(_newpos) +''' + if (abs(self.parameters.monox_oldpos - _newpos) > self._monox_dpos()).any(): + _new_pos = list(map (lambda x:min(_limits[1],x),_newpos)) + _new_pos = list(map (lambda x:max(_limits[0],x),_new_pos)) + return numpy.array(_new_pos) else: return self.parameters.monox_oldpos - +''' """ SPEC diff --git a/id26/controllers/parameters.py b/id26/controllers/parameters.py new file mode 100644 index 0000000000000000000000000000000000000000..0a82dcb61963001677a4c1e71addc6b216975bf1 --- /dev/null +++ b/id26/controllers/parameters.py @@ -0,0 +1,19 @@ +from bliss import setup_globals +from bliss.common.standard import info +from id26.scripts.beamline_parameters import ID26Parameters, switch_instance +from id26.scripts.energy import angletoenergy, energytoangle + +class ID26Vars (ID26Parameters): + + def __init__ (self, name, config_dict): + param_name = name + + self.bli = config_dict #['bli'] + _defaults = {} + for item in config_dict['defaults']: + _defaults.update (item.items()) + + super().__init__(param_name, _defaults) + + def __info__(self): + return (info(self.parameters)) diff --git a/id26/scripts/beamcheck.py b/id26/scripts/beamcheck.py new file mode 100644 index 0000000000000000000000000000000000000000..3246c92af3fad12dced644a55839fd689b5f38c5 --- /dev/null +++ b/id26/scripts/beamcheck.py @@ -0,0 +1,22 @@ +''' +https://bliss.gitlab-pages.esrf.fr/bliss/master/scan_engine_preset.html + +To pause a scan¶ + +As Preset callbacks are executed synchronously they can easily pause it, just by not returning imediatly from prepare, start or stop callback methods. If for example you need to pause a scan if there is no beam, you just have to check in a loop the condition. As an example, wait to start a scan if the beam is not present. +''' + +class Preset(ScanPreset): + def __init__(self, diode, beam_trigger_value): + self._diode = diode + self._beam_trigger_value + + def prepare(self,scan): + beam_value = self._diode.read() + while beam_value < self._beam_trigger_value: + print("Waiting for beam") + print(f"read {beam_value} expect {self._beam_trigger_value}",end='\r') + gevent.sleep(1) + beam_value = self._diode.read() + + diff --git a/id26/scripts/beamline_parameters.py b/id26/scripts/beamline_parameters.py index e3322e4f1d1b37d275d1248a0ad625a0ee589968..09d487128e11b5d2ec58c59efbd9be2a8e8b3fee 100644 --- a/id26/scripts/beamline_parameters.py +++ b/id26/scripts/beamline_parameters.py @@ -118,14 +118,14 @@ class ID26Parameters: Allows to save in a file a parameter set """ if self.param_name != None and len(directory)>1: - file_name = prefix + self.param_name + suffix + file_name = prefix + self.param_name + suffix +'.yml' sep = '' if directory [-1] == '/' else '/' file_name = directory + sep + file_name self.parameters.to_file(file_name) @switch_instance - def reset_parameters(self): + def _reset_parameters(self): """ Deletes all parameters of the object from Redis to re-start with the default values diff --git a/id26/scripts/fscan.py b/id26/scripts/fscan.py index 09531af3a0df3553c5eac567e22ea84a58f91edb..79435dbfbcb4731017b0bd868b110bd6f5371420 100644 --- a/id26/scripts/fscan.py +++ b/id26/scripts/fscan.py @@ -2,9 +2,10 @@ import numpy import datetime import time +from bliss.scanning.scan import Scan, ScanState, DataWatchCallback from bliss.scanning.chain import AcquisitionChain, ChainPreset + from bliss.scanning.toolbox import ChainBuilder -from bliss.scanning.scan import Scan, ScanState, DataWatchCallback from bliss.scanning.acquisition.musst import MusstAcquisitionMaster, MusstAcquisitionSlave from bliss.common.motor_group import TrajectoryGroup @@ -16,7 +17,8 @@ from bliss.data.node import is_zerod from bliss import setup_globals from id26.musst_prg.zap_musst_prg import zap_musst_program_data -from id26.scripts.fscan_pm600_prog import pm600_prog_mono_trajectory, pm600_prog_white_opiom_trig +from id26.scripts.fscan_pm600_prog import pm600_prog_mono_trajectory, pm600_prog_white_opiom_trig +from id26.controllers.BM23CalcCounters import DarkCounter from id26.scripts.energy import stepstoenergy , angletoenergy #TODO from id26.opiom1_multiplexer import ZapMultiplexerPreset @@ -25,10 +27,21 @@ from id26.scripts.energy import stepstoenergy , angletoenergy #TODO define id26 global settings : zap.id_linked # if id_linked no zingzing, nscans=1 +# +# GLOBALS +# + +MONO_PM600 = setup_globals.mono +ENERGY = setup_globals.energy +ENC_PM = setup_globals.musst_enc_pm +ENC_HDH = setup_globals.musst_enc_hdh +MUSST = setup_globals.musst_cc1 +OPIOM = setup_globals.multiplexer_opiom_cc1 + + def testmc(): pass -def fscan (energy_start, energy_end, duration, energy_step, *counters, nscans=1, nintervals=10, backlash=20000, save=True ): # ID26CTL> fscan2 A[energy]-0.005 A[energy]+0.005 10 0.002 # $1 : energy_start in keV: : A[energy]-0.005 # $2 : energy_end : : A[energy]+0.005 @@ -38,14 +51,48 @@ def fscan (energy_start, energy_end, duration, energy_step, *counters, nscans=1, # $6 : [nintervals] : 10 # $7 : [backlash] : 20000 - print ("Running fscan {0}\n".format(fscan.__module__)) + +class ZapMultiplexerPreset (ChainPreset): + def __init__(self, multiplexer): + ChainPreset.__init__(self) + self.multiplexer = multiplexer + + + def prepare(self, acq_chain): + pass + + def start(self, acq_chain): + print ('OPIOM switches to ZAP mode') + self.multiplexer.switch('ACQ_TRIG','ZAP') + + def stop(self, acq_chain): + print ('OPIOM switches to COUNT mode') + self.multiplexer.switch('ACQ_TRIG','COUNT') + +def fscan (energy_start, energy_end, duration, energy_step, *counters, nscans=1, nintervals=10, backlash=20000, save=True ): + """ + Start hardware triggered time scan on detectors with MUSST card as the master, but itself triggered by the PM600 controller. + To select the detectors, enable/disable them using the ACTIVE_MG measurement group. + Args: + energy_start: + energy_end: + duration: + energy_step: + nscans: + nintervals: + backlash: + save: choose to save or not the data [default: True] + Returns: + Scan: the scan handler + """ + + print ("Running fscan {0}\n".format(fscan.__module__)) print ("Initialising {0}\n".format((energy_start, energy_end, duration, energy_step))) - energy_start, nbpoints, exp_time = fscan_initialize (energy_start, energy_end, duration, energy_step) + energy_start, nbpoints, exp_time = fscan_energy_start (energy_start, energy_end, duration, energy_step) -# print ("Initialised with energy_start {0} nbpoints {1} and exp_time {2}\n".format(energy_start, nbpoints, exp_time))) - print ("Initialised with energy_start ",energy_start,"nbpoints ",nbpoints,"and exp_time", exp_time,"\n") + print ("Initialised with energy_start: {0}, nbpoints:, {1} and exp_time: {2}\n".format(energy_start, nbpoints, exp_time)) if nscans > 1: raise RuntimeError ("nscans over 1 not implemented yet") @@ -56,24 +103,22 @@ def fscan (energy_start, energy_end, duration, energy_step, *counters, nscans=1, chain = fscan_build_acquisition_chain (exp_time, 0, nbpoints, *counters) - print ("Build chain {0}\n".format(chain)) + + print ("Built chain {0}\n".format(chain)) print ("Programming trajectories {0}\n".format((energy_start, energy_end, duration, energy_step, nscans, nintervals, backlash))) #WHITE comment below - #group = fscan_program_trajectories (energy_start, energy_end, duration, energy_step, nscans, nintervals, backlash) - + group = fscan_program_trajectories (energy_start, energy_end, duration, energy_step, nscans, nintervals, backlash) + #WHITE uncomment: - start_cmd = pm600_prog_white_opiom_trig (setup_globals.mono) + #start_cmd = pm600_prog_white_opiom_trig (MONO_PM600) #WHITE uncomment: - setup_globals.mono.controller.raw_write_read(start_cmd) #1XS2 - - - #group.move_to_start() -> unused (pm600, IDs) motors already moved at initialize phase to have precise step0/energy_start + #MONO_PM600.controller.raw_write_read(start_cmd) #1XS2 - #WHITE comment below - #REAL uncomment but then will be MOVING!!! group.move_to_end() #-> calls prepare_move(finalpos);pm600.start_trajectory;pm600.stop_trajectory; -> 1XS2 + #group.move_to_start() -> unused (pm600, IDs) motors already moved at fscan_energy_start phase to have precise step0/energy_start + #saving_parameters = setup_globals.SCAN_SAVING.get() scan_info = {'title': 'fscan ({0}, {1}, {2}, {3})'.format(energy_start, energy_end, duration, energy_step), @@ -81,7 +126,6 @@ def fscan (energy_start, energy_end, duration, energy_step, *counters, nscans=1, 'type': fscan, 'count_time': exp_time, 'data_dim': 2, - 'caramel_mou':10, } scan = Scan(chain, name='fscan', @@ -91,83 +135,95 @@ def fscan (energy_start, energy_end, duration, energy_step, *counters, nscans=1, print ("Ready to run scan {0}".format(scan._next_scan_number()+1)) + #WHITE comment below + #REAL uncomment but then will be MOVING!!! + + return scan, group + + group.move_to_end() #-> calls prepare_move(finalpos);pm600.start_trajectory;pm600.stop_trajectory; -> 1XS2 scan.run() - + fscan_back_to_idle() return scan -#NO MONO MOTION SCAN : programming empty traj with only output register to trig musst. -def fscan_time (exp_time, nbpoints, *counters, save=False ): - - print ("Running fscan_time {0}\n".format(fscan_time.__module__)) - - print ("Building acquisition chain {0}\n".format((exp_time, 0, nbpoints))) +def fscan_energy_start (energy_start, energy_end, duration, energy_step): - chain = fscan_build_acquisition_chain (exp_time, 0, nbpoints, *counters) - - print ("Build chain {0}\n".format(chain)) - - print ("Programming EMPTY trajectory\n") - - start_cmd = pm600_prog_white_opiom_trig (setup_globals.mono) + #TODO if nscans>1 add backlash to motions + #TODO if id_linked adapt energy start and end to gaps possibilities - setup_globals.mono.controller.raw_write_read (start_cmd) #1XS2 + ''' + Will move mono to energy_start, check the position it has really reached, and recalc energy_start form it + ''' + + if energy_start > energy_end: + raise RuntimeError( + "Energy must go incrementing. start ({0}) end ({1})".format(energy_start, energy_end) + ) + + if energy_start == energy_end or energy_step == 0: + raise RuntimeError( + "Rather run fscan_time." + ) - scan_info = {'title': 'fscan_time ({1}, {0})'.format(nbpoints, exp_time), - 'npoints':nbpoints, - 'type': fscan_time, - 'count_time': exp_time, - 'data_dim': 2, - } + mono_init_angle = MONO_PM600.position #in angle + mono_init_steps = float(MONO_PM600.controller.raw_write_read("1OA\r").split(':')[1]) # Out [1]: 2295137.0 - scan = Scan(chain, name='fscan', - scan_info=scan_info, - save=save, - data_watch_callback=CScanDisplay('elapsed_time') ) + #TODO check positions are within limits: bliss does it for me, but to be checked... + + #mv energy FSCAN_PAR["_ene_start"] + # means: + #TODO Move mono and ID to starting position (calculated from given energy). + # Move only mono (fscan2_mono_init() when ID not linked, fscan2_undu_init() elseway + # "1SV%i\r", FSCAN_PAR["comeback_speed"]) + # Move mono means also monox and phaseplate ...id26_newEnergy_with_monox.mac + #now MOVE MONOX and PM600 for now on - print ("Ready to run scan {0}".format(scan._next_scan_number()+1)) + ENERGY.move (energy_start) - scan.run() + step0 = float(MONO_PM600.controller.raw_write_read("1OA\r").split(':')[1]) # Out [1]: 2295137.0 + enc_pm_0 = ENC_PM.read() # Out [2]: -219960 + enc_hdh_0 = ENC_HDH.read() # Out [3]: -244601 - fscan_back_to_idle() + #QUESTION: + #uncomment when really MOVING: energy_start = stepstoenergy (step0) #this is from dial mono position, like this in fscan2.mac !!! + #OR + energy_start = ENC_HDH.energy () #this is from hdh encoder position - return scan - + if energy_start > energy_end: + raise RuntimeError( + "Energy must go incrementing. start ({0}) end ({1})".format(energy_start, energy_end) + ) + + if energy_start == energy_end or energy_step == 0: + raise RuntimeError( + "Rather run fscan_time." + ) -def fscan_back_to_idle(): - ''' - TODO - if ID_linked move back undulators to the beg of scan... - fscan2_cleanup_standard : - _mono_wait_idle_state(FSCAN_PAR["scan_duration"] + 60) - _mono_speed(FSCAN_PAR["comeback_speed"]) - END -''' + nbpoints = round((energy_end - energy_start) / (energy_step)) + exp_time = duration/nbpoints - setup_globals.mono.controller.raw_write_read("1WP11111111\r") - setup_globals.mono.controller.sock.flush() + return energy_start, nbpoints, exp_time + def fscan_build_acquisition_chain (exp_time, latency_time, nbpoints, *counters): - period = exp_time + latency_time # scan_params = {'npoints': nbpoints, # 'count_time':min(.01,exp_time), # } - + + period = exp_time + latency_time chain = AcquisitionChain() # OPIOM MULTIPLEXER PRESET - opiom1_preset = ZapMultiplexerPreset() - chain.add_preset(opiom1_preset) + chain.add_preset (ZapMultiplexerPreset(OPIOM)) # MUSST MASTER - musst_board = setup_globals.musst_cc1 - musst_clock = _musst_set_tmrcfg (musst_board,nbpoints*period) + musst_clock = _musst_set_tmrcfg (MUSST, nbpoints*period) if musst_clock is 0: raise RuntimeError ("no compatible clock frq on MUSST") @@ -179,7 +235,7 @@ def fscan_build_acquisition_chain (exp_time, latency_time, nbpoints, *counters): period = musst_period / musst_clock exp_time = musst_exposure_time / musst_clock - musst_master = MusstAcquisitionMaster(musst_board, + musst_master = MusstAcquisitionMaster (MUSST, program_data = zap_musst_program_data, program_start_name = 'FTIMESCAN', program_abort_name = 'CLEAN', @@ -188,32 +244,33 @@ def fscan_build_acquisition_chain (exp_time, latency_time, nbpoints, *counters): 'NPTS': int(nbpoints) }) - print ("MusstAcquisitionMaster : {0} / program FTIMESCAN inputs: {1}".format(musst_board.name,musst_master.vars)) + print ("MusstAcquisitionMaster : {0} / program FTIMESCAN inputs: {1}".format(MUSST.name,musst_master.vars)) # MUSST DEVICE musst_store_list=['time_start','mono_enc_start','hdh_enc_start','time_stop','mono_enc_stop','hdh_enc_stop'] - musst_device = MusstAcquisitionSlave(musst_board, + musst_device = MusstAcquisitionSlave(MUSST, store_list = musst_store_list) print ("MusstAcquisitionSlave storelist {}".format(musst_store_list)) chain.add(musst_master, musst_device) - #TODO conversions - + #TODO more conversions channel + # check channels with chain.nodes_list[0].channels[0].name + conversion = lambda data : (data / musst_clock) musst_master.add_external_channel(musst_device, 'time_start', rename='elapsed_time', conversion=conversion, dtype=numpy.float) - - offset = setup_globals.musst_enc_pm.offset_user - factor = setup_globals.musst_enc_pm.factor + + offset = ENC_PM.parameters.offset_user + factor = ENC_PM.parameters.factor conversion = lambda data : (data + offset / factor) musst_master.add_external_channel(musst_device, 'mono_enc_start', rename='mono_angle', conversion=conversion, dtype=numpy.float) - offset = setup_globals.musst_enc_hdh.offset_user - factor = setup_globals.musst_enc_hdh.factor + offset = ENC_HDH.parameters.offset_user + factor = ENC_HDH.parameters.factor conversion = lambda data : (data + offset / factor) musst_master.add_external_channel(musst_device, 'hdh_enc_start', rename='hdh_angle', conversion=conversion, dtype=numpy.float) @@ -229,18 +286,23 @@ def fscan_build_acquisition_chain (exp_time, latency_time, nbpoints, *counters): for node in builder.get_nodes_by_controller_type(CT2Controller): node.set_parameters(acq_params={"npoints":nbpoints, - "acq_mode":AcqMode.ExtGate, - "acq_expo_time":exp_time, - "acq_point_period":None, - "prepare_once":True, - "start_once":True, - }) + "acq_mode":AcqMode.ExtGate, + "acq_expo_time":exp_time, + "acq_point_period":None, + "prepare_once":True, + "start_once":True, + }) for child_node in node.children: child_node.set_parameters(acq_params={"count_time": exp_time}) chain.add (musst_master,node) + for node in builder.get_nodes_by_controller_type(DarkCounter): + + chain.add (musst_master,node) + + print (chain._tree) for node in builder.get_nodes_not_ready(): @@ -254,7 +316,6 @@ def fscan_build_acquisition_chain (exp_time, latency_time, nbpoints, *counters): return chain - def fscan_program_trajectories (energy_start, energy_end, duration, energy_step, nscans, nintervals, backlash): @@ -264,7 +325,7 @@ def fscan_program_trajectories (energy_start, energy_end, duration, energy_step, #load trajectories into the motor controller: TODO reprogram traj only if necessary, only new trajectorie.... #Si(111) - pm600_trajectory = pm600_prog_mono_trajectory (setup_globals.mono,\ + pm600_trajectory = pm600_prog_mono_trajectory (MONO_PM600,\ energy_start, energy_end, duration, \ nscans, nintervals, backlash) print (pm600_trajectory.pvt) @@ -281,194 +342,13 @@ def fscan_program_trajectories (energy_start, energy_end, duration, energy_step, raise (err) #check what traj was loaded in controller - setup_globals.mono.controller.trajectory_list(pm600_trajectory) + MONO_PM600.controller.trajectory_list(pm600_trajectory) return group -def fscan_initialize (energy_start, energy_end, duration, energy_step): - - #TODO if nscans>1 add backlash to motions - #TODO if id_linked adapt energy start and end to gaps possibilities - - if energy_start > energy_end: - raise RuntimeError( - "Energy must go incrementing. start ({0}) end ({1})".format(energy_start, energy_end) - ) - - if energy_start == energy_end or energy_step == 0: - raise RuntimeError( - "Rather run fscan_time." - ) - - nbpoints = round((energy_end - energy_start) / (energy_step)) - exp_time = duration/nbpoints - mono_init_angle = setup_globals.mono.position #in angle - mono_init_steps = float(setup_globals.mono.controller.raw_write_read("1OA\r").split(':')[1]) #in dial/steps - - #TODO check positions are within limits: I hope bliss does it for me, but to be checked... - - #mv energy FSCAN_PAR["_ene_start"] - # means: - #TODO Move mono and ID to starting position (calculated from given energy). - # Move only mono (fscan2_mono_init() when ID not linked, fscan2_undu_init() elseway - # "1SV%i\r", FSCAN_PAR["comeback_speed"]) - # Move mono means also monox and phaseplate ...id26_newEnergy_with_monox.mac - #now MOVE MONOX and PM600 for now on - setup_globals.energy.move (energy_start) #only simu motors attached ... - - step0 = float(setup_globals.mono.controller.raw_write_read("1OA\r").split(':')[1]) - #enc_pm_0 = setup_globals.musst_cc1.get_channel_by_name('encoder_pm600').value - #enc_hdh_0 = setup_globals.musst_cc1.get_channel_by_name('encoder_hdh').value - enc_pm_0 = setup_globals.musst_enc_pm.read() - enc_hdh_0 = setup_globals.musst_enc_hdh.read() - - #uncomment when really MOVING: energy_start = stepstoenergy (step0) #this is from dial mono position, like this in fscan2.mac !!! - #OR uncomment when really MOVING: energy_start = setup_globals.musst_enc_hdh.energy () #this is from hdh encoder position - - if energy_start > energy_end: - raise RuntimeError( - "Energy must go incrementing. start ({0}) end ({1})".format(energy_start, energy_end) - ) - - if energy_start == energy_end or energy_step == 0: - raise RuntimeError( - "Rather run fscan_time." - ) - - return energy_start, nbpoints, exp_time - - -def zaptime(nbpoints, exp_time, *counters, latency_time=0, save=True): - """ - Start hardware triggered time scan on detectors with MUSST card as the master. - To select the detectors, enable/disable them using the ACTIVE_MG measurement group. - Args: - nbpoints: number of acquistion points - exp_time: exposure time in second - latency_time: time to wait between 2 points [default: 0] - save: choose to save or not the data [default: True] - Returns: - Scan: the scan handler - """ - - print ("Running zaptime {0}\n".format(zaptime.__module__)) - - period = exp_time+latency_time -# scan_params={'npoints': nbpoints, -# 'count_time':min(.01,exp_time), -# } - - chain = AcquisitionChain() - - # OPIOM MULTIPLEXER PRESET - opiom1_preset = ZapMultiplexerPreset() - - chain.add_preset(opiom1_preset) - - # MUSST MASTER - musst_board = setup_globals.musst_cc1 - musst_clock = _musst_set_tmrcfg (musst_board,nbpoints*period) - - if musst_clock is 0: - return - - # round (ceil) timing here to the upper tick of the musst clock - musst_period = int(numpy.ceil(period * musst_clock)) - musst_exposure_time = int(exp_time * musst_clock) - - period = musst_period / musst_clock - exp_time = musst_exposure_time / musst_clock - - musst_master = MusstAcquisitionMaster(musst_board, - program_data = zap_musst_program_data, - program_start_name = 'FTIMESCAN', - program_abort_name = 'CLEAN', - vars={'PERIOD': musst_period, - 'ACQTIME': musst_exposure_time, - 'NPTS': int(nbpoints) - }) - - print ("MusstAcquisitionMaster : {0} / program FTIMESCAN inputs: {1}".format(musst_board.name,musst_master.vars)) - - # MUSST DEVICE - - musst_store_list=['time_start','mono_enc_start','hdh_enc_start','time_stop','mono_enc_stop','hdh_enc_stop'] - - musst_device = MusstAcquisitionSlave(musst_board, - store_list = musst_store_list) - - print ("MusstAcquisitionSlave : storelist {}".format(musst_store_list)) - - chain.add(musst_master, musst_device) - - conversion = lambda data : (data / musst_clock) - musst_master.add_external_channel(musst_device, 'time_start', rename='elapsed_time', - conversion=conversion, dtype=numpy.float) - conversion = lambda data : (data / 1) - musst_master.add_external_channel(musst_device, 'mono_enc_start', rename='mono_angle', - conversion=conversion, dtype=numpy.float) - conversion = lambda data : (data / 1) - musst_master.add_external_channel(musst_device, 'hdh_enc_start', rename='hdh_angle', - conversion=conversion, dtype=numpy.float) - - # CHAIN BUILDER for P201 counters - - builder = ChainBuilder(counters) -# builder.print_tree() - - for node in builder.get_nodes_by_controller_type(CT2Controller): - node.set_parameters(acq_params={"npoints":nbpoints, - "acq_mode":AcqMode.ExtGate, - "acq_expo_time":exp_time, - "acq_point_period":None, - "prepare_once":True, - "start_once":True, - }) - - for child_node in node.children: - child_node.set_parameters(acq_params={"count_time": exp_time}) - - chain.add (musst_master,node) - - - print (chain._tree) - - - - for node in builder.get_nodes_not_ready(): - print ("WARNING: NOT in the acquisition chain:") - print(f"-->{ node.get_repr_str() }") - for child in node.children: - print(" |") - print(f" { child.get_repr_str() }") - print("\n") - - - - saving_parameters = setup_globals.SCAN_SAVING.get() - - scan_info = {'title': 'zaptime ({0}, {1}, {2})'.format(nbpoints,exp_time,latency_time), - 'npoints':nbpoints, - 'type': zaptime, - 'count_time': exp_time, - 'data_dim': 2, - } - - scan = Scan(chain,name='zaptime', - scan_info=scan_info, - save=save, - data_watch_callback=CScanDisplay('elapsed_time') ) - - print ("Ready to run scan {0}".format(scan._next_scan_number()+1)) - - scan.run() - - return scan - - -def _musst_set_tmrcfg (musst_board,scan_duration): +def _musst_set_tmrcfg (musst_board, scan_duration): for musst_new_clock in [(50e6,'50MHZ'),(10e6,'10MHZ'),(1e6,'1MHZ'),(100e3,'100KHZ'),(10e3,'10KHZ'),(1e3,'1KHZ')]: max_duration = (2**31)/musst_new_clock[0] # 31 bit because seb is reading the musst timer in signed @@ -479,9 +359,28 @@ def _musst_set_tmrcfg (musst_board,scan_duration): return clock print ('Sorry, cannot manage a so long scan duration {0:.2f} mn, \ - please change the zaptime parameters, maximum is {1:.2f} mn'.format(scan_duration/60,max_duration/60)) + please change the fscan parameters, maximum is {1:.2f} mn'.format(scan_duration/60,max_duration/60)) + return 0 + +def fscan_back_to_idle(): + ''' + TODO + if ID_linked move back undulators to the beg of scan... + fscan2_cleanup_standard : + _mono_wait_idle_state(FSCAN_PAR["scan_duration"] + 60) + _mono_speed(FSCAN_PAR["comeback_speed"]) + END +''' + + MONO_PM600.controller.raw_write_read("1WP11111111\r") + MONO_PM600.controller.sock.flush() + + + + + class CScanDisplay(DataWatchCallback): @@ -542,21 +441,6 @@ class CScanDisplay(DataWatchCallback): return -class ZapMultiplexerPreset(ChainPreset): - def __init__(self): - ChainPreset.__init__(self) - - multiplexer = setup_globals.multiplexer_opiom_cc1 - - def prepare(self, acq_chain): - pass - - def start(self, acq_chain): - self.multiplexer.switch('ACQ_TRIG','ZAP') - - def stop(self, acq_chain): - self.multiplexer.switch('ACQ_TRIG','COUNT') - class PM600TrajectoryID26(object): @@ -604,3 +488,42 @@ so unused outside of ID control, maybe... and it will be a trajectory on ID hopefully ''' + +def zaptime(nbpoints, exp_time, latency_time, *counters, save=False): + """ + Start hardware triggered time scan on detectors with MUSST card as the master. + To select the detectors, enable/disable them using the ACTIVE_MG measurement group. + Args: + nbpoints: number of acquistion points + exp_time: exposure time in second + latency_time: time to wait between 2 points [default: 0] + save: choose to save or not the data [default: False] + Returns: + Scan: the scan handler + """ + + print ("Running zaptime {0}\n".format(zaptime.__module__)) + + chain = fscan_build_acquisition_chain (exp_time, latency_time, nbpoints, *counters) + + print ("Built chain {0}\n".format(chain)) + + scan_info = {'title': 'zaptime ({0}, {1}, {2}, {3})'.format(nbpoints,exp_time,latency_time, counters), + 'npoints':nbpoints, + 'type': zaptime, + 'count_time': exp_time, + 'data_dim': 2, + } + + scan = Scan(chain,name='zaptime', + scan_info=scan_info, + save=save, + data_watch_callback=CScanDisplay('elapsed_time') ) + + print ("Ready to run scan {0}".format(scan._next_scan_number()+1)) + + scan.run() + + return scan + + diff --git a/id26/scripts/position_instances.py b/id26/scripts/position_instances.py new file mode 100644 index 0000000000000000000000000000000000000000..651b46bf541b95f7fa61fa3c68dd5d8fcad51cd5 --- /dev/null +++ b/id26/scripts/position_instances.py @@ -0,0 +1,281 @@ +from bliss import current_session, global_map +from bliss.common.axis import Axis +from bliss.common.standard import move +from bliss.shell.cli.user_dialog import UserChoice +from bliss.shell.cli.pt_widgets import BlissDialog, button_dialog + +from id26.scripts.beamline_parameters import ID26Parameters, switch_instance + + + + +class ID26Positions(ID26Parameters): + def __init__(self, *axis_list): + param_name = current_session.name + "_positions" + positions_dict = {} + + if len(axis_list) is 0: + axis_list = list(global_map.get_axes_iter()) + + for axis in axis_list: + if isinstance(axis, Axis): + positions_dict[axis.name] = None + else: + print("{0} not an Axis in current session.".format(axis)) + print("hint: do not use string names for axis list, but objects.") + + super().__init__(param_name, positions_dict) + print("axis names: {0}".format(list(map(lambda x: x, positions_dict)))) + + def _purge(self): + """ + remove all existing instances of current bliss session saved positions from redis . + make sure that' s what you want before proceeding ... + """ + self.parameters.purge() + + @switch_instance + def update(self, *axis_list): + """ + assign current positions + """ + + parameters_dict = self.parameters.to_dict(export_properties=True) + ret = {} + if len(axis_list): + for axis in axis_list: + if isinstance(axis, Axis) and axis.name in parameters_dict: + ret[axis.name] = axis.position + else: + for name in self.parameters.to_dict(export_properties=False): + axis = current_session.config.get(name) + ret[name] = axis.position + print("updating {0} with {1}".format(self.instance_name, ret)) + + parameters_dict.update(ret) + self.parameters.from_dict(parameters_dict) + + @switch_instance + def move(self, *axis_list): + """ + move motors to stored positions + """ + motion = () + positions_dict = self.parameters.to_dict() + for axis in axis_list: + if ( + isinstance(axis, Axis) + and axis.name in positions_dict + and positions_dict[axis.name] + ): + motion += (axis, positions_dict[axis.name]) + + if len(axis_list) is 0: + print( + "You must specify the list of axis you want to move, or use .move_all()" + ) + else: + if len(motion): + move(*motion) + else: + print("nothing to move...") + + @switch_instance + def move_all(self): + """ + move all motors to stored positions + """ + motion = () + positions_dict = self.parameters.to_dict(export_properties=False) + for name in positions_dict: + axis = current_session.config.get(name) + if positions_dict[name]: + motion += (axis, positions_dict[name]) + if len(motion): + move(*motion) + else: + print("nothing to move...") + + @switch_instance + def assign(self, axis, new_position): + """ + assign a position to one particular axis (before move() call for instance) + can also be done with setup() in an interactive way + """ + parameters_dict = self.parameters.to_dict(export_properties=True) + ret = {} + if isinstance(axis, Axis) and axis.name in parameters_dict: + ret[axis.name] = new_position + parameters_dict.update(ret) + self.parameters.from_dict(parameters_dict) + else: + print( + "{0} not an axis in the saved positions list, sorry.".format( + axis.name if isinstance(axis, Axis) else axis + ) + ) + + @switch_instance + def add(self, *axis_list): + """ + add new axis to the set + """ + for axis in axis_list: + if isinstance(axis, Axis): + self.parameters.add(axis.name, None) + print("{0} added".format(axis.name)) + else: + print("{0} not an axis".format(axis)) + if len(axis_list) is 0: + print("Nothing added, please specify axis you want to add") + + @switch_instance + def remove(self, *axis_list): + """ + remove axis from the set + """ + for axis in axis_list: + if isinstance(axis, Axis): + self.parameters.remove("." + axis.name) + print("{0} removed".format(axis.name)) + else: + print("{0} not an axis".format(axis)) + if len(axis_list) is 0: + print( + "Nothing removed, please specify axis you want to be removed from the instances" + ) + + @switch_instance + def remove_instance(self, instance_name): + """ + remove instance + """ + if instance_name in self.parameters.instances: + self.parameters.remove(instance_name) + print("{0} removed.".format(instance_name)) + else: + print("{0} unknown instance ".format(instance_name)) + + @switch_instance + def show(self): + """ + Shows all positions saved in different instances + - Property attributes are identified with an # (hash) + - parameters taken from default are identified with an * (asterisk) + - responsabilty of user to clear unused instances to free redis mem + """ + + self.parameters.show_table() + + + @switch_instance + def save(self, directory, suffix=''): + """ + saves to disk a yml files with instances content + directory must be specified + """ + if self.param_name != None and len(directory)>1: + file_name = self.param_name + suffix + '.yml' + sep = '' if directory [-1] == '/' else '/' + file_name = directory + sep + file_name + self.parameters.to_file(file_name, *self.parameters.instances) + + print ('Saving instances of saved positions on: {0}'.format(file_name)) + + @switch_instance + def load(self, directory, suffix=''): + """ + loads from a previously saved file + directory must be specified + old1.load (directory) + loads only position for instance referenced by the calling object. (old1 in the example) + """ + if self.param_name != None and len(directory)>1: + file_name = self.param_name + suffix + '.yml' + sep = '' if directory [-1] == '/' else '/' + file_name = directory + sep + file_name + + #self.parameters,from_file (file_name, instance_name = self.instance_name) does not work + + with open(file_name) as file: + try: + self.parameters._from_yml(file.read(), instance_name=self.instance_name) + print ("Loading instance '{0}' of saved positions saved from '{1}'".format(self.instance_name, file_name)) + except KeyError: + print ("Can't find instance '{0}' in file '{1}'" .format(self.instance_name, file_name)) + + + +class PositionInstances(ID26Positions): + """ + + Class provinding tools to store/retrieve positions of a set of axis along other user actions. + the positions are made persistant in time, so that they can be retrieved even in case of quitting bliss session. + they belong to one particular bliss session and cannot be shared between sessions. + + Usage: + + - **user_instance = PositionInstances ('instance_name' , mot1, mot2, mot3 ...)** + loads as 'user_instance' session object any previously initialised PositionInstances with the name 'instance_name', or create it if it is the first time it is instantiated. + - if a list of axis is passed as arguments, slots are prepared for that set of axis in redis database. + - if no axis is passed, slots are prepared for all the motors of the session. + + - **user_instance.update ()** + stores in 'user_instance' object (attached to 'instance_name' in redis) the current positions of the axis in the set. + - **user_instance.update (mot1, mot2)** + stores in 'user_instance' object the current positions of the listed axis only. + - **user_instance.assign (mot3, 3.21)** + stores in 'user_instance' the specified position value for the specified axis. value can be None. + - **user_instance.setup ()** + launches a dialog window to enter positions to be stored in 'user_instance'. Position can be None + - **user_instance.move (mot3, mot4)** + move all axis listed to the position that was stored (if any) for them in 'user_instance' + - **user_instance.move_all ()** + move all axis of the set to the position that was stored (if any) for them in 'user_instance'. + - **user_instance.load (directory, suffix='')** + stores positions read from a previously saved file (see .save() below) + - directory (path) must be specified + - a suffix can be given, the filename is : {session}_positions{suffix}.yml + + The following functions act on all the position instances existing in redis database. The behaviour will be the same whatever is the session object calling it. + - **user_instance.show ()** + shows all positions sets stored among their different existing instances in redis database. It might happen that some instances are no longer useful. **note that this is the user responsibility to clear unused instances** to free redis memory with the next command. + - **user_instance.remove_instance ('unwanted_instance_name')** + remove specified instance from the database. + + - **user_instance.add (mot3, mot4, mot5)** + add some axis slots. do not forget to assign them some positions after. (.update(), .assign() or .setup()). + - **user_instance.remove (mot1, mot2)** + remove axis slots from the instances. + + - **user_instance.save (directory, suffix = '')** + saves to disk a yml files with all instances content. + - a directory must be specified. + - a suffix can be given, + - the filename is : {session}_positions{suffix}.yml + + + Example: + + pos0 = PositionsInstances('oldpos0', gap) + pos0.update() + #stores gap position + + user_script_aligning_mot1() + + pos1 = PositionsInstances('oldpos1', mot1) + #create a slot for mot1, gap slot is kept. + pos1.update() + #stores new positions for gap and mot1 + + pos0.move(gap) + #moves back gap to pos0 stored position + + user_script_aligning_continue() + + """ + + def __init__(self, instance_name, *axis): + print("Initialising {0}".format(instance_name)) + super().__init__(*axis) + self.instance_name = instance_name