fab.beamtime
1import os 2import pandas as pd 3from typing import Protocol 4from dataclasses import dataclass 5import glob 6import seaborn as sns 7 8from .settings import cfg 9 10import logging 11logger = logging.getLogger(__name__) 12tele_log = logging.getLogger('fab.telemetry') 13 14 15try: 16 import scicat 17except ImportError: 18 ScicatClient = None 19 20def autodetect_beamtime(): 21 ''' Attemps to autodetect the beamtime number from the current working directory.''' 22 try: 23 return int(os.getcwd().split('/')[7]) 24 except IndexError as e: 25 raise ValueError("Could not autodetect beamtime number from current working directory") from e 26 27def beamtime_basepath(beamtime_num): 28 basepath = f"/asap3/*flash*/gpfs/*/*/data/{beamtime_num}" 29 basepath = glob.glob(basepath)[0] 30 31 return basepath 32 33def _mark_uniques(styler, col): 34 """ Marks each unique entry in a column with a different color """ 35 df = styler.data 36 if col in df.columns: 37 types = df[col][::-1].unique() 38 palette = dict(zip(types, sns.color_palette(n_colors=len(types)).as_hex())) # Reverse order so that oldest runs get always the same color 39 palette = { k: v+"55" for k ,v in palette.items() } # Add alpha channel 40 styler.applymap(lambda x: f"background-color: {palette[x]}", subset=[col]) 41 42class Beamtime: 43 """ Represents a beamtime at the FLASH facility 44 45 Properties: 46 num: The beamtime number 47 basepath: The basepath of the beamtime 48 runs: The fablive run table as a pandas DataFrame 49 pruns: The pretty-printed run table 50 """ 51 52 def __init__(self, beamtime_num): 53 self.num = beamtime_num 54 self.basepath = beamtime_basepath(beamtime_num) 55 56 if scicat is not None: 57 self.scicat = scicat.ScicatClient(beamtime_num, self.basepath) 58 else: 59 self.scicat = None 60 61 @staticmethod 62 def _make_runs_dataframe(datasets): 63 """ Creates a pandas DataFrame from a list of scicat datasets """ 64 65 def process_dataset(dset): 66 """ Processes a single dataset """ 67 daq_run = int(dset['pid'].split('/')[-1]) 68 try: 69 fablive = dset['scientificMetadata']['fablive'] 70 except KeyError: 71 return None 72 return { 'daq_run': daq_run, **fablive } 73 74 processed = [row for dset in datasets if (row := process_dataset(dset))] 75 if len(processed) != len(datasets): 76 logger.warn(f"{len(datasets) - len(processed)} datasets were skipped due to missing fablive metadata") 77 return pd.DataFrame(processed).set_index('daq_run').sort_index(ascending=False) 78 79 def styler(self, df) -> pd.DataFrame: 80 """ Applies a default style to the run table returned by `pruns` 81 82 You can alter the style by changing the parameters in the 83 config, or you can monkeypatch this method to fully customize 84 the style. 85 """ 86 styler = df.style 87 styler.applymap_index(lambda x: "font-family: monospace; font-weight: bold;") 88 89 # Color unique entries differently for selected columns 90 for col in cfg.btm.styler.mark_unique: 91 _mark_uniques(styler, col) 92 93 styler.format(na_rep="", precision=3) 94 95 if cfg.btm.styler.hide_run_thr is not False: 96 styler.hide(subset=styler.data.index < cfg.btm.styler.hide_run_thr, axis=0) 97 98 # Mark aborted runs 99 if 'aborted' in df.columns and cfg.btm.styler.mark_aborted: 100 styler.applymap(lambda x: _striped_background if x is True else "", subset=['aborted']) 101 styler.apply(lambda x: ['opacity:0.55']*len(x) if x.aborted is True else [""]*len(x), axis=1) 102 103 # Mark in progress runs 104 if cfg.btm.styler.mark_in_progress: 105 styler.hide(subset=['in_progress'], axis=1) 106 styler.format(_inprogress_animation_formatter, subset=(styler.data['in_progress'] == True, 'shot_count')) 107 108 if cfg.btm.styler.shot_count_hist: 109 styler.bar(subset=['shot_count'], color='#6495ED40', vmin=0, height=65, width=90, props='width: 60px;') 110 111 #set max column width and ellipsize text 112 if cfg.btm.styler.collapse_cols: 113 styler.set_properties(**{'max-width': cfg.btm.styler.max_col_width, 114 'text-align': 'left', 115 'white-space': 'nowrap', 116 'overflow': 'hidden', 117 'text-overflow': 'ellipsis'}) 118 119 # Expand on hover 120 if cfg.btm.styler.show_on_hover: 121 styler.set_table_styles([{'selector': 'td:hover', 122 'props': [('max-width', cfg.btm.styler.max_col_width), 123 ('white-space', 'normal'), 124 ('overflow', 'visible')]}]) 125 126 return styler 127 128 @property 129 def pruns(self) -> pd.DataFrame.style: 130 """ Pretty-printed run table """ 131 tele_log.info("pruns") 132 return self.styler(self.runs) 133 134 @property 135 def runs(self) -> pd.DataFrame: 136 """ Retrives the fablive run table from SciCat as a pandas DataFrame """ 137 assert self.scicat is not None, "ScicatClient not available" 138 139 datasets = self.scicat.get_all_datasets() 140 return self._make_runs_dataframe(datasets) 141 142 @property 143 def raw_runs(self) -> pd.DataFrame: 144 """ Retrives the run table from SciCat as a pandas DataFrame """ 145 assert self.scicat is not None, "ScicatClient not available" 146 147 datasets = self.scicat.get_all_datasets() 148 return scicat.csvruntable.CsvRunTable(datasets, True)._create_run_table() 149 150 def update_run(self, daq_run, **kwargs): 151 """ Updates the run table entry for the specified DAQ run 152 153 Use with caution 154 """ 155 assert self.scicat is not None, "ScicatClient not available" 156 metadata = self.scicat.get_dataset(daq_run)[0]['scientificMetadata'] 157 metadata = { **metadata, 'fablive': { **metadata['fablive'], **kwargs } } 158 self.scicat.update_dataset(daq_run, metadata) 159 160 def _repr_html_(self): 161 return self.pruns._repr_html_() 162 163 164 # css and html for the in-progress animation 165_inprogress_animation_formatter = """ 166<style> 167.loader-line {{ 168 height: 4px; 169 left: -5px; 170 position: relative; 171 overflow: hidden; 172 background-color: #99999940; 173 }} 174 175 .loader-line:before {{ 176 content: ""; 177 position: absolute; 178 left: -50%; 179 height: 4px; 180 width: 40%; 181 background-color: #6495EDAA; 182 -webkit-animation: lineAnim 1s linear infinite; 183 -moz-animation: lineAnim 1s linear infinite; 184 animation: lineAnim 5s linear infinite; 185 }} 186 187 @keyframes lineAnim {{ 188 0% {{ 189 left: -40%; 190 }} 191 50% {{ 192 left: 20%; 193 width: 80%; 194 }} 195 100% {{ 196 left: 100%; 197 width: 100%; 198 }} 199 }} 200</style> 201<div class="loader-line"></div> 202""" 203 204# strpied backgrond for aborted runs 205_striped_background = """ 206background: repeating-linear-gradient( 207 45deg, 208 #FFA38C85, 209 #FFA38C85 10px, 210 #FFA38C00 10px, 211 #FFA38C00 20px 212);"""
logger =
<Logger fab.beamtime (INFO)>
tele_log =
<Logger fab.telemetry (INFO)>
def
autodetect_beamtime():
21def autodetect_beamtime(): 22 ''' Attemps to autodetect the beamtime number from the current working directory.''' 23 try: 24 return int(os.getcwd().split('/')[7]) 25 except IndexError as e: 26 raise ValueError("Could not autodetect beamtime number from current working directory") from e
Attemps to autodetect the beamtime number from the current working directory.
def
beamtime_basepath(beamtime_num):
class
Beamtime:
43class Beamtime: 44 """ Represents a beamtime at the FLASH facility 45 46 Properties: 47 num: The beamtime number 48 basepath: The basepath of the beamtime 49 runs: The fablive run table as a pandas DataFrame 50 pruns: The pretty-printed run table 51 """ 52 53 def __init__(self, beamtime_num): 54 self.num = beamtime_num 55 self.basepath = beamtime_basepath(beamtime_num) 56 57 if scicat is not None: 58 self.scicat = scicat.ScicatClient(beamtime_num, self.basepath) 59 else: 60 self.scicat = None 61 62 @staticmethod 63 def _make_runs_dataframe(datasets): 64 """ Creates a pandas DataFrame from a list of scicat datasets """ 65 66 def process_dataset(dset): 67 """ Processes a single dataset """ 68 daq_run = int(dset['pid'].split('/')[-1]) 69 try: 70 fablive = dset['scientificMetadata']['fablive'] 71 except KeyError: 72 return None 73 return { 'daq_run': daq_run, **fablive } 74 75 processed = [row for dset in datasets if (row := process_dataset(dset))] 76 if len(processed) != len(datasets): 77 logger.warn(f"{len(datasets) - len(processed)} datasets were skipped due to missing fablive metadata") 78 return pd.DataFrame(processed).set_index('daq_run').sort_index(ascending=False) 79 80 def styler(self, df) -> pd.DataFrame: 81 """ Applies a default style to the run table returned by `pruns` 82 83 You can alter the style by changing the parameters in the 84 config, or you can monkeypatch this method to fully customize 85 the style. 86 """ 87 styler = df.style 88 styler.applymap_index(lambda x: "font-family: monospace; font-weight: bold;") 89 90 # Color unique entries differently for selected columns 91 for col in cfg.btm.styler.mark_unique: 92 _mark_uniques(styler, col) 93 94 styler.format(na_rep="", precision=3) 95 96 if cfg.btm.styler.hide_run_thr is not False: 97 styler.hide(subset=styler.data.index < cfg.btm.styler.hide_run_thr, axis=0) 98 99 # Mark aborted runs 100 if 'aborted' in df.columns and cfg.btm.styler.mark_aborted: 101 styler.applymap(lambda x: _striped_background if x is True else "", subset=['aborted']) 102 styler.apply(lambda x: ['opacity:0.55']*len(x) if x.aborted is True else [""]*len(x), axis=1) 103 104 # Mark in progress runs 105 if cfg.btm.styler.mark_in_progress: 106 styler.hide(subset=['in_progress'], axis=1) 107 styler.format(_inprogress_animation_formatter, subset=(styler.data['in_progress'] == True, 'shot_count')) 108 109 if cfg.btm.styler.shot_count_hist: 110 styler.bar(subset=['shot_count'], color='#6495ED40', vmin=0, height=65, width=90, props='width: 60px;') 111 112 #set max column width and ellipsize text 113 if cfg.btm.styler.collapse_cols: 114 styler.set_properties(**{'max-width': cfg.btm.styler.max_col_width, 115 'text-align': 'left', 116 'white-space': 'nowrap', 117 'overflow': 'hidden', 118 'text-overflow': 'ellipsis'}) 119 120 # Expand on hover 121 if cfg.btm.styler.show_on_hover: 122 styler.set_table_styles([{'selector': 'td:hover', 123 'props': [('max-width', cfg.btm.styler.max_col_width), 124 ('white-space', 'normal'), 125 ('overflow', 'visible')]}]) 126 127 return styler 128 129 @property 130 def pruns(self) -> pd.DataFrame.style: 131 """ Pretty-printed run table """ 132 tele_log.info("pruns") 133 return self.styler(self.runs) 134 135 @property 136 def runs(self) -> pd.DataFrame: 137 """ Retrives the fablive run table from SciCat as a pandas DataFrame """ 138 assert self.scicat is not None, "ScicatClient not available" 139 140 datasets = self.scicat.get_all_datasets() 141 return self._make_runs_dataframe(datasets) 142 143 @property 144 def raw_runs(self) -> pd.DataFrame: 145 """ Retrives the run table from SciCat as a pandas DataFrame """ 146 assert self.scicat is not None, "ScicatClient not available" 147 148 datasets = self.scicat.get_all_datasets() 149 return scicat.csvruntable.CsvRunTable(datasets, True)._create_run_table() 150 151 def update_run(self, daq_run, **kwargs): 152 """ Updates the run table entry for the specified DAQ run 153 154 Use with caution 155 """ 156 assert self.scicat is not None, "ScicatClient not available" 157 metadata = self.scicat.get_dataset(daq_run)[0]['scientificMetadata'] 158 metadata = { **metadata, 'fablive': { **metadata['fablive'], **kwargs } } 159 self.scicat.update_dataset(daq_run, metadata) 160 161 def _repr_html_(self): 162 return self.pruns._repr_html_()
Represents a beamtime at the FLASH facility
Properties:
num: The beamtime number basepath: The basepath of the beamtime runs: The fablive run table as a pandas DataFrame pruns: The pretty-printed run table
def
styler(self, df) -> pandas.core.frame.DataFrame:
80 def styler(self, df) -> pd.DataFrame: 81 """ Applies a default style to the run table returned by `pruns` 82 83 You can alter the style by changing the parameters in the 84 config, or you can monkeypatch this method to fully customize 85 the style. 86 """ 87 styler = df.style 88 styler.applymap_index(lambda x: "font-family: monospace; font-weight: bold;") 89 90 # Color unique entries differently for selected columns 91 for col in cfg.btm.styler.mark_unique: 92 _mark_uniques(styler, col) 93 94 styler.format(na_rep="", precision=3) 95 96 if cfg.btm.styler.hide_run_thr is not False: 97 styler.hide(subset=styler.data.index < cfg.btm.styler.hide_run_thr, axis=0) 98 99 # Mark aborted runs 100 if 'aborted' in df.columns and cfg.btm.styler.mark_aborted: 101 styler.applymap(lambda x: _striped_background if x is True else "", subset=['aborted']) 102 styler.apply(lambda x: ['opacity:0.55']*len(x) if x.aborted is True else [""]*len(x), axis=1) 103 104 # Mark in progress runs 105 if cfg.btm.styler.mark_in_progress: 106 styler.hide(subset=['in_progress'], axis=1) 107 styler.format(_inprogress_animation_formatter, subset=(styler.data['in_progress'] == True, 'shot_count')) 108 109 if cfg.btm.styler.shot_count_hist: 110 styler.bar(subset=['shot_count'], color='#6495ED40', vmin=0, height=65, width=90, props='width: 60px;') 111 112 #set max column width and ellipsize text 113 if cfg.btm.styler.collapse_cols: 114 styler.set_properties(**{'max-width': cfg.btm.styler.max_col_width, 115 'text-align': 'left', 116 'white-space': 'nowrap', 117 'overflow': 'hidden', 118 'text-overflow': 'ellipsis'}) 119 120 # Expand on hover 121 if cfg.btm.styler.show_on_hover: 122 styler.set_table_styles([{'selector': 'td:hover', 123 'props': [('max-width', cfg.btm.styler.max_col_width), 124 ('white-space', 'normal'), 125 ('overflow', 'visible')]}]) 126 127 return styler
Applies a default style to the run table returned by pruns
You can alter the style by changing the parameters in the config, or you can monkeypatch this method to fully customize the style.
pruns: <property object at 0x2adbdef99670>
129 @property 130 def pruns(self) -> pd.DataFrame.style: 131 """ Pretty-printed run table """ 132 tele_log.info("pruns") 133 return self.styler(self.runs)
Pretty-printed run table
runs: pandas.core.frame.DataFrame
135 @property 136 def runs(self) -> pd.DataFrame: 137 """ Retrives the fablive run table from SciCat as a pandas DataFrame """ 138 assert self.scicat is not None, "ScicatClient not available" 139 140 datasets = self.scicat.get_all_datasets() 141 return self._make_runs_dataframe(datasets)
Retrives the fablive run table from SciCat as a pandas DataFrame
raw_runs: pandas.core.frame.DataFrame
143 @property 144 def raw_runs(self) -> pd.DataFrame: 145 """ Retrives the run table from SciCat as a pandas DataFrame """ 146 assert self.scicat is not None, "ScicatClient not available" 147 148 datasets = self.scicat.get_all_datasets() 149 return scicat.csvruntable.CsvRunTable(datasets, True)._create_run_table()
Retrives the run table from SciCat as a pandas DataFrame
def
update_run(self, daq_run, **kwargs):
151 def update_run(self, daq_run, **kwargs): 152 """ Updates the run table entry for the specified DAQ run 153 154 Use with caution 155 """ 156 assert self.scicat is not None, "ScicatClient not available" 157 metadata = self.scicat.get_dataset(daq_run)[0]['scientificMetadata'] 158 metadata = { **metadata, 'fablive': { **metadata['fablive'], **kwargs } } 159 self.scicat.update_dataset(daq_run, metadata)
Updates the run table entry for the specified DAQ run
Use with caution