diff --git a/eco/acquisition/counters.py b/eco/acquisition/counters.py new file mode 100644 index 0000000..49e1aec --- /dev/null +++ b/eco/acquisition/counters.py @@ -0,0 +1,559 @@ + + + + + +# likely never worked ... +# def _wait_for_tasks(scan, **kwargs): +# print("checking remaining tasks from previous scan ...") +# for task in scan.remaining_tasks: +# task.join() +# print("... done.") + +class CounterStatusInitNamespaceToDAQ: + def __init__(self, namespace=None, daq=None): + self.namespace = namespace + self.daq = daq + + def callback_start_scan(self,scan=None, append_status_info=True): + if not append_status_info: + return + namespace_status = namespace.get_status(base=None) + stat = {"status_run_start": namespace_status} + scan.status = stat + + def callback_start_step(self, scan=None, append_status_info=True): + pass + def callback_end_step(self, scan=None, append_status_info=True): + pass + def callback_end_scan(self, scan=None, append_status_info=True): + pass + + + + + + def _write_namespace_status_to_scan( + scan, daq=daq, namespace=namespace, append_status_info=True, end_scan=True, **kwargs + ): + if not append_status_info: + return + if end_scan: + namespace_status = namespace.get_status(base=None) + scan.status["status_run_end"] = namespace_status + if (not end_scan) and not (len(scan.values_done) == 1): + return + if hasattr(scan, "daq_run_number"): + runno = scan.daq_run_number + else: + runno = daq.get_last_run_number() + pgroup = daq.pgroup + tmpdir = Path(f"/sf/bernina/data/{pgroup}/res/tmp/stat_run{runno:04d}") + tmpdir.mkdir(exist_ok=True, parents=True) + try: + tmpdir.chmod(0o775) + except: + pass + + statusfile = tmpdir / Path("status.json") + if not statusfile.exists(): + with open(statusfile, "w") as f: + json.dump(scan.status, f, sort_keys=True, cls=NumpyEncoder, indent=4) + else: + with open(statusfile, "r+") as f: + f.seek(0) + json.dump(scan.status, f, sort_keys=True, cls=NumpyEncoder, indent=4) + f.truncate() + print("Wrote status with seek truncate!") + if not statusfile.group() == statusfile.parent.group(): + shutil.chown(statusfile, group=statusfile.parent.group()) + + response = daq.append_aux( + statusfile.resolve().as_posix(), + pgroup=pgroup, + run_number=runno, + ) + print("####### transfer status #######") + print(response.json()) + print("###############################") + scan.scan_info["scan_parameters"]["status"] = "aux/status.json" + + +def _write_namespace_aliases_to_scan(scan, daq=daq, force=False, **kwargs): + if force or (len(scan.values_done) == 1): + namespace_aliases = namespace.alias.get_all() + if hasattr(scan, "daq_run_number"): + runno = scan.daq_run_number + else: + runno = daq.get_last_run_number() + pgroup = daq.pgroup + tmpdir = Path(f"/sf/bernina/data/{pgroup}/res/tmp/aliases_run{runno:04d}") + tmpdir.mkdir(exist_ok=True, parents=True) + try: + tmpdir.chmod(0o775) + except: + pass + aliasfile = tmpdir / Path("aliases.json") + if not Path(aliasfile).exists(): + with open(aliasfile, "w") as f: + json.dump( + namespace_aliases, f, sort_keys=True, cls=NumpyEncoder, indent=4 + ) + else: + with open(aliasfile, "r+") as f: + f.seek(0) + json.dump( + namespace_aliases, f, sort_keys=True, cls=NumpyEncoder, indent=4 + ) + f.truncate() + if not aliasfile.group() == aliasfile.parent.group(): + shutil.chown(aliasfile, group=aliasfile.parent.group()) + + scan.remaining_tasks.append( + Thread( + target=daq.append_aux, + args=[aliasfile.resolve().as_posix()], + kwargs=dict(pgroup=pgroup, run_number=runno), + ) + ) + # DEBUG + print( + f"Sending scan_info_rel.json in {Path(aliasfile).parent.stem} to run number {runno}." + ) + scan.remaining_tasks[-1].start() + # response = daq.append_aux( + # aliasfile.resolve().as_posix(), + # pgroup=pgroup, + # run_number=runno, + # ) + print("####### transfer aliases started #######") + # print(response.json()) + # print("################################") + scan.scan_info["scan_parameters"]["aliases"] = "aux/aliases.json" + + +def _message_end_scan(scan, **kwargs): + print(f"Finished run {scan.run_number}.") + if hasattr(scan, "daq_run_number"): + runno_daq_saved = scan.daq_run_number + print(f"daq_run_number is run {runno_daq_saved}.") + + try: + runno = daq.get_last_run_number() + print(f"daq last run number is run {runno}.") + except: + pass + + try: + e = pyttsx3.init() + e.say(f"Finished run {scan.run_number}.") + e.runAndWait() + e.stop() + except: + print("Audio output failed.") + + +# def _copy_scan_info_to_raw(scan, daq=daq): +# run_number = daq.get_last_run_number() +# pgroup = daq.pgroup +# print(f"Copying info file to run {run_number} to the raw directory of {pgroup}.") +# response = daq.append_aux( +# scan.scan_info_filename, pgroup=pgroup, run_number=run_number +# ) +# print(f"Status: {response.json()['status']} Message: {response.json()['message']}") + + +def _create_general_run_info(scan, daq=daq, **kwargs): + with open(scan.scan_info_filename, "r") as f: + si = json.load(f) + + info = {} + # general info, potentially automatically filled + info["general"] = {} + # individual data filled by daq/writers/user through api + info["start"] = {} + info["end"] = {} + info["steps"] = [] + + +def _copy_scan_info_to_raw(scan, daq=daq, **kwargs): + t_start = time.time() + + scan.writeScanInfo() + + # get data that should come later from api or similar. + run_directory = list( + Path(f"/sf/bernina/data/{daq.pgroup}/raw").glob(f"run{scan.run_number:04d}*") + )[0].as_posix() + with open(scan.scan_info_filename, "r") as f: + si = json.load(f) + + # correct some data in there (relative paths for now) + from os.path import relpath + + newfiles = [] + for files in si["scan_files"]: + newfiles.append([relpath(file, run_directory) for file in files]) + + si["scan_files"] = newfiles + + # save temprary file and send then to raw + if hasattr(scan, "daq_run_number"): + runno = scan.daq_run_number + else: + runno = daq.get_last_run_number() + pgroup = daq.pgroup + tmpdir = Path(f"/sf/bernina/data/{pgroup}/res/tmp/info_run{runno:04d}") + tmpdir.mkdir(exist_ok=True, parents=True) + try: + tmpdir.chmod(0o775) + except: + pass + scaninfofile = tmpdir / Path("scan_info_rel.json") + if not Path(scaninfofile).exists(): + with open(scaninfofile, "w") as f: + json.dump(si, f, sort_keys=True, cls=NumpyEncoder, indent=4) + else: + with open(scaninfofile, "r+") as f: + f.seek(0) + json.dump(si, f, sort_keys=True, cls=NumpyEncoder, indent=4) + f.truncate() + if not scaninfofile.group() == scaninfofile.parent.group(): + shutil.chown(scaninfofile, group=scaninfofile.parent.group()) + # print(f"Copying info file to run {runno} to the raw directory of {pgroup}.") + + scan.remaining_tasks.append( + Thread( + target=daq.append_aux, + args=[scaninfofile.as_posix()], + kwargs=dict(pgroup=pgroup, run_number=runno), + ) + ) + # DEBUG + print( + f"Sending scan_info_rel.json in {Path(scaninfofile).parent.stem} to run number {runno}." + ) + scan.remaining_tasks[-1].start() + # response = daq.append_aux(scaninfofile.as_posix(), pgroup=pgroup, run_number=runno) + # print(f"Status: {response.json()['status']} Message: {response.json()['message']}") + # print( + # f"--> creating and copying file took{time.time()-t_start} s, presently adding to deadtime." + # ) + + +from eco.detector import Jungfrau + + +def _copy_selected_JF_pedestals_to_raw( + scan, daq=daq, copy_selected_JF_pedestals_to_raw=True, **kwargs +): + def copy_to_aux(daq, scan): + if hasattr(scan, "daq_run_number"): + runno = scan.daq_run_number + else: + runno = daq.get_last_run_number() + + pgroup = daq.pgroup + + for jf_id in daq.channels["channels_JF"](): + jf = Jungfrau(jf_id, name="noname", pgroup_adj=config_bernina.pgroup) + print( + f"Copying {jf_id} pedestal to run {runno} in the raw directory of {pgroup}." + ) + response = daq.append_aux( + jf.get_present_pedestal_filename_in_run(intempdir=True), + pgroup=pgroup, + run_number=runno, + ) + print( + f"Status: {response.json()['status']} Message: {response.json()['message']}" + ) + print( + f"Copying {jf_id} gainmap to run {runno} in the raw directory of {pgroup}." + ) + + response = daq.append_aux( + jf.get_present_gain_filename_in_run(intempdir=True), + pgroup=pgroup, + run_number=runno, + ) + print( + f"Status: {response.json()['status']} Message: {response.json()['message']}" + ) + + if copy_selected_JF_pedestals_to_raw: + scan.remaining_tasks.append(Thread(target=copy_to_aux, args=[daq, scan])) + scan.remaining_tasks[-1].start() + + +def _increment_daq_run_number(scan, daq=daq, **kwargs): + try: + daq_last_run_number = daq.get_last_run_number() + if int(scan.run_number) is int(daq_last_run_number) + 1: + print("############ incremented ##########") + daq_run_number = daq.get_next_run_number() + else: + daq_run_number = daq_last_run_number + if int(scan.run_number) is not int(daq_run_number): + print( + f"Difference in run number between eco {int(scan.run_number)} and daq {int(daq_run_number)}: using run number {int(scan.run_number)}" + ) + if int(scan.run_number) > int(daq_run_number): + n = int(scan.run_number) - int(daq_run_number) + print("Increasing daq run_number") + for i in range(n): + rn = daq.get_next_run_number() + print(rn) + scan.daq_run_number = rn + else: + scan.daq_run_number = daq_run_number + + except Exception as e: + print(e) + + +class Monitor: + def __init__(self, pvname, start_immediately=True): + self.data = {} + self.print = False + self.pv = PV(pvname) + self.cb_index = None + if start_immediately: + self.start_callback() + + def start_callback(self): + self.cb_index = self.pv.add_callback(self.append) + + def stop_callback(self): + self.pv.remove_callback(self.cb_index) + + def append(self, pvname=None, value=None, timestamp=None, **kwargs): + if not (pvname in self.data): + self.data[pvname] = [] + ts_local = time.time() + self.data[pvname].append( + {"value": value, "timestamp": timestamp, "timestamp_local": ts_local} + ) + if self.print: + print( + f"{pvname}: {value}; time: {timestamp}; time_local: {ts_local}; diff: {ts_local-timestamp}" + ) + + +import traceback + + +def append_scan_monitors( + scan, + daq=daq, + custom_monitors={}, + **kwargs, +): + scan.monitors = {} + for adj in scan.adjustables: + try: + tname = adj.alias.get_full_name() + except Exception: + tname = adj.name + traceback.print_exc() + try: + scan.monitors[tname] = Monitor(adj.pvname) + except Exception: + print(f"Could not add CA monitor for {tname}") + traceback.print_exc() + try: + rname = adj.readback.alias.get_full_name() + except Exception: + print("no readback configured") + traceback.print_exc() + try: + scan.monitors[rname] = Monitor(adj.readback.pvname) + except Exception: + print(f"Could not add CA readback monitor for {tname}") + traceback.print_exc() + + for tname, tobj in custom_monitors.items(): + try: + if type(tobj) is str: + tmonpv = tobj + scan.monitors[tname] = Monitor(tmonpv) + print(f"Added custom monitor for {tname}") + except Exception: + print(f"Could not add custom monitor for {tname}") + traceback.print_exc() + try: + tname = daq.pulse_id.alias.get_full_name() + scan.monitors[tname] = Monitor(daq.pulse_id.pvname) + except Exception: + print(f"Could not add daq.pulse_id monitor") + traceback.print_exc() + + +def end_scan_monitors(scan, daq=daq, **kwargs): + for tmon in scan.monitors: + scan.monitors[tmon].stop_callback() + + monitor_result = {tmon: scan.monitors[tmon].data for tmon in scan.monitors} + + ####### + # get data that should come later from api or similar. + run_directory = list( + Path(f"/sf/bernina/data/{daq.pgroup}/raw").glob(f"run{scan.run_number:04d}*") + )[0].as_posix() + + # correct some data in there (relative paths for now) + from os.path import relpath + + # save temprary file and send then to raw + if hasattr(scan, "daq_run_number"): + runno = scan.daq_run_number + else: + runno = daq.get_last_run_number() + pgroup = daq.pgroup + tmpdir = Path(f"/sf/bernina/data/{pgroup}/res/tmp/info_run{runno:04d}") + tmpdir.mkdir(exist_ok=True, parents=True) + try: + tmpdir.chmod(0o775) + except: + pass + scanmonitorfile = tmpdir / Path("scan_monitor.pkl") + if not Path(scanmonitorfile).exists(): + with open(scanmonitorfile, "wb") as f: + pickle.dump(monitor_result, f) + + print(f"Copying monitor file to run {runno} to the raw directory of {pgroup}.") + response = daq.append_aux( + scanmonitorfile.as_posix(), pgroup=pgroup, run_number=runno + ) + print(f"Status: {response.json()['status']} Message: {response.json()['message']}") + + # scan.monitors = None + + +def _init_all(scan, append_status_info=True, **kwargs): + if not append_status_info: + return + namespace.init_all(silent=False) + + +callbacks_start_scan = [] +callbacks_start_scan.append(_init_all) +callbacks_start_scan.append(_wait_for_tasks) +callbacks_start_scan.append(_append_namesace_status_to_scan) +callbacks_start_scan.append(_increment_daq_run_number) +callbacks_start_scan.append(append_scan_monitors) +callbacks_end_step = [] +callbacks_end_step.append(_copy_scan_info_to_raw) +callbacks_end_step.append(_write_namespace_aliases_to_scan) +callbacks_end_step.append( + lambda scan, daq=daq, namespace=namespace, append_status_info=True, end_scan=True, **kwargs: _write_namespace_status_to_scan( + scan, + daq=daq, + namespace=namespace, + append_status_info=append_status_info, + end_scan=False, + **kwargs, + ) +) +callbacks_end_scan = [] +callbacks_end_scan.append(_write_namespace_status_to_scan) +callbacks_end_scan.append(_copy_scan_info_to_raw) +callbacks_end_scan.append( + lambda scan, daq=daq, force=True, **kwargs: _write_namespace_aliases_to_scan( + scan, daq=daq, force=force, **kwargs + ) +) +callbacks_end_scan.append(_copy_selected_JF_pedestals_to_raw) +callbacks_end_scan.append(end_scan_monitors) +callbacks_end_scan.append(_message_end_scan) + +# >>>> Extract for run_table and elog + + +# if self._run_table or self._elog: +def _create_metadata_structure_start_scan( + scan, run_table=run_table, elog=elog, append_status_info=True, **kwargs +): + runname = os.path.basename(scan.fina).split(".")[0] + runno = int(runname.split("run")[1].split("_")[0]) + metadata = { + "type": "scan", + "name": runname.split("_", 1)[1], + "scan_info_file": scan.scan_info_filename, + } + for n, adj in enumerate(scan.adjustables): + nname = None + nId = None + if hasattr(adj, "Id"): + nId = adj.Id + if hasattr(adj, "name"): + nname = adj.name + + metadata.update( + { + f"scan_motor_{n}": nname, + f"from_motor_{n}": scan.values_todo[0][n], + f"to_motor_{n}": scan.values_todo[-1][n], + f"id_motor_{n}": nId, + } + ) + if np.mean(np.diff(scan.pulses_per_step)) < 1: + pulses_per_step = scan.pulses_per_step[0] + else: + pulses_per_step = scan.pulses_per_step + metadata.update( + { + "steps": len(scan.values_todo), + "pulses_per_step": pulses_per_step, + "counters": [daq.name for daq in scan.counterCallers], + } + ) + + try: + try: + metadata.update({"scan_command": get_ipython().user_ns["In"][-1]}) + except: + print("Count not retrieve ipython scan command!") + + message_string = f"#### Run {runno}" + if metadata["name"]: + message_string += f': {metadata["name"]}\n' + else: + message_string += "\n" + + if "scan_command" in metadata.keys(): + message_string += "`" + metadata["scan_command"] + "`\n" + message_string += "`" + metadata["scan_info_file"] + "`\n" + elog_ids = elog.post( + message_string, + Title=f'Run {runno}: {metadata["name"]}', + text_encoding="markdown", + ) + scan._elog_id = elog_ids[1] + metadata.update({"elog_message_id": scan._elog_id}) + metadata.update( + {"elog_post_link": scan._elog.elogs[1]._log._url + str(scan._elog_id)} + ) + except: + print("Elog posting failed with:") + traceback.print_exc() + if not append_status_info: + return + d = {} + ## use values from status for run_table + try: + status = scan.status["status_run_start"] + d = status["settings"] + d.update(status["status"]) + except: + print("Tranferring values from status to run_table did not work") + t_start_rt = time.time() + try: + run_table.append_run(runno, metadata=metadata, d=d) + except: + print("WARNING: issue adding data to run table") + print(f"RT appending: {time.time()-t_start_rt:.3f} s") + + +# <<<< Extract for run table and elog +callbacks_start_scan.append(_create_metadata_structure_start_scan) \ No newline at end of file diff --git a/eco/acquisition/daq_client.py b/eco/acquisition/daq_client.py index 5983aa9..bd091da 100644 --- a/eco/acquisition/daq_client.py +++ b/eco/acquisition/daq_client.py @@ -87,6 +87,8 @@ class Daq(Assembly): @pgroup.setter def pgroup(self, value): + if isinstance(self._pgroup, Adjustable): + return self._pgroup.set_target_value().wait() self._pgroup = value def acquire(self, file_name=None, Npulses=100, acq_pars={}): @@ -376,3 +378,12 @@ def validate_response(resp): # } # r = requests.post(f'{broker_address}/retrieve_from_buffers',json=parameters, timeout=TIMEOUT_DAQ).json() + + + + +#### >>> TODO implment >>> + + + +#### <<< TODO implment <<< diff --git a/eco/acquisition/scan.py b/eco/acquisition/scan.py index 200e6ae..cb9b9c5 100755 --- a/eco/acquisition/scan.py +++ b/eco/acquisition/scan.py @@ -139,6 +139,7 @@ class Scan: def doNextStep(self, step_info=None, verbose=True): # for call in self.callbacks_start_step: # call() + t_step_start = time() if self.checker: first_check = time() checker_unhappy = False @@ -159,10 +160,14 @@ class Scan: ) self.checker.clear_and_start_counting() + + if self.callbacks_start_step: for caller in self.callbacks_start_step: caller(self, **self.callbacks_kwargs) + dt_callbacks_step_start = time()-t_step_start + if not len(self.values_todo) > 0: return False values_step = self.values_todo[0] @@ -171,16 +176,20 @@ class Scan: "Starting scan step %d of %d" % (self.nextStep + 1, len(self.values_todo) + len(self.values_done)) ) - ms = [] + fina = self.get_filename(self.nextStep) + t_adj_start = time() + ms = [] for adj, tv in zip(self.adjustables, values_step): ms.append(adj.set_target_value(tv)) for tm in ms: tm.wait() + dt_adj = time()-t_adj_start # settling sleep(self.settling_time) + t_ctr_start = time() readbacks_step = [] adjs_name = [] adjs_offset = [] @@ -225,26 +234,42 @@ class Scan: for ta in acs: ta.wait() filenames.extend(ta.file_names) + dt_ctr = time() - t_ctr_start + if verbose: print("Done with acquisition") + ### >>> Callback end + t_callbacks_step_end = time() if self.checker: if not self.checker.stop_and_analyze(): return True if callable(step_info): tstepinfo = step_info() else: - tstepinfo = step_info + tstepinfo = {} self.values_done.append(self.values_todo.pop(0)) self.pulses_done.append(self.pulses_per_step.pop(0)) self.readbacks.append(readbacks_step) + + if self.callbacks_end_step: + for caller in self.callbacks_end_step: + caller(self, **self.callbacks_kwargs) + dt_callbacks_step_end = time() - t_callbacks_step_end + ### <<<< Callback end + + tstepinfo['times'] = { + "callbacks_step_start":dt_callbacks_step_start, + "adjustables" : dt_adj, + "counters" : dt_ctr, + "callbacks_step_end": dt_callbacks_step_end, + } + self.appendScanInfo( values_step, readbacks_step, step_files=filenames, step_info=tstepinfo ) self.writeScanInfo() - if self.callbacks_end_step: - for caller in self.callbacks_end_step: - caller(self, **self.callbacks_kwargs) + self.nextStep += 1 return True diff --git a/eco/acquisition/utilities.py b/eco/acquisition/utilities.py index ca1bb67..8d7b8eb 100644 --- a/eco/acquisition/utilities.py +++ b/eco/acquisition/utilities.py @@ -38,7 +38,7 @@ class Acquisition: if self._thread.ident is None: return "waiting" else: - if self._thread.isAlive(): + if self._thread.is_alive(): return "acquiring" else: return "done" diff --git a/eco/bernina/bernina.py b/eco/bernina/bernina.py index 0216781..59dcbb1 100644 --- a/eco/bernina/bernina.py +++ b/eco/bernina/bernina.py @@ -1239,6 +1239,8 @@ namespace.append_obj( # module_name="eco.acquisition.epics_data", # ) + +## TODO pgroup non adjustable/dynamically changeable! namespace.append_obj( "Scans", name="scans_epics", @@ -1289,6 +1291,8 @@ namespace.append_obj( lazy=True, ) + +#TODO: need to check if the value property actually works here for the pgroup in the run table to make is dynamic! namespace.append_obj( "Run_Table2", name="run_table", @@ -1870,6 +1874,7 @@ namespace.append_obj( name="checker", ) +# TODO resove scans pgroup sensitivity! Clearly non dynamic. namespace.append_obj( "Scans", data_base_dir="scan_data", @@ -2191,25 +2196,51 @@ class Incoupling(Assembly): def __init__(self, name=None): super().__init__(name=name) self._append(SmaractRecord, "SARES23-LIC:MOT_13", name="ry", is_setting=True) - self._append(SmaractRecord, "SARES23-LIC:MOT_16", name="rx", is_setting=True) + self._append(SmaractRecord, "SARES23-USR:MOT_4", name="rx", is_setting=True) self._append(SmaractRecord, "SARES23-LIC:MOT_15", name="y", is_setting=True) self._append(MotorRecord, "SARES20-MF2:MOT_5", name="x",is_setting=True) - self._append(SmaractRecord, "SARES23-USR:MOT_2", name="focus_pos",is_setting=True) + self._append(SmaractRecord, "SARES23-USR:MOT_3", name="eos_focus",is_setting=True) + + self._append(AnalogOutput, 'SLAAR21-LDIO-LAS6991:DAC06_VOLTS',name='eos_fb_rx', is_setting=True) + self._append(AnalogOutput, 'SLAAR21-LDIO-LAS6991:DAC05_VOLTS',name='eos_fb_ry', is_setting=True) + + self._append(AnalogOutput, 'SLAAR21-LDIO-LAS6991:DAC09_VOLTS',name='nir_mirr1_ry', is_setting=True) + self._append(AnalogOutput, 'SLAAR21-LDIO-LAS6991:DAC10_VOLTS',name='nir_mirr1_rx', is_setting=True) + + self._append(AnalogOutput, 'SLAAR21-LDIO-LAS6991:DAC11_VOLTS',name='nir_mirr2_ry', is_setting=True) + self._append(AnalogOutput, 'SLAAR21-LDIO-LAS6991:DAC12_VOLTS',name='nir_mirr2_rx', is_setting=True) + + self._append( + AdjustablePv, + pvsetname="SLAAR21-LCAM-C561:FIT2_REQUIRED.PROC", + name="eos_fb_setpoint_rq", + accuracy=1, + is_setting=True, + ) + self._append( + AdjustablePv, + pvsetname="SLAAR21-LCAM-C561:FIT2_DEFAULT.PROC", + name="eos_fb_setpoint_df", + accuracy=1, + is_setting=True, + ) + self._append( + AdjustablePv, + pvsetname="SLAAR21-LTIM01-EVR0:CALCW.A", + name="eos_fd_enable", + accuracy=1, + is_setting=True, + ) try: self.motor_configuration_thorlabs = { - "polarizer": { - "pvname": "SLAAR21-LMOT-ELL3", - }, - "hwp": { - "pvname": "SLAAR21-LMOT-ELL5", - }, - "block": { + "nir_block": { "pvname": "SLAAR21-LMOT-ELL2", }, - "nd_filter": { + "eos_block": { "pvname": "SLAAR21-LMOT-ELL4", - }, + } + } ### thorlabs piezo motors ### @@ -2222,6 +2253,18 @@ class Incoupling(Assembly): ) except Exception as e: print(e) + # self._append(AdjustableVirtual, + # [self.crystal, self.hwp], + # self.thz_pol_get, + # self.thz_pol_set, + # name="thz_polarization", + # ) + + # def thz_pol_set(self, val): + # return 1.0 * val, 1.0 / 2 * val + + # def thz_pol_get(self, val, val2): + # return 1.0 * val2 namespace.append_obj( Incoupling, @@ -2394,6 +2437,39 @@ namespace.append_obj( # THz, # lazy=True, # name="thz", +# self._append( +# AdjustableVirtual, +# [self.crystal_ROT, self.thz_wp], +# self.thz_pol_get, +# self.thz_pol_set, +# name="", +# ) +# # self.thz_polarization = AdjustableVirtual( +# # [self.crystal_ROT, self.thz_wp], +# # self.thz_pol_get, +# # self.thz_pol_set, +# # name="thz_polarization", +# # ) +# self._append( +# AdjustableVirtual, +# [self.delay_thz, self.delay_800_pump], +# self.delay_get, +# self.delay_set, +# name="combined_delay", +# ) + +# # self.combined_delay = AdjustableVirtual( +# # [self.delay_thz, self.delay_800_pump], +# # self.delay_get, +# # self.delay_set, +# # name="combined_delay", +# # ) + +# def thz_pol_set(self, val): +# return 1.0 * val, 1.0 / 2 * val + +# def thz_pol_get(self, val, val2): +# return 1.0 * val2 # ) # class THz_in_air(Assembly): @@ -2775,6 +2851,7 @@ namespace.append_obj( # try to append pgroup folder to path !!!!! This caused eco to run in a timeout without error traceback !!!!! +# TODO pgroup non dynamic here! try: import sys from ..utilities import TimeoutPath @@ -2906,7 +2983,7 @@ class IlluminatorsLasers(Assembly): self._append( MpodChannel, pvbase="SARES21-PS7071", - channel_number=5, + channel_number=3, name="illumination_inline", ) self._append( diff --git a/eco/dbase/archiver.py b/eco/dbase/archiver.py index 5e0c228..50f2d9c 100644 --- a/eco/dbase/archiver.py +++ b/eco/dbase/archiver.py @@ -1,4 +1,4 @@ -from data_api import get_data, search +# from data_api import get_data, search from ..epics.detector import DetectorPvDataStream from fnmatch import translate import datetime, dateutil diff --git a/eco/devices_general/cameras_swissfel.py b/eco/devices_general/cameras_swissfel.py index 2421150..4d32f9e 100644 --- a/eco/devices_general/cameras_swissfel.py +++ b/eco/devices_general/cameras_swissfel.py @@ -324,7 +324,8 @@ class CameraBasler(Assembly): camserver_alias=camserver_alias, camserver_group=camserver_group, name="config_cs", - is_display=False, + is_display="recursive", + is_setting=True ) self.config_cs.set_alias() diff --git a/eco/devices_general/motors.py b/eco/devices_general/motors.py index 1b8f3e4..aaecdac 100755 --- a/eco/devices_general/motors.py +++ b/eco/devices_general/motors.py @@ -1283,12 +1283,15 @@ class MotorRecord(Assembly): if not step_value: step_value = pv.get() print(f"Tweaking {self.name} at step size {step_value}", end="\r") + start_value = self.get_current_value() + # help = "q = exit; up = step*2; down = step/2, left = neg dir, right = pos dir\n" + # help = help + "g = go abs, s = set" help = "q = exit; up = step*2; down = step/2, left = neg dir, right = pos dir\n" - help = help + "g = go abs, s = set" + help = help + "g = go abs, s = start value, r = reset current value to" print(f"tweaking {self.name}") print(help) - print(f"Starting at {self.get_current_value()}") + print(f"Starting at {start_value}") step_value = float(step_value) oldstep = 0 k = KeyPress() @@ -1323,6 +1326,9 @@ class MotorRecord(Assembly): pvf.put(1) elif k.isl(): pvr.put(1) + elif k.iskey("s"): + self.set_target_value(start_value) + p.print(value=self.get_current_value()) elif k.iskey("g"): print("enter absolute position (char to abort go to)") sys.stdout.flush() @@ -1333,7 +1339,7 @@ class MotorRecord(Assembly): except: print("value cannot be converted to float, exit go to mode ...") sys.stdout.flush() - elif k.iskey("s"): + elif k.iskey("r"): print("enter new set value (char to abort setting)") sys.stdout.flush() v = sys.stdin.readline() @@ -1341,15 +1347,17 @@ class MotorRecord(Assembly): v = float(v[0:-1]) self.reset_current_value_to(v) except: - print("value cannot be converted to float, exit go to mode ...") + print("value cannot be converted to float, exit reset mode ...") sys.stdout.flush() elif k.isq(): break else: print(help) self.clear_value_callback(index=ind_callback) + print("\r", end="") print(f"final position: {self.get_current_value()}") print(f"final tweak step: {pv.get()}") + # print('\033[K',"the info",sep='',flush=True) def tweak(self, *args, **kwargs): return self._tweak_ioc(*args, **kwargs) diff --git a/eco/devices_general/powerbrick.py b/eco/devices_general/powerbrick.py index e7e9034..1cf8758 100644 --- a/eco/devices_general/powerbrick.py +++ b/eco/devices_general/powerbrick.py @@ -49,6 +49,8 @@ class PowerBrickComm: parstring.strip("\n") setstring = parstring + "=" + str(value) return self.pbsshcom.iawrite(setstring) + + # def set_hmz class PowerBrickChannelPars(Assembly): diff --git a/eco/devices_general/powerbrick_parameters.py b/eco/devices_general/powerbrick_parameters.py index 7ea1720..b749163 100644 --- a/eco/devices_general/powerbrick_parameters.py +++ b/eco/devices_general/powerbrick_parameters.py @@ -40,6 +40,7 @@ pars_StatusMotorListCtrl = { ("EncLoss", "r"), ("HomeInProgress", "r"), ("HomeComplete", "r"), + ("HomeOffset", "r"), ("TriggerMove", "r"), ("DacLimit", "r"), ("SoftLimit", "r"), diff --git a/eco/devices_general/powersockets.py b/eco/devices_general/powersockets.py index cb318e5..1ecfe0b 100644 --- a/eco/devices_general/powersockets.py +++ b/eco/devices_general/powersockets.py @@ -118,7 +118,10 @@ class MpodChannel(Assembly): self.pvbase+f':{self._module_string}_CH{self.channel_number}_RMP_DOWN_RATE_SP', pvlowlimname = self.pvbase+f':{self._module_string}_CH{self.channel_number}_RMP_DOWN_RATE_SP.LOPR', pvhighlimname = self.pvbase+f':{self._module_string}_CH{self.channel_number}_RMP_DOWN_RATE_SP.HOPR', - name='ramp_down', is_setting=True) + name='ramp_down', is_setting=True) + self._append(AdjustablePv, + self.pvbase+f':{self._module_string}_CH{self.channel_number}_MEAS_OUT_A', + name='current', is_setting=False, is_display=True) self._append(MpodStatus,self.pvbase, self.channel_number, self._module_string, name='flags') def get_current_value(self,*args,**kwargs): diff --git a/eco/devices_general/utilities.py b/eco/devices_general/utilities.py index c5572e4..fc640b9 100644 --- a/eco/devices_general/utilities.py +++ b/eco/devices_general/utilities.py @@ -8,6 +8,7 @@ class Changer: self._changer = changer self._stopper = stopper self._thread = PropagatingThread(target=self._changer, args=(target,)) + # self._thread = Thread(target=self._changer, args=(target,)) if not hold: self._thread.start() diff --git a/eco/devices_general/wago.py b/eco/devices_general/wago.py index 0c35248..63714a6 100644 --- a/eco/devices_general/wago.py +++ b/eco/devices_general/wago.py @@ -139,6 +139,34 @@ class AnalogOutput(Assembly): is_setting=False, is_display=False, ) + self._append( + AdjustablePv, + self.pvname + ".AOFF", + name="_adj_offset", + is_setting=True, + is_display=False, + ) + self._append( + AdjustablePv, + self.pvname + ".ASLO", + name="_adj_slope", + is_setting=True, + is_display=False, + ) + self._append( + AdjustablePv, + self.pvname + ".EOFF", + name="linear_calibration_offset", + is_setting=True, + is_display=False, + ) + self._append( + AdjustablePv, + self.pvname + ".ESLO", + name="linear_calibration_slope", + is_setting=True, + is_display=False, + ) def get_current_value(self): return self.value.get_current_value() diff --git a/eco/elements/acquisition.py b/eco/elements/acquisition.py new file mode 100644 index 0000000..6181f03 --- /dev/null +++ b/eco/elements/acquisition.py @@ -0,0 +1,46 @@ + + + +# single acquisition class +class Acquisition: + def __init__( + self, + parent=None, + acquire=lambda: None, + acquisition_kwargs={}, + hold=True, + stopper=None, + get_result=lambda: None, + ): + self.acquisition_kwargs = acquisition_kwargs + for key, val in acquisition_kwargs.items(): + self.__dict__[key] = val + self._stopper = stopper + self._get_result = get_result + if acquire: + self.set_acquire_foo(acquire, hold=hold) + + def set_acquire_foo(self, acquire, hold=True): + self._acquire = acquire + self._thread = PropagatingThread(target=self._acquire) + if not hold: + self._thread.start() + + def wait(self): + self._thread.join() + return self._get_result() + + def start(self): + self._thread.start() + + def status(self): + if self._thread.ident is None: + return "waiting" + else: + if self._thread.is_alive(): + return "acquiring" + else: + return "done" + + def stop(self): + self._stopper() diff --git a/eco/elements/adjustable.py b/eco/elements/adjustable.py index dbd7b40..18f038b 100644 --- a/eco/elements/adjustable.py +++ b/eco/elements/adjustable.py @@ -82,7 +82,7 @@ def spec_convenience(Adj): try: self._currentChange = self.set_target_value(value) self._currentChange.wait() - except KeyboardInterrupt: + except (KeyboardInterrupt,SystemExit): self._currentChange.stop() return self._currentChange @@ -101,9 +101,12 @@ def spec_convenience(Adj): self._currentChange = self.set_target_value( value + startvalue, *args, **kwargs ) + print("spec conven") self._currentChange.wait() - except KeyboardInterrupt: + + except (KeyboardInterrupt,SystemExit): self._currentChange.stop() + return self._currentChange Adj.mv = mv @@ -292,7 +295,7 @@ def update_changes(Adj): cb_id = self.add_value_callback(cbfoo) self._currentChange = self.set_target_value(value) self._currentChange.wait() - except KeyboardInterrupt: + except (KeyboardInterrupt, SystemExit): self._currentChange.stop() print(f"\nAborted change at (~) {self.get_current_value():1.5g}") finally: @@ -641,14 +644,15 @@ class AdjustableVirtual: ) def check_target_value_within_limits(self, value): - in_lims = [] + in_lims = [True] values = self._foo_set_target_value_current_value(value) if not hasattr(values, "__iter__"): values = (values,) for val, adj in zip(values, self._adjustables): lim_low, lim_high = adj.get_limits() - in_lims.append((lim_low < val) and (val < lim_high)) + if not val is None: + in_lims.append((lim_low < val) and (val < lim_high)) return all(in_lims) def reset_current_value_to(self, value): @@ -847,7 +851,7 @@ class Tweak: self.thread = None def print(self): - if self.thread and self.thread.isAlive(): + if self.thread and self.thread.is_alive(): return else: self.thread = Thread(target=self.print_foo) diff --git a/eco/elements/memory.py b/eco/elements/memory.py index b48b532..bbeffde 100644 --- a/eco/elements/memory.py +++ b/eco/elements/memory.py @@ -7,8 +7,8 @@ from tabulate import tabulate import sys, colorama try: from inspect import getargspec -except: - from inspect import getfullargspec +except: # for python 3.12 + from inspect import getfullargspec as getargspec import eco from ansi2html import Ansi2HTMLConverter diff --git a/eco/elements/protocols.py b/eco/elements/protocols.py index 9f4dc73..e0fabfc 100644 --- a/eco/elements/protocols.py +++ b/eco/elements/protocols.py @@ -28,3 +28,17 @@ class ValueUpdateMonitorable(Protocol): class InitialisationWaitable(Protocol): def _wait_for_initialisation(self): ... + +@runtime_checkable +class Counter: + def acquire(self): + ... + def start(self): + ... + def stop(self): + ... + + + # file_name=fina, Npulses=self.pulses_per_step[0], acq_pars=acq_pars): + ) + \ No newline at end of file diff --git a/eco/endstations/bernina_diffractometers.py b/eco/endstations/bernina_diffractometers.py index 4c158fc..59bda3d 100644 --- a/eco/endstations/bernina_diffractometers.py +++ b/eco/endstations/bernina_diffractometers.py @@ -431,14 +431,6 @@ class GPS(Assembly): append_diffractometer_modules(self, configuration) - if configuration.diffcalc(): - self._append( - Crystals, - diffractometer_you=self, - name="diffcalc", - is_setting=False, - is_display=False, - ) for jf_id, jf_name in configuration.jfs(): self._append( Jungfrau, @@ -468,6 +460,14 @@ class GPS(Assembly): is_setting=False, is_display=True, ) + if configuration.diffcalc(): + self._append( + Crystals, + diffractometer_you=self, + name="diffcalc", + is_setting=False, + is_display=False, + ) if recspace_conv is not None: module_name, Conv_name = recspace_conv.split(":") diff --git a/eco/endstations/bernina_robots.py b/eco/endstations/bernina_robots.py index ad13b46..ea967cf 100644 --- a/eco/endstations/bernina_robots.py +++ b/eco/endstations/bernina_robots.py @@ -145,6 +145,13 @@ class StaeubliTx200(Assembly): try: import bernina_urdf self._urdf = bernina_urdf.models.Tx200_Ceiling(jf_id=robot_config.jf_id()) + if robot_config.vis_gps(): + try: + self._urdf.__dict__['gps'] = bernina_urdf.models.GPS() + from epics import PV + self._gps_motors = [PV(f'SARES22-GPS:MOT_{m}.RBV') for m in ['TX', 'TY', 'RX', 'NY_RY2TH', 'MY_RYTH', 'HEX_TX', 'HEX_RX']] + except Exception as e: + print(f"Adding visual model of GPS failed with {e}") self._append( AdjustableFS, f"/sf/bernina/config/eco/reference_values/robot_auto_update_simulation.json", @@ -603,10 +610,13 @@ class StaeubliTx200(Assembly): def show(self): self._urdf.sim.show() + if 'gps' in self._urdf.__dict__.keys(): + self._urdf.sim.vis.add(self._urdf.__dict__['gps']) ######## Helper functions ########## def _auto_updater_simulation(self): # while True: + gpss = None js = np.array( [ self._cache["pos"][k] @@ -616,11 +626,22 @@ class StaeubliTx200(Assembly): if np.any([j is None for j in js]): time.sleep(1) continue + if "_gps_motors" in self.__dict__.keys(): + try: + gpss = np.array([m.value if not 'MOT_RX.RBV' in m.pvname else np.arctan(m.value / 1146) * 180 / np.pi for m in self._gps_motors ]) + except Exception as e: + print(e) if self.auto_update_simulation(): + change = False if not np.all(js.round(3) == self._urdf.sim.pos.round(3)): self._urdf.sim.pos = js - if self._urdf.sim._vis_running(): - self._urdf.sim.vis.step(0) + change = True + if (gpss is not None)&('gps' in self._urdf.__dict__.keys()): + if not np.all(gpss.round(3) == self._urdf.gps.sim.pos.round(3)): + self._urdf.gps.sim.pos = gpss + change = True + if self._urdf.sim._vis_running() and change: + self._urdf.sim.vis.step(0) time.sleep(0.05) def _get_on_poll_info(self): diff --git a/eco/endstations/bernina_sample_environments.py b/eco/endstations/bernina_sample_environments.py index f961da2..e7b1033 100644 --- a/eco/endstations/bernina_sample_environments.py +++ b/eco/endstations/bernina_sample_environments.py @@ -2,6 +2,7 @@ from eco.xoptics.attenuator_safety_bernina import AttenuatorSafetyBernina from scipy import constants from eco.devices_general.powersockets import MpodChannel from eco.devices_general.wago import AnalogOutput +from eco.devices_general.cameras_swissfel import CameraBasler from eco.epics.detector import DetectorPvDataStream import sys @@ -88,18 +89,55 @@ class THzVirtualStages(Assembly): self.offset_mirr_z.mv(self._mz()) self.offset_par_z.mv(self._pz()) +class THz_cameras(Assembly): + def __init__(self, name=None, camera_config={}): + super().__init__(name=name) + for name, cfg in camera_config.items(): + self._append( + CameraBasler, + cfg["pvname"], + camserver_alias = "THC_" + name, + name=name, + is_setting=True, + is_display="recursive", + ) + self.__dict__[name].serial_no.mv(cfg["serial_number"]) class High_field_thz_chamber(Assembly): def __init__( self, + delay_offset_detector = None, + thc_x_adjustable=None, name=None, configuration=[], illumination_mpod=None, helium_control_valve=None, ): super().__init__(name=name) + self.delay_offset_detector = delay_offset_detector + self._thc_x_adjustable = thc_x_adjustable self.par_out_pos = [-20, -9.5] + self.camera_configuration = { + "inline": { + "pvname": "SARES20-CAMS142-M2", + "serial_number": 23067644, + }, + "sideview_45": { + "pvname": "SARES20-CAMS142-M3", + "serial_number": 23075971, + }, + } self.motor_configuration = { + "inline_mirror": { + "id": "SARES23-USR:MOT_1", + "pv_descr": "THz Chamber Inline_Mirror", + "direction": 1, + "sensor": 1, + "speed": 250, + "offset": 2.150241, + "home_direction": "back", + "kwargs": {"accuracy": 0.01}, + }, "rx": { # "id": "SARES23-USR:MOT_13", "id": "SARES23-USR:MOT_16", @@ -194,6 +232,14 @@ class High_field_thz_chamber(Assembly): "home_direction": "forward", }, } + + ### Cameras ### + self._append( + THz_cameras, + name="camera", + camera_config=self.camera_configuration, + ) + ### lakeshore temperatures #### self._append( AdjustablePv, @@ -219,20 +265,7 @@ class High_field_thz_chamber(Assembly): name="temp_gishield", is_setting=False, ) - ### in vacuum smaract motors ### - # for name, config in self.motor_configuration.items(): - # if "kwargs" in config.keys(): - # tmp_kwargs = config["kwargs"] - # else: - # tmp_kwargs = {} - # self._append( - # SmaractStreamdevice, - # pvname=Id + config["id"], - # name=name, - # is_setting=True, - # **tmp_kwargs, - # ) - ### in vacuum smaract motors ### + for name, config in self.motor_configuration.items(): self._append( SmaractRecord, @@ -255,6 +288,40 @@ class High_field_thz_chamber(Assembly): name="delay_x_center", ) + self._append( + MotorRecord, + pvname="SLAAR21-LMOT-M522:MOTOR_1", + name="delaystage_thz", + is_setting=True, + is_display=False, + is_status=True, + ) + self._append( + DelayTime, + self.delaystage_thz, + name="delay_thz", + offset_detector=self.delay_offset_detector, + is_setting=False, + is_display=True, + is_status=True, + ) + if self._thc_x_adjustable is not None: + + def movexcomp(x): + delay = self.delay_thz.get_current_value() + dx = x - self._thc_x_adjustable.get_current_value() + new_delay = delay + dx / 1000 / constants.c + return x, new_delay + + self._append( + AdjustableVirtual, + [self._thc_x_adjustable, self.delay_thz], + lambda x, delay_thz: x, + movexcomp, + name="thcx_delaycomp", + is_setting=False, + ) + if "cube" in configuration: for name, config in self.motor_configuration_cube.items(): self._append( diff --git a/eco/endstations/hexapod.py b/eco/endstations/hexapod.py index 70a29c6..70477da 100644 --- a/eco/endstations/hexapod.py +++ b/eco/endstations/hexapod.py @@ -139,7 +139,7 @@ class HexapodPI(Assembly): ), reset_current_value_to=False, change_simultaneously=False, - check_limits=False, + check_limits=True, append_aliases=True, unit="mm", name="y", diff --git a/eco/epics/adjustable.py b/eco/epics/adjustable.py index 41e3017..c6860fb 100644 --- a/eco/epics/adjustable.py +++ b/eco/epics/adjustable.py @@ -198,6 +198,7 @@ class AdjustablePv: """ 0: moving 1: move done""" change_done = 1 if self.accuracy is not None: + if ( np.abs( self.get_current_value(readback=False) @@ -221,9 +222,8 @@ class AdjustablePv: ) self._pv.put(value) - time.sleep(0.1) while self.get_change_done() == 0: - time.sleep(0.1) + time.sleep(0.01) def set_target_value(self, value, hold=False): """Adjustable convention""" diff --git a/eco/epics/detector.py b/eco/epics/detector.py index bd31bfe..78fb03f 100644 --- a/eco/epics/detector.py +++ b/eco/epics/detector.py @@ -48,8 +48,12 @@ class DetectorPvData(Assembly): else: return self.readback.get_current_value() - # def get_current_value_callback(self): - # pass + def get_current_value_callback(self, foo='accumulate',collector = [], run_once=True, print_output=False): + if hasattr(self, "_pv"): + return CallbackEpics(self,self._pv,foo=foo,collector=collector, run_once=run_once, print_output=print_output) + # else: + # raise Exception('the object does not have a _pv') + def __call__(self): return self.get_current_value() @@ -249,3 +253,40 @@ class DetectorPvDataStream(Assembly): def get_current_value(self, **kwargs): return self._pv.get(**kwargs) + +class CallbackEpics: + def __init__(self,pv,foo='accumulate',collector = [], run_once=True, print_output=False): + self.pv = pv + if collector is not None: + self.data = collector + + if foo=='accumulate': + foo = self.accumulate_values + self.foo = foo + self.run_once = run_once + self.print = print_output + + def start(self): + self.cb_index = self.pv.add_callback(self.foo,run_once=True,) + + def stop(self): + self.pv.remove_callback(self.cb_index) + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def accumulate_values(self, pvname=None, value=None, timestamp=None, **kwargs): + # if not self.data: + # self.data = [] + ts_local = time() + self.data.append( + {"value": value, "timestamp_ioc": timestamp, "timestamp_local": ts_local} + ) + if self.print: + print( + f"{pvname}: {value}; time_ioc: {timestamp}; time_local: {ts_local}; diff: {ts_local-timestamp}" + ) diff --git a/eco/loptics/bernina_laser.py b/eco/loptics/bernina_laser.py index 1a98f85..30d05b0 100644 --- a/eco/loptics/bernina_laser.py +++ b/eco/loptics/bernina_laser.py @@ -1120,6 +1120,12 @@ class LaserBernina(Assembly): name="delaystage_m2", is_setting=True, ) + self._append( + MotorRecord, + "SLAAR21-LMOT-M533:MOT", + name="hwp_compressor", + is_setting=True, + ) self._append( DelayTime, self.delaystage_pump, @@ -1301,14 +1307,14 @@ class PositionMonitors(Assembly): # is_display="recursive", # is_status=True, # ) - # self._append( - # CameraPositionMonitor, - # "SLAAR21-LCAM-CS841", - # # name="table2_position", - # name="timing_drift", - # is_display="recursive", - # is_status=True, - # ) + self._append( + CameraPositionMonitor, + "SLAAR21-LCAM-CS841", + # name="table2_position", + name="timing_drift", + is_display="recursive", + is_status=True, + ) self._append( CameraPositionMonitor, "SLAAR21-LCAM-C561", diff --git a/eco/timing/event_timing_new_new.py b/eco/timing/event_timing_new_new.py index 861849e..ee27758 100644 --- a/eco/timing/event_timing_new_new.py +++ b/eco/timing/event_timing_new_new.py @@ -248,12 +248,33 @@ class MasterEventSystem(Assembly): def __repr__(self): return self.status(printit=False) +class EvrSequencer(Assembly): + def __init__(self, pv_base, name=None): + super().__init__(name=name) + try: + self._append(AdjustablePvEnum, pv_base + ':SEQ_SOURCE', name='source', is_display=True, is_setting=True) + self._append(AdjustablePvEnum, pv_base + ':SEQ_SNUMPD', name='pulser_number', is_display=True, is_setting=True) + self._append(DetectorPvData, pv_base + ':SEQ_RUNNING', name='is_running', is_display=True) + self._append(AdjustablePvEnum, pv_base + ':Seq-Ena-Sel', name='enabled', is_display=True, is_setting=True) + self._append(DetectorPvData, pv_base + ':SEQ_SELECT_FREQ', name='frequency', is_display=True) + except: + print(f'The evr sequencer of {pv_base} is likely old type') + self._append(AdjustableMemory, None, name='frequency', is_display=True) + + self._append(AdjustablePvEnum, pv_base + ':Seq-RunMode-Sel', name='mode', is_display=True, is_setting=True) + + + self._append(AdjustablePv, pv_base + ':SEQ_DELAY', name='delay', is_display=True, is_setting=True) + self._append(AdjustablePv, pv_base + ':SEQ_REPS', name='repetitions', is_display=True, is_setting=True) + self._append(AdjustablePv, pv_base + ':SEQ_MULTIPLIER', name='freq_multiplier', is_display=True, is_setting=True) + class EvrPulser(Assembly): - def __init__(self, pv_base, event_master, name=None): + def __init__(self, pv_base, event_master, parent_evr=None, name=None): super().__init__(name=name) self.pv_base = pv_base self._event_master = event_master + self._parent_evr = parent_evr self._append( AdjustablePvString, pv_base + "-Name-I", name="description", is_display=True @@ -329,10 +350,13 @@ class EvrPulser(Assembly): @property def _eventcode(self): - try: - return self._event_master.event_codes[self.eventcode.get_current_value()] - except KeyError: - return None + if self.eventcode.get_current_value() == 27: + return self._parent_evr.sequencer + else: + try: + return self._event_master.event_codes[self.eventcode.get_current_value()] + except KeyError: + return None class DummyPulser(Assembly): @@ -527,12 +551,17 @@ class EventReceiver(Assembly): ): super().__init__(name=name) self.pvname = pvname + + self._append(EvrSequencer,self.pvname,name='sequencer', is_display=True, is_setting=True) + + pulsers = [] for n in range(n_pulsers): self._append( EvrPulser, f"{self.pvname}:Pul{n}", event_master, + parent_evr=self, name=f"pulser{n}", is_setting=True, is_display=False, @@ -564,6 +593,8 @@ class EventReceiver(Assembly): # to._pulsers = self.pulsers self.outputs = outputs + + self._append( AdjustablePv, self.pvname + ":SYSRESET", diff --git a/eco/timing/timetool_online_helper.py b/eco/timing/timetool_online_helper.py index 3c45368..2e51c0b 100644 --- a/eco/timing/timetool_online_helper.py +++ b/eco/timing/timetool_online_helper.py @@ -11,12 +11,13 @@ plt.ion() class TtProcessor: - def __init__(self, Nbg=10, channel_proj = "SARES20-CAMS142-M5.roi_signal_x_profile"): + def __init__(self, Nbg=10, ref_code=25, channel_proj = "SARES20-CAMS142-M5.roi_signal_x_profile"): self.channel_proj = channel_proj self.bg = deque([], Nbg) self.sig = deque([], 1) self.pos = deque([], 1) self.amp = deque([], 1) + self.ref_code=ref_code self.spec_ppd = deque([], 1) self.accumulator = Thread(target=self.run_continuously) self.accumulator.start() @@ -41,7 +42,7 @@ class TtProcessor: codes = m.data.data["SAR-CVME-TIFALL5:EvtSet"].value if codes is None: continue - is_reference = codes[25] == 1 + is_reference = codes[self.ref_code] == 1 try: if (lastgoodix - ix) > 1: print(f"missed {lastgoodix-ix-1} events!") diff --git a/eco/timing/timing_diag.py b/eco/timing/timing_diag.py index d66aac2..5d4a173 100644 --- a/eco/timing/timing_diag.py +++ b/eco/timing/timing_diag.py @@ -178,14 +178,12 @@ class TimetoolBerninaUSD(Assembly): AdjustablePv, pvsetname="SLAAR21-LFEEDBACK1:TARGET1", name="feedback_setpoint", - accuracy=10, is_setting=True, ) self._append( AdjustablePv, pvsetname="SLAAR21-LFEEDBACK1:ENABLE", name="feedback_enabled", - accuracy=10, is_setting=True, ) self._append( diff --git a/eco/utilities/config.py b/eco/utilities/config.py index 59b0b3e..bc60d47 100644 --- a/eco/utilities/config.py +++ b/eco/utilities/config.py @@ -540,6 +540,11 @@ class Namespace(Assembly): print(f"Initialisation took {time()-starttime} seconds") if (not silent) and print_times: + try: + from collections import Iterable + except: + import collections.abc + collections.Iterable = collections.abc.Iterable from ascii_graph import Pyasciigraph gr = Pyasciigraph() diff --git a/eco/utilities/recspace.py b/eco/utilities/recspace.py index f9bc03f..f449f14 100644 --- a/eco/utilities/recspace.py +++ b/eco/utilities/recspace.py @@ -397,18 +397,35 @@ class DiffGeometryYou(Assembly): ### use robot motors if robot is in config if cfg.robot(): + self._append( + AdjustableFS, + f"/photonics/home/gac-bernina/eco/configuration/crystals/move_robot", + name="move_robot", + default_value=True, + is_setting=False, + ) + def rob_get(a): return a + def rob_set(a): + if self.move_robot(): + return [a] + else: + return None + gam_rob = AdjustableVirtual([self.diffractometer.gamma_robot],rob_get, rob_set, name="gamma_robot", check_limits=True) + del_rob = AdjustableVirtual([self.diffractometer.delta_robot],rob_get, rob_set, name="delta_robot", check_limits=True) + get_lims = lambda a: a + gam_rob.get_limits = get_lims(self.diffractometer.gamma_robot.get_limits) + del_rob.get_limits = get_lims(self.diffractometer.delta_robot.get_limits) adjustables_dict.update( { - "gamma": self.diffractometer.gamma_robot, - "delta": self.diffractometer.delta_robot, + "gamma": gam_rob, + "delta": del_rob, } ) ### add the phi constraint to thc as phi_wobble if thc is in config if cfg.thc(): - import eco.bernina as b - - thc = b.__dict__["thc"] - thc.phi_wobble = self.constraints.phi + def phi_wobble_get(a): return a + def phi_wobble_set(a): return [a] + self.diffractometer.thc._append(AdjustableVirtual, [self.constraints.phi], phi_wobble_get, phi_wobble_set, name='phi_wobble') if cfg.kappa(): adjs = ["gamma", "mu", "delta", "eta_kap", "kappa", "phi_kap"] diff --git a/eco/utilities/utilities.py b/eco/utilities/utilities.py index b39654b..40f71ec 100644 --- a/eco/utilities/utilities.py +++ b/eco/utilities/utilities.py @@ -1,4 +1,4 @@ -from time import sleep +from time import sleep, time import sys, select from threading import Thread @@ -32,9 +32,9 @@ class TimeoutPath: def __str__(self) -> str: return str(self._path) - class PropagatingThread(Thread): def run(self): + run_start = time() self.exc = None try: if hasattr(self, "_Thread__target"): @@ -44,6 +44,7 @@ class PropagatingThread(Thread): ) else: self.ret = self._target(*self._args, **self._kwargs) + except BaseException as e: self.exc = e diff --git a/utilities_test.py b/utilities_test.py new file mode 100644 index 0000000..b39654b --- /dev/null +++ b/utilities_test.py @@ -0,0 +1,177 @@ +from time import sleep +import sys, select +from threading import Thread + +from concurrent.futures import ThreadPoolExecutor, TimeoutError +from pathlib import Path +from typing import Any +import numpy as np +import matplotlib.pyplot as plt +from numbers import Number + +class TimeoutPath: + executor = ThreadPoolExecutor(max_workers=1) + + def __init__(self, *args, timeout: float = 1, **kwargs): + self._path = Path(*args, **kwargs) + self.timeout = timeout + + def exists(self) -> bool: + future = TimeoutPath.executor.submit(self._path.exists) + try: + return future.result(self.timeout) + except TimeoutError: + return False + + def get_path(self) -> Path: + return self._path + + def __getattr__(self, name: str) -> Any: + return getattr(self._path, name) + + def __str__(self) -> str: + return str(self._path) + + +class PropagatingThread(Thread): + def run(self): + self.exc = None + try: + if hasattr(self, "_Thread__target"): + # Thread uses name mangling prior to Python 3. + self.ret = self._Thread__target( + *self._Thread__args, **self._Thread__kwargs + ) + else: + self.ret = self._target(*self._args, **self._kwargs) + except BaseException as e: + self.exc = e + + def join(self): + super(PropagatingThread, self).join() + if self.exc: + raise self.exc + return self.ret + +def isiter(a): + try: + iter(a) + return True + except TypeError: + return False + +def roundto(v,interval): + return np.rint(v/interval)*interval + +def linlog_intervals(*args, verbose=True, plot=False): + """Get linearly and logarithmically spaced arrays from providing limits and intervals or number of intervals. + Example usages: + linlog_intervals(-1e-12,('lin',.1e-12),2e-12,('log',2e-12),1e-6,('lin',4),5e-6) + + Args: + *args : limits and specifications of intervals, + limits are numbers, + specifications tuples of strings 'lin' or 'log' + and the definition if interval. + A integer is interpretet as number of intervals within the limits, + a float is interpreted asinterval size, + where in log definition, the size of the first (smallest) interval is matched. + verbose (bool, optional): _description_. Defaults to True. + plot (bool, optional): _description_. Defaults to False. + + Raises: + Exception: _description_ + Exception: _description_ + Exception: _description_ + + Returns: + numpy ndarray: 1D array of intervals. + """ + limits = [] + idefs = [] + last_lim = False + for arg in args: + if not last_lim and isinstance(arg,Number): + limits.append(arg) + last_lim = True + elif last_lim and isiter(arg): + idefs.append(arg) + last_lim = False + else: + raise Exception('Limits need to follow interval description and vice versa') + if verbose: + print(limits,idefs) + if not len(limits)==len(idefs)+1: + raise Exception('need exactly one more limit than interval definitions!') + + a = [] + for i,idef in enumerate(idefs): + tlims = limits[i:i+2] + if np.diff(tlims)<=0: + raise Exception('number limits should increasing!') + if isinstance(a,np.ndarray): + if np.isclose(a[-1],tlims[0]): + a = a[:-1] + a = [a] + + + if idef[0] == 'lin': + if type(idef[1]) is int: + a.append(np.linspace(*tlims,idef[1]+1)) + if type(idef[1]) is float: + a.append(np.arange(tlims[0],tlims[1]+idef[1],idef[1])) + if verbose: + print(f'From {tlims[0]:5g} to {a[-1][-1]:5g}: {len(a[-1])-1} linear intervals of {np.mean(np.diff(a[-1])):5g} size') + if plot: + plt.plot(np.arange(len(np.hstack(a))-len(a[-1]), len(np.hstack(a))),a[-1],'db', mfc='none') + if idef[0] == 'log': + tlims = np.asarray(tlims) + if type(idef[1]) is int: + a.append(np.logspace(*np.log10(tlims),idef[1]+1)) + if type(idef[1]) is float: + intlog = np.log10(tlims[0]+idef[1])-np.log10(tlims[0]) + a.append(10**np.arange(np.log10(tlims[0]),np.log10(tlims[1]),intlog)) + if verbose: + intervals = np.diff(np.log10(a[-1])) + + print(f'From {tlims[0]:5g} to {a[-1][-1]:5g}: {len(a[-1])-1} logarithmic intervals between {np.min(intervals):5g} and {np.max(intervals):5g} in sizes.') + if plot: + plt.plot(np.arange(len(np.hstack(a))-len(a[-1]), len(np.hstack(a))),a[-1],'or',mfc='none') + + if verbose: + if not np.isclose(a[-1][-1],tlims[1]): + print(f' NB: given limit {tlims[1]:5g} computes off by {tlims[1]-a[-1][-1]:5g} from last element!') + + + + a = np.hstack(a) + if plot: + plt.plot(np.arange(len(a)),a,'.k') + if verbose: + print(f'{len(a)} elements in total.') + return a + + + + + +# TODO + +# _wait_strs = '\|/-\|/-' + +# class WaitInput: +# def __init__(self,text,wait_time=5,update_interval=1): +# self.text = text +# self.wait_time=wait_time + + +# def start(self): +# resttime = self.wait_time +# while resttime>0: +# print(f"You have {resttime} seconds to answer!") +# i, o, e = select.select( [sys.stdin], [], [], 2 ) + +# if (i): +# print("You said", sys.stdin.readline().strip()) +# else: +# print("You said nothing!")