class Run(Analysis): """ Run class containing all the information for a single run. """ NTelPlanes = 4 def __init__(self, number=None, testcampaign=None, load_tree=True, verbose=None): """ :param number: if None is provided it creates a dummy run :param testcampaign: if None is provided ... :param load_tree: load the ROOT TTree :param verbose: turn on more output """ # Basics super(Run, self).__init__(testcampaign, verbose=verbose, pickle_dir='Run') self.Number = number # Directories / Test Campaign self.IrradiationFile = join(self.Dir, self.MainConfig.get('MISC', 'irradiation file')) # Configuration & Root Files self.Config = self.load_run_config() self.RootFileDir = self.load_rootfile_dirname() self.RootFilePath = self.load_rootfile_path() # Run Info self.InfoFile = join(self.TCDir, 'run_log.json') self.Info = self.load_run_info() self.RootFile = None self.Tree = TTree() self.TreeName = self.Config.get('BASIC', 'treename') self.DUTs = [self.dut(i + 1, self.Info) for i in range(self.get_n_diamonds())] if self.Number is not None else None # Settings self.Plane = Plane() self.TriggerPlanes = self.load_trigger_planes() # General Information self.Flux = self.get_flux() self.Type = self.get_type() # Times self.LogStart = self.load_log_start() self.LogEnd = self.load_log_stop() self.Duration = self.LogEnd - self.LogStart self.Converter = Converter(self) if self.set_run(number, load_tree): # tree info self.TimeOffset = None self.Time = self.load_time_vec() self.StartEvent = 0 self.NEvents = int(self.Tree.GetEntries()) self.EndEvent = self.NEvents - 1 self.StartTime = self.get_time_at_event(self.StartEvent) self.EndTime = self.get_time_at_event(self.EndEvent) self.TotalTime = self.load_total_time() self.TotalMinutes = self.TotalTime / 60000. self.Duration = timedelta(seconds=self.TotalTime) self.LogEnd = self.LogStart + self.Duration # overwrite if we know exact duration self.NPlanes = self.load_n_planes() self.TInit = time() - self.InitTime def __str__(self): return f'{self.__class__.__name__} {self.Number}{self.evt_str} ({self.TCString})' def __repr__(self): return self.__str__() def __call__(self, number, load_tree=False): self.set_run(number, load_tree) return self def __gt__(self, other): return self.Number > (other.Number if isinstance(other, Run) else other) @property def evt_str(self): return f' with {make_ev_str(self.Info["events"])} ev' if 'events' in self.Info else f' with {make_ev_str(self.NEvents)} ev' if self.Tree.Hash() else '' def set_run(self, number, load_tree): if number is None: return False if number < 0 and type(number) is not int: critical('incorrect run number') self.Number = number self.load_run_info() self.Flux = self.get_flux() # check for conversion if load_tree: self.Converter.convert_run() self.load_rootfile() else: return False if not self.rootfile_is_valid(): self.Converter.convert_run() self.load_rootfile() return True def get_type(self): return self.Config.get('BASIC', 'type') if self.Number is not None else None def set_estimate(self, n=None): self.Tree.SetEstimate(choose(n, -1)) def is_volt_scan(self): return any(name in self.Info['runtype'] for name in ['voltage', 'hv']) # ---------------------------------------- # region INIT @property def dut(self): return DUT def load_rootfile(self, prnt=True): self.info('Loading information for rootfile: {file}'.format(file=basename(self.RootFilePath)), endl=False, prnt=prnt) self.RootFile = TFile(self.RootFilePath) self.Tree = self.RootFile.Get(self.TreeName) return self.Tree def load_run_config(self): base_file_name = join(get_base_dir(), 'config', self.TCString, 'RunConfig.ini') if not file_exists(base_file_name): critical('RunConfig.ini does not exist for {0}! Please create it in config/{0}!'.format(self.TCString)) parser = Config(base_file_name) # first read the main config file with general information for all splits if parser.has_section('SPLIT') and self.Number is not None: split_runs = [0] + loads(parser.get('SPLIT', 'runs')) + [inf] config_nr = next(i for i in range(1, len(split_runs)) if split_runs[i - 1] <= self.Number < split_runs[i]) parser.read(join(get_base_dir(), 'config', self.TCString, 'RunConfig{nr}.ini'.format(nr=config_nr))) # add the content of the split config return parser @staticmethod def make_root_filename(run): return f'TrackedRun{run:0>3}.root' def make_root_subdir(self): return join('root', 'pads' if self.get_type() == 'pad' else self.get_type()) def load_rootfile_path(self, run=None): run = choose(run, self.Number) return None if run is None else join(self.RootFileDir, self.make_root_filename(run)) def load_rootfile_dirname(self): return ensure_dir(join(self.TCDir, self.make_root_subdir())) if self.Number is not None else None def load_trigger_planes(self): return array(self.Config.get_list('BASIC', 'trigger planes', [1, 2])) def get_n_diamonds(self, run_number=None): run_info = self.load_run_info(run_number) return len([key for key in run_info if key.startswith('dia') and key[-1].isdigit()]) def load_dut_numbers(self): return [i + 1 for i in range(len([key for key in self.Info.keys() if key.startswith('dia') and key[-1].isdigit()]))] def load_dut_type(self): dut_type = self.Config.get('BASIC', 'type') if self.Number is not None else None if dut_type not in ['pixel', 'pad', None]: critical("The DUT type {0} has to be either 'pixel' or 'pad'".format(dut_type)) return dut_type def load_default_info(self): with open(join(self.Dir, 'Runinfos', 'defaultInfo.json')) as f: return load(f) def load_run_info_file(self): if not file_exists(self.InfoFile): critical('Run Log File: "{f}" does not exist!'.format(f=self.InfoFile)) with open(self.InfoFile) as f: return load(f) def load_run_info(self, run_number=None): data = self.load_run_info_file() run_number = self.Number if run_number is None else run_number if run_number is not None: run_info = data.get(str(run_number)) if run_info is None: # abort if the run is still not found critical('Run {} not found in json run log file!'.format(run_number)) self.Info = run_info self.Info['masked pixels'] = [0] * 4 self.translate_diamond_names() return run_info else: self.Info = self.load_default_info() return self.Info def load_dut_names(self): return [self.Info['dia{nr}'.format(nr=i)] for i in range(1, self.get_n_diamonds() + 1)] def load_biases(self): return [int(self.Info['dia{nr}hv'.format(nr=i)]) for i in range(1, self.get_n_diamonds() + 1)] def load_log_start(self): return conv_log_time(self.Info['starttime0']) def load_log_stop(self): return conv_log_time(self.Info['endtime']) def load_total_time(self): return (self.Time[-1] - self.Time[0]) / 1000 def load_n_planes(self): if self.has_branch('cluster_col'): self.Tree.Draw('@cluster_col.size()', '', 'goff', 1) return int(self.Tree.GetV1()[0]) else: return 4 def load_time_vec(self): t = get_time_vec(self.Tree) t0 = datetime.fromtimestamp(t[0] / 1000) if t[0] < 1e12 else None self.TimeOffset = None if t0 is None or t0.year > 2000 and t0.day == self.LogStart.day else t[0] - time_stamp(self.LogStart) * 1000 return t if self.TimeOffset is None else t - self.TimeOffset def load_plane_efficiency(self, plane): return self.load_plane_efficiencies()[plane - 1] def load_plane_efficiencies(self): return [ufloat(e, .03) for e in self.Config.get_list('BASIC', 'plane efficiencies', default=[.95, .95])] # endregion INIT # ---------------------------------------- # ---------------------------------------- # region MASK def load_mask_file_path(self): mask_dir = self.MainConfig.get('MAIN', 'maskfile directory') if self.MainConfig.has_option('MAIN', 'maskfile directory') else join(self.DataDir, self.TCDir, 'masks') if not dir_exists(mask_dir): warning('Mask file directory does not exist ({})!'.format(mask_dir)) return join(mask_dir, basename(self.Info['maskfile'])) def load_mask(self, plane=None): mask_file = self.load_mask_file_path() if basename(mask_file).lower() in ['no mask', 'none', 'none!', ''] or self.Number is None: return try: data = genfromtxt(mask_file, [('id', 'U10'), ('pl', 'i'), ('x', 'i'), ('y', 'i')]) if 'cornBot' not in data['id']: warning('Invalid mask file: "{}". Not taking any mask!'.format(mask_file)) mask = [[data[where((data['pl'] == pl) & (data['id'] == n))][0][i] for n in ['cornBot', 'cornTop'] for i in [2, 3]] for pl in sorted(set(data['pl']))] mask = [[max(1, m[0]), max(1, m[1]), min(self.Plane.NCols - 2, m[2]), min(self.Plane.NRows - 2, m[3])] for m in mask] # outer pixels are ignored return mask if plane is None else mask[plane - 1] if plane - 1 < len(mask) else None except Exception as err: warning(err) warning('Could not read mask file... not taking any mask!') def get_mask_dim(self, plane=1, mm=True): return Plane.get_mask_dim(self.load_mask(plane), mm) def get_mask_dims(self, mm=True): return array([self.get_mask_dim(pl, mm) for pl in [1, 2]]) def get_unmasked_area(self, plane): return None if self.Number is None else Plane.get_area(self.load_mask(plane)) def find_for_in_comment(self): for name in ['for1', 'for2']: if name not in self.Info: for cmt in self.Info['comments'].split('\r\n'): cmt = cmt.replace(':', '') cmt = cmt.split(' ') if str(cmt[0].lower()) == name: self.Info[name] = int(cmt[1]) return 'for1' in self.Info # endregion MASK # ---------------------------------------- # ---------------------------------------- # region HELPERS def translate_diamond_names(self): for key, value in [(key, value) for key, value in self.Info.items() if key.startswith('dia') and key[-1].isdigit()]: self.Info[key] = self.translate_dia(value) def register_new_dut(self): if input('Do you want to add a new diamond? [y,n] ').lower() in ['y', 'yes']: dut_type = int(input('Enter the DUT type (1 for pCVD, 2 for scCVD, 3 for silicon): ')) - 1 dut_name = input('Enter the name of the DUT (no "_"): ') alias = input(f'Enter the alias (no "_", for default {dut_name.lower()} press enter): ') self.add_alias(alias, dut_name, dut_type) self.add_dut_info(dut_name) return True else: return False @staticmethod def add_alias(alias, dut_name, dut_type): alias_file = join(Dir, 'config', 'DiamondAliases.ini') with open(alias_file, 'r+') as f: lines = [line.strip(' \n') for line in f.readlines()] i0 = lines.index(['# pCVD', '# scCVD', '# Silicon'][dut_type]) i = next(i for i, line in enumerate(lines[i0:], i0) if line.strip() == '') lines.insert(i, f'{(alias if alias else dut_name).lower()} = {dut_name}') f.seek(0) f.writelines([f'{line}\n' for line in lines]) info(f'added entry: {(alias if alias else dut_name).lower()} = {dut_name} in {alias_file}') def add_dut_info(self, dut_name): dia_info_file = join(Dir, 'Runinfos', 'dia_info.json') data = load_json(dia_info_file) if dut_name in data: return warning('The entered DUT name already exists!') tc = get_input(f'Enter the beam test [YYYYMM]', self.TCString) data[dut_name] = {'irradiation': {tc: get_input(f'Enter the irradiation for {tc}', '0')}, 'boardnumber': {tc: get_input(f'Enter the board number for {tc}')}, 'thickness': get_input('Enter the thickness'), 'size': get_input('Enter the lateral size ([x, y])'), 'manufacturer': get_input('Enter the manufacturer')} with open(dia_info_file, 'w') as f: dump(data, f, indent=2) info(f'added {dut_name} to {dia_info_file}') def translate_dia(self, dia): name, suf = dia.split('_')[0].lower(), '_'.join(dia.split('_')[1:]) if name not in Config(join(self.Dir, 'config', 'DiamondAliases.ini')).options('ALIASES'): warning(f'{dia} was not found in config/DiamondAliases.ini!') if not self.register_new_dut(): critical(f'unknown diamond {dia}') parser = Config(join(self.Dir, 'config', 'DiamondAliases.ini')) return '_'.join([parser.get('ALIASES', name)] + ([suf] if suf else [])) def reload_run_config(self, run_number): self.Number = run_number self.Config = self.load_run_config() self.Info = self.load_run_info() self.RootFileDir = self.load_rootfile_dirname() self.RootFilePath = self.load_rootfile_path() return self.Config def rootfile_is_valid(self, file_path=None): tfile = self.RootFile if file_path is None else TFile(file_path) ttree = self.Tree if file_path is None else tfile.Get(self.TreeName) is_valid = not tfile.IsZombie() and tfile.ClassName() == 'TFile' and ttree and ttree.ClassName() == 'TTree' if not is_valid: warning('Invalid TFile or TTree! Deleting file {}'.format(tfile.GetName())) remove_file(tfile.GetName()) return is_valid def calculate_plane_flux(self, plane=1, corr=True): """estimate the flux [kHz/cm²] through a trigger plane based on Poisson statistics.""" rate, eff, area = self.Info[f'for{plane}'], self.load_plane_efficiency(plane), self.get_unmasked_area(plane) return -log(1 - rate / Plane.Frequency) * Plane.Frequency / area / 1000 / (eff if corr else ufloat(1, .05)) # count zero hits of Poisson def find_n_events(self, n, cut, start=0): evt_numbers = self.get_tree_vec(var='Entry$', cut=cut, nentries=self.NEvents, firstentry=start) return int(evt_numbers[:n][-1] + 1 - start) def get_max_run(self): return int(max(self.load_run_info_file(), key=int)) # endregion HELPERS # ---------------------------------------- # ---------------------------------------- # region GET def get_flux(self, plane=None, corr=True): if self.Number is None: return if not self.find_for_in_comment(): # warning('no plane rates in the data...') return self.Info['measuredflux'] / (mean(self.load_plane_efficiencies()) if corr else 1) return self.get_mean_flux(corr) if plane is None else self.calculate_plane_flux(plane, corr) def get_mean_flux(self, corr=True): return mean([self.get_flux(pl, corr) for pl in [1, 2]]) def get_time(self): return ufloat(time_stamp(self.LogStart + self.Duration / 2), self.Duration.seconds / 2) def get_channel_name(self, channel): self.Tree.GetEntry() return self.Tree.sensor_name[channel] def get_time_at_event(self, event): """ For negative event numbers it will return the time stamp at the startevent. """ return self.Time[min(event, self.EndEvent)] / 1000. def get_event_at_time(self, seconds, rel=True): """ Returns the event nunmber at time dt from beginning of the run. Accuracy: +- 1 Event """ if seconds - (0 if rel else self.StartTime) >= self.TotalTime or seconds == -1: # return time of last event if input is too large return self.NEvents - 1 return where(self.Time <= 1000 * (seconds + (self.StartTime if rel else 0)))[0][-1] def get_tree_vec(self, var, cut='', dtype=None, nentries=None, firstentry=0): return get_tree_vec(self.Tree, var, cut, dtype, nentries, firstentry) def get_tree_tuple(self): return (self.Tree, self.RootFile) if self.Tree is not None else False def get_time_vec(self): return self.Time if hasattr(self, 'Time') else None def get_bias_strings(self): return [str(b) for b in self.load_biases()] @save_pickle('HR', suf_args=0) def get_high_rate_run(self, high=True): from src.run_selection import RunSelector return int(RunSelector(testcampaign=self.TCString).get_high_rate_run(self.Number, high)) def get_low_rate_run(self): return self.get_high_rate_run(high=False) # endregion GET # ---------------------------------------- # ---------------------------------------- # region SHOW def show_info(self): print('Run information for', self) for key, value in sorted(self.Info.items()): print(f'{key:<13}: {value}') def has_branch(self, name): return bool(self.Tree.GetBranch(name)) def info(self, msg, endl=True, blank_lines=0, prnt=True): return info(msg, endl, prnt=self.Verbose and prnt, blank_lines=blank_lines) def add_to_info(self, t, txt='Done', prnt=True): return add_to_info(t, txt, prnt=self.Verbose and prnt)