diff --git a/eco/acquisition/scan.py b/eco/acquisition/scan.py index 25e6a31..38d2a50 100755 --- a/eco/acquisition/scan.py +++ b/eco/acquisition/scan.py @@ -16,6 +16,19 @@ ScanNameError = Exception( ) +class NumpyEncoder(json.JSONEncoder): + """Special json encoder for numpy types""" + + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + return json.JSONEncoder.default(self, obj) + + class Scan: def __init__( self, @@ -29,6 +42,7 @@ class Scan: checker=None, scan_directories=False, callbackStartStep=None, + callbacks_start_scan=[], checker_sleep_time=0.2, return_at_end="question", run_table=None, @@ -74,6 +88,10 @@ class Scan: self.initial_values.append(adj.get_current_value()) print("Initial value of %s : %g" % (adj.name, tv)) + if callbacks_start_scan: + for caller in callbacks_start_scan: + caller(self) + if self._run_table or self._elog: runname = os.path.basename(fina).split(".")[0] runno = int(runname.split("run")[1].split("_")[0]) @@ -246,11 +264,11 @@ class Scan: def writeScanInfo(self): if not Path(self.scan_info_filename).exists(): with open(self.scan_info_filename, "w") as f: - json.dump(self.scan_info, f, indent=4, sort_keys=True) + json.dump(self.scan_info, f, indent=4, sort_keys=True, cls=NumpyEncoder) else: with open(self.scan_info_filename, "r+") as f: f.seek(0) - json.dump(self.scan_info, f, indent=4, sort_keys=True) + json.dump(self.scan_info, f, indent=4, sort_keys=True, cls=NumpyEncoder) f.truncate() def scanAll(self, step_info=None): @@ -293,10 +311,12 @@ class Scans: default_counters=[], checker=None, scan_directories=False, + callbacks_start_scan=[], run_table=None, elog=None, ): self._run_table = run_table + self.callbacks_start_scan = callbacks_start_scan self.data_base_dir = data_base_dir scan_info_dir = Path(scan_info_dir) if not scan_info_dir.exists(): @@ -358,6 +378,7 @@ class Scans: scan_info_dir=self.scan_info_dir, checker=self.checker, scan_directories=self._scan_directories, + callbacks_start_scan=self.callbacks_start_scan, run_table=self._run_table, elog=self._elog, return_at_end=return_at_end, @@ -395,6 +416,7 @@ class Scans: scan_info_dir=self.scan_info_dir, checker=self.checker, scan_directories=self._scan_directories, + callbacks_start_scan=self.callbacks_start_scan, run_table=self._run_table, elog=self._elog, run_number=run_number, @@ -434,6 +456,7 @@ class Scans: checker=self.checker, scan_directories=self._scan_directories, return_at_end=return_at_end, + callbacks_start_scan=self.callbacks_start_scan, run_table=self._run_table, elog=self._elog, run_number=run_number, @@ -473,6 +496,7 @@ class Scans: checker=self.checker, scan_directories=self._scan_directories, return_at_end=return_at_end, + callbacks_start_scan=self.callbacks_start_scan, run_table=self._run_table, elog=self._elog, run_number=run_number, @@ -515,6 +539,7 @@ class Scans: checker=self.checker, scan_directories=self._scan_directories, return_at_end=return_at_end, + callbacks_start_scan=self.callbacks_start_scan, run_table=self._run_table, elog=self._elog, run_number=run_number, @@ -555,6 +580,7 @@ class Scans: checker=self.checker, scan_directories=self._scan_directories, return_at_end=return_at_end, + callbacks_start_scan=self.callbacks_start_scan, run_table=self._run_table, elog=self._elog, ) diff --git a/eco/aliases/aliases.py b/eco/aliases/aliases.py index 064c1bf..fc0a17e 100644 --- a/eco/aliases/aliases.py +++ b/eco/aliases/aliases.py @@ -125,7 +125,7 @@ class Namespace: def update(self, alias, channel, channeltype): assert not alias in self.aliases, f"Duplicate alias {alias} found!" - assert not channel in self.channels, f"Duplicate channel {channel} found!" + # assert not channel in self.channels, f"Duplicate channel {channel} found!" self.data.append( {"alias": alias, "channel": channel, "channeltype": channeltype} ) diff --git a/eco/bernina/bernina.py b/eco/bernina/bernina.py index 3670d35..4aaf40e 100644 --- a/eco/bernina/bernina.py +++ b/eco/bernina/bernina.py @@ -379,6 +379,21 @@ namespace.append_obj( module_name="eco.acquisition.daq_client", lazy=True, ) + + +def _append_namesace_status_to_scan(scan): + scan.scan_info["scan_parameters"]["namespace_status"] = namespace.get_status() + + +def _append_namespace_aliases_to_scan(scan): + scan.scan_info["scan_parameters"]["namespace_aliases"] = namespace.alias.get_all() + + +callbacks_start_scan = [lambda scan: namespace.init_all()] +callbacks_start_scan.append(_append_namespace_aliases_to_scan) +callbacks_start_scan.append(_append_namesace_status_to_scan) + + namespace.append_obj( "Scans", data_base_dir="scan_data", @@ -386,6 +401,7 @@ namespace.append_obj( default_counters=[daq], checker=checker, scan_directories=True, + callbacks_start_scan=callbacks_start_scan, run_table=run_table, elog=elog, name="scans", @@ -466,13 +482,13 @@ namespace.append_obj( module_name="eco.devices_general.cameras_swissfel", ) -namespace.append_obj( - "PaseShifterAramis", - "SLAAR02-TSPL-EPL", - lazy=True, - name="phase_shifter", - module_name="eco.devices_general.timing", -) +# namespace.append_obj( +# "PaseShifterAramis", +# "SLAAR02-TSPL-EPL", +# lazy=True, +# name="phase_shifter", +# module_name="eco.devices_general.timing", +# ) # will be split in permanent and temporary @@ -493,6 +509,13 @@ namespace.append_obj( name="las", module_name="eco.loptics.bernina_laser", ) +namespace.append_obj( + "IncouplingCleanBernina", + lazy=False, + name="las_inc", + module_name="eco.loptics.bernina_laser", +) + from ..elements.assembly import Assembly from ..devices_general.motors import SmaractStreamdevice diff --git a/eco/elements/assembly.py b/eco/elements/assembly.py index 8ab4f67..25bf8c0 100644 --- a/eco/elements/assembly.py +++ b/eco/elements/assembly.py @@ -5,6 +5,7 @@ from . import memory from enum import Enum import os import subprocess +from rich.progress import track class Collection: @@ -96,27 +97,57 @@ class Assembly: if view_toplevel_only: self.view_toplevel_only.append(self.__dict__[name]) - def get_status(self, base=None): + def get_status(self, base=None, verbose=True): if base is None: base = self settings = {} status_indicators = {} - for ts in self.settings_collection.get_list(): + nodet = [] + geterror = [] + for ts in track( + self.settings_collection.get_list(), + transient=True, + description="Reading settings ...", + ): # if (not (ts is self)) and hasattr(ts, "get_status"): # tstat = ts.get_status(base=base) # settings.update(tstat["settings"]) # status_indicators.update(tstat["status_indicators"]) # else: - settings[ts.alias.get_full_name(base=base)] = ts.get_current_value() - for ts in self.status_indicators_collection.get_list(): + if hasattr(ts, "get_current_value"): + try: + settings[ts.alias.get_full_name(base=base)] = ts.get_current_value() + except: + geterror.append(ts.alias.get_full_name(base=base)) + else: + nodet.append(ts.alias.get_full_name(base=base)) + for ts in track( + self.status_indicators_collection.get_list(), + transient=True, + description="Reading status indicators ...", + ): # if (not (ts is self)) and hasattr(ts, "get_status"): # tstat = ts.get_status(base=base) # status_indicators.update(tstat["settings"]) # status_indicators.update(tstat["status_indicators"]) # else: - status_indicators[ - ts.alias.get_full_name(base=base) - ] = ts.get_current_value() + if hasattr(ts, "get_current_value"): + try: + status_indicators[ + ts.alias.get_full_name(base=base) + ] = ts.get_current_value() + except: + geterror.append(ts.alias.get_full_name(base=base)) + else: + nodet.append(ts.alias.get_full_name(base=base)) + if verbose: + if nodet: + print("Could not retrieve status from: " + ", ".join(nodet)) + if geterror: + print( + "Retrieved error while running get_current_value from: " + + ", ".join(geterror) + ) return {"settings": settings, "status_indicators": status_indicators} def status(self, get_string=False): diff --git a/eco/fel/atmosphere.py b/eco/fel/atmosphere.py index b692dc3..18ed783 100644 --- a/eco/fel/atmosphere.py +++ b/eco/fel/atmosphere.py @@ -4,24 +4,74 @@ from ..elements import Assembly class BerninaEnv(Assembly): - def __init__(self,name=None): + def __init__(self, name=None): super().__init__(name=name) # self._append(DetectorPvDataStream, 'D_OSFA_IKLTK_2401_EB06501_M01_A', name='control_room_temperature') # self._append(DetectorPvDataStream, 'D_OSFA_IKLTK_2401_EB06501_M02_A', name='control_room_humidity') # self._append(DetectorPvDataStream, 'D_OSFA_IKLUM_8701_EB01904_M01_A', name='hutch_temperature_ac') # self._append(DetectorPvDataStream, 'D_OSFA_IKLUM_8701_EB01921_M01_A', name='hutch_humdity_door') - self._append(DetectorPvDataStream, 'SLAAR21-LI2C01_CH1:TEMP', name='las_sens1_temperature') - self._append(DetectorPvDataStream, 'SLAAR21-LI2C01_CH1:HUMIREL', name='las_sens1_humidity') - self._append(DetectorPvDataStream, 'SLAAR21-LI2C01_CH1:PRES', name='las_sens1_pressure') - self._append(DetectorPvDataStream, 'SLAAR21-LI2C01_CH2:TEMP', name='las_sens2_temperature') - self._append(DetectorPvDataStream, 'SLAAR21-LI2C01_CH2:HUMIREL', name='las_sens2_humidity') - self._append(DetectorPvDataStream, 'SLAAR21-LI2C01_CH2:PRES', name='las_sens2_pressure') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH1:TEMP', name='lhx_sens7_temperature') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH1:HUMIREL', name='lhx_sens7_humidity') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH1:PRES', name='lhx_sens7_pressure') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH2:TEMP', name='lhx_sens5_temperature') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH2:HUMIREL', name='lhx_sens5_humidity') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH2:PRES', name='lhx_sens5_pressure') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH3:TEMP', name='lhx_sens9_temperature') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH3:HUMIREL', name='lhx_sens9_humidity') - self._append(DetectorPvDataStream, 'SLAAR02-LI2C02_CH3:PRES', name='lhx_sens9_pressure') + self._append( + DetectorPvDataStream, + "SLAAR21-LI2C01_CH1:TEMP", + name="las_sens1_temperature", + ) + self._append( + DetectorPvDataStream, + "SLAAR21-LI2C01_CH1:HUMIREL", + name="las_sens1_humidity", + ) + self._append( + DetectorPvDataStream, "SLAAR21-LI2C01_CH1:PRES", name="las_sens1_pressure" + ) + self._append( + DetectorPvDataStream, + "SLAAR21-LI2C01_CH2:TEMP", + name="las_sens2_temperature", + ) + self._append( + DetectorPvDataStream, + "SLAAR21-LI2C01_CH2:HUMIREL", + name="las_sens2_humidity", + ) + self._append( + DetectorPvDataStream, "SLAAR21-LI2C01_CH2:PRES", name="las_sens2_pressure" + ) + self._append( + DetectorPvDataStream, + "SLAAR02-LI2C02_CH1:TEMP", + name="lhx_sens7_temperature", + ) + self._append( + DetectorPvDataStream, + "SLAAR02-LI2C02_CH1:HUMIREL", + name="lhx_sens7_humidity", + ) + self._append( + DetectorPvDataStream, "SLAAR02-LI2C02_CH1:PRES", name="lhx_sens7_pressure" + ) + self._append( + DetectorPvDataStream, + "SLAAR02-LI2C02_CH2:TEMP", + name="lhx_sens5_temperature", + ) + self._append( + DetectorPvDataStream, + "SLAAR02-LI2C02_CH2:HUMIREL", + name="lhx_sens5_humidity", + ) + self._append( + DetectorPvDataStream, "SLAAR02-LI2C02_CH2:PRES", name="lhx_sens5_pressure" + ) + self._append( + DetectorPvDataStream, + "SLAAR02-LI2C02_CH3:TEMP", + name="lhx_sens9_temperature", + ) + self._append( + DetectorPvDataStream, + "SLAAR02-LI2C02_CH3:HUMIREL", + name="lhx_sens9_humidity", + ) + self._append( + DetectorPvDataStream, "SLAAR02-LI2C02_CH3:PRES", name="lhx_sens9_pressure" + ) diff --git a/eco/fel/swissfel.py b/eco/fel/swissfel.py index 1cf0e7c..8623348 100644 --- a/eco/fel/swissfel.py +++ b/eco/fel/swissfel.py @@ -1,11 +1,12 @@ from ..elements.assembly import Assembly from ..xoptics.dcm import EcolEnergy_new -from ..elements.adjustable import Changer,spec_convenience,default_representation +from ..elements.adjustable import Changer, spec_convenience, default_representation from ..epics.adjustable import AdjustablePvEnum, AdjustablePvString, AdjustablePv from ..epics.detector import DetectorPvData from ..aliases import Alias from datetime import datetime from time import sleep +from ..detector.detectors_psi import DetectorBsStream class SwissFel(Assembly): @@ -17,7 +18,7 @@ class SwissFel(Assembly): name="aramis_pulse_energy", is_status=True, ) - self._append(UndulatorK,name='aramis_photon_energy_undulators',is_status=True) + self._append(UndulatorK, name="aramis_photon_energy_undulators", is_status=True) self._append(EcolEnergy_new, name="aramis_electron_energy_ecol", is_status=True) self._append( DetectorPvData, @@ -26,10 +27,10 @@ class SwissFel(Assembly): is_status=True, ) # self._append( - # DetectorPvData, - # "SARUN:FELPHOTENE", - # name="aramis_photon_energy", - # is_status=True, + # DetectorPvData, + # "SARUN:FELPHOTENE", + # name="aramis_photon_energy", + # is_status=True, # ) self._append( DetectorPvData, @@ -105,6 +106,31 @@ class SwissFel(Assembly): MessageBoard, name="message", is_setting=True, is_status="recursive" ) + self._append( + DetectorBsStream, + "SINLH01-DBAM010:EOM1_T1", + name="bam_injector", + is_setting=False, + ) + self._append( + DetectorBsStream, + "S10BC01-DBAM070:EOM1_T1", + name="bam_linac_70m", + is_setting=False, + ) + self._append( + DetectorBsStream, + "SARCL01-DBAM110:EOM1_T1", + name="bam_linac_110m", + is_setting=False, + ) + self._append( + DetectorBsStream, + "SARUN20-DBAM020:EOM1_T1", + name="bam_aramisund", + is_setting=False, + ) + class MessageBoard(Assembly): def __init__(self, name=None): @@ -178,7 +204,7 @@ class Message: @spec_convenience class UndulatorK(Assembly): - def __init__(self,name=None): + def __init__(self, name=None): super().__init__(name=name) self._append( DetectorPvData, @@ -188,48 +214,54 @@ class UndulatorK(Assembly): ) self.ksets = [] self.gaps = [] - for undno in [3,4,5,6,7,8,9,10,11,12,13,14,15]: - self._append(AdjustablePv,f'SARUN{undno:02d}-UIND030:K_SET',name=f'und{undno:02d}_Kset',is_setting=False,is_status=False) - self.ksets.append(self.__dict__[f'und{undno:02d}_Kset']) - self._append(AdjustablePv,f'SARUN{undno:02d}-UIND030:GAP_SP',pvreadbackname=f'SARUN{undno:02d}-UIND030:GAP-READ',accuracy=.0002,name=f'und{undno:02d}_gap',is_setting=False,is_status=False) - self.gaps.append(self.__dict__[f'und{undno:02d}_gap']) + for undno in [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]: + self._append( + AdjustablePv, + f"SARUN{undno:02d}-UIND030:K_SET", + name=f"und{undno:02d}_Kset", + is_setting=False, + is_status=False, + ) + self.ksets.append(self.__dict__[f"und{undno:02d}_Kset"]) + self._append( + AdjustablePv, + f"SARUN{undno:02d}-UIND030:GAP_SP", + pvreadbackname=f"SARUN{undno:02d}-UIND030:GAP-READ", + accuracy=0.0002, + name=f"und{undno:02d}_gap", + is_setting=False, + is_status=False, + ) + self.gaps.append(self.__dict__[f"und{undno:02d}_gap"]) self.settings_collection.append(self) self.unit = self.aramis_undulator_photon_energy.unit - def calc_new_Ksets(self,energy_target,energy_start=None): + def calc_new_Ksets(self, energy_target, energy_start=None): if not energy_start: - energy_start=self.aramis_undulator_photon_energy.get_current_value() + energy_start = self.aramis_undulator_photon_energy.get_current_value() K_start = [tks.get_current_value() for tks in self.ksets] - return [(energy_start/energy_target * (tK_start**2 + 2) - 2)**.5 for tK_start in K_start] + return [ + (energy_start / energy_target * (tK_start ** 2 + 2) - 2) ** 0.5 + for tK_start in K_start + ] def get_current_value(self): return self.aramis_undulator_photon_energy.get_current_value() - def change_energy(self,energy): + def change_energy(self, energy): vals = self.calc_new_Ksets(energy) - for kset,val in zip(self.ksets,vals): + for kset, val in zip(self.ksets, vals): kset.set_target_value(val) - sleep(.2) + sleep(0.2) for gap in self.gaps: while gap.get_change_done() == 0: - sleep(.02) - - def set_target_value(self,value,hold=False): - return Changer(target=value,parent=self,changer=self.change_energy,hold=hold,stopper=None) - - - - - - - - - - - - - - - - + sleep(0.02) + def set_target_value(self, value, hold=False): + return Changer( + target=value, + parent=self, + changer=self.change_energy, + hold=hold, + stopper=None, + ) diff --git a/eco/loptics/bernina_laser.py b/eco/loptics/bernina_laser.py index 1462b0a..1b3e7d6 100644 --- a/eco/loptics/bernina_laser.py +++ b/eco/loptics/bernina_laser.py @@ -10,6 +10,13 @@ from pint import UnitRegistry ureg = UnitRegistry() +class IncouplingCleanBernina(Assembly): + def __init__(self, name=None): + super().__init__(name=name) + self._append(SmaractStreamdevice,"SARES23-LIC13",name='tilt') + self._append(SmaractStreamdevice,"SARES23-LIC14",name='rotation') + self._append(SmaractStreamdevice,"SARES23-LIC15",name='transl_vertical') + class LaserBernina(Assembly): def __init__(self, pvname, name=None): diff --git a/eco/timing/sequencer.py b/eco/timing/sequencer.py index af5495a..1eecde5 100644 --- a/eco/timing/sequencer.py +++ b/eco/timing/sequencer.py @@ -60,13 +60,11 @@ class CtaSequencer(Assembly): DetectorPvData, self._pvstr("Ctrl-IsRunning-O"), name="is_running", - is_setting=True, ) self._append( DetectorPvData, self._pvstr("Ctrl-StartedAt-O"), name="last_start_pulse_id", - is_setting=True, ) self.event_code_sequences = {} for i, eventcode in enumerate(self.event_codes): diff --git a/eco/timing/timing_diag.py b/eco/timing/timing_diag.py index 41e30c0..a01aaf7 100644 --- a/eco/timing/timing_diag.py +++ b/eco/timing/timing_diag.py @@ -24,6 +24,8 @@ class TimetoolBerninaUSD(Assembly): processing_pipeline="SARES20-CAMS142-M5_psen_db", processing_instance="SARES20-CAMS142-M5_psen_db1", spectrometer_camera_channel="SARES20-CAMS142-M5:FPICTURE", + spectrometer_pvname="SARES20-CAMS142-M5", + microscope_pvname="SARES20-PROF141-M1", delaystage_PV="SLAAR21-LMOT-M524:MOTOR_1", pvname_mirror="SARES23-LIC9", pvname_zoom="SARES20-MF1:MOT_8", @@ -76,9 +78,9 @@ class TimetoolBerninaUSD(Assembly): ) self._append( CameraBasler, - "SARES20-PROF141-M1", + pvname=microscope_pvname, name="camera_microscope", - camserver_name = f"{name} ({pvname_camera})", + camserver_alias = f"{name} ({microscope_pvname})", is_setting=True, is_status=False, ) @@ -87,9 +89,9 @@ class TimetoolBerninaUSD(Assembly): ) self._append( CameraPCO, - "SARES20-CAMS142-M5", + pvname=spectrometer_pvname, name="camera_spectrometer", - camserver_name = f"{name} ({pvname_camera})", + camserver_alias = f"{name} ({spectrometer_pvname})", is_setting=True, is_status=False, ) diff --git a/eco/utilities/config.py b/eco/utilities/config.py index 3cd7dfc..5d4d87d 100644 --- a/eco/utilities/config.py +++ b/eco/utilities/config.py @@ -15,6 +15,8 @@ from importlib import import_module from lazy_object_proxy import Proxy as Proxy_orig from tabulate import tabulate from concurrent.futures import ThreadPoolExecutor +from tqdm import tqdm +from rich import progress import traceback @@ -299,12 +301,33 @@ class Namespace(Assembly): if raise_errors: raise expt - def init_all(self, verbose=True, raise_errors=False, max_workers=20): + def init_all( + self, verbose=False, raise_errors=False, print_summary=True, max_workers=5 + ): with ThreadPoolExecutor(max_workers=max_workers) as exc: - for name in self.all_names: - exc.submit( - self.init_name, name, verbose=verbose, raise_errors=raise_errors + # for name in self.all_names: + # exc.submit( + # self.init_name, name, verbose=verbose, raise_errors=raise_errors + # ) + list( + progress.track( + exc.map( + lambda name: self.init_name( + name, verbose=verbose, raise_errors=raise_errors + ), + self.all_names, + ), + description="Initializing ...", + total=len(self.all_names), ) + ) + + if print_summary: + print( + f"Initialized {len(self.initialized_names)} of {len(self.all_names)}." + ) + print("Failed objects: " + ", ".join(self.lazy_names)) + # if verbose: # print(("Configuring %s " % (name)).ljust(25), end="") # sys.stdout.flush() diff --git a/eco/xdiagnostics/intensity_monitors.py b/eco/xdiagnostics/intensity_monitors.py index 1bd2f45..cae7167 100755 --- a/eco/xdiagnostics/intensity_monitors.py +++ b/eco/xdiagnostics/intensity_monitors.py @@ -60,9 +60,9 @@ class SolidTargetDetectorPBPS_assi(Assembly): left="SAROP21-CVME-PBPS1:Lnk9Ch15", right="SAROP21-CVME-PBPS1:Lnk9Ch13", ), + channels_int=None, name=None, ): - print("hia----->", name) super().__init__(name=name) self.pvname = pvname self._append( diff --git a/eco/xoptics/dcm_pathlength_compensation.py b/eco/xoptics/dcm_pathlength_compensation.py index 7c34f49..93a1135 100644 --- a/eco/xoptics/dcm_pathlength_compensation.py +++ b/eco/xoptics/dcm_pathlength_compensation.py @@ -7,6 +7,8 @@ from ..elements import Assembly def energy2tthe(energy, hkl=(1, 1, 1), material=materials.Si): """calculates 2 theta angle of certain energy, given material and bragg reflection""" + if energy == 0: + return np.nan return 2 * np.arcsin( constants.h * constants.c diff --git a/startup_inline_new.py b/startup_inline_new.py index 4f3901a..9769ca4 100644 --- a/startup_inline_new.py +++ b/startup_inline_new.py @@ -81,3 +81,4 @@ from IPython import get_ipython _ipy = get_ipython() _ipy.Completer.use_jedi = False # print(arguments) +_ipy.magic("load_ext rich")