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):
28def beamtime_basepath(beamtime_num):
29    basepath = f"/asap3/*flash*/gpfs/*/*/data/{beamtime_num}"
30    basepath = glob.glob(basepath)[0]
31    
32    return basepath 
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

Beamtime(beamtime_num)
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
num
basepath
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