diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6d79b91..afc5941 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -48,6 +48,18 @@ histogram?* - Histogram names are built from the card and must match what the analysis actually wrote. A rename on either side is a silent failure at datacard time. +### Eras + +- The FLAF era is `era` (`--era`); `period` is refused by FLAF as a parameter, option or keyword + and survives only as the AnaTuple column. +- `era` is FLAF's `EraParameter(max_len=1)`: a tuple holding one real era. It is read as + `self.era[0]` — `self.era` itself in a path, a label or a command is the tuple, not the name. + Requirements pass one real era as a tuple: `X.req(self, era=(e,), …)`. +- A meta-era (`era_groups:` in the card) never goes into `era`, because FLAF's `Setup` knows only + real eras: it goes into `meta_era`, and `era` holds one of its sub-eras as the `Setup` anchor. + `datacard_era` (`meta_era`, else `era[0]`) keys the outputs and store paths, and + `get_sub_eras()` lists the real eras whose histograms feed a card. + ### Binning optimisation Changes under `bin_opt/` alter which events land in which bin, and therefore the sensitivity. diff --git a/law/CreateDatacardsTask.py b/law/CreateDatacardsTask.py index 88794e1..56408ab 100644 --- a/law/CreateDatacardsTask.py +++ b/law/CreateDatacardsTask.py @@ -20,7 +20,7 @@ class CreateDatacardsTask(StatInferenceTask, HTCondorWorkflow, law.LocalWorkflow Runs dc_make/create_datacards.py on whatever `//.root` tree it is given -- the configuration's `preprocess:` step if it declares one, otherwise the merged histograms directly -- and does no binning of its own. For a meta-era - (`meta_era` set, `period` one of its real sub-eras) it holds every sub-era's + (`meta_era` set, `era` one of its real sub-eras) it holds every sub-era's histograms at once, which is what the summed shape behind each datacard bin is built from, and why the stacked shape plots are drawn here. """ @@ -28,9 +28,9 @@ class CreateDatacardsTask(StatInferenceTask, HTCondorWorkflow, law.LocalWorkflow max_runtime = copy_param(HTCondorWorkflow.max_runtime, 2.0) n_cpus = copy_param(HTCondorWorkflow.n_cpus, 1) - # If set, build datacards for this meta-era instead of self.period. self.period must + # If set, build datacards for this meta-era instead of self.era[0]. self.era[0] must # then be one of its real sub-eras -- it is only used to construct a valid FLAF Setup - # (Setup.getGlobal requires a real, known period; a meta-era name isn't one). + # (Setup.getGlobal requires a real, known era; a meta-era name isn't one). meta_era = luigi.Parameter(default="") # Stacked plots of the shapes going into the cards. This task is where they belong: @@ -38,10 +38,10 @@ class CreateDatacardsTask(StatInferenceTask, HTCondorWorkflow, law.LocalWorkflow # merged shape (and hence each datacard bin) is built from. make_plots = luigi.BoolParameter(default=True) - def get_sub_periods(self): - """Real periods whose histograms feed into datacard_era: its constituent - sub-eras for a meta-era, otherwise just [self.period].""" - return self.get_era_groups().get(self.datacard_era, [self.period]) + def get_sub_eras(self): + """Real eras whose histograms feed into datacard_era: its constituent + sub-eras for a meta-era, otherwise just [self.era[0]].""" + return self.get_era_groups().get(self.datacard_era, [self.era[0]]) def input_hist_reqs(self): """{key: task} for the shapes the cards are built from. @@ -59,7 +59,7 @@ def input_hist_reqs(self): return { f"MergedHists_{era}_{variable}": req for (era, variable), req in self.merged_hist_reqs( - self.get_sub_periods() + self.get_sub_eras() ).items() } @@ -112,9 +112,7 @@ def run(self): else: targets = { key: req.output() - for key, req in self.merged_hist_reqs( - self.get_sub_periods() - ).items() + for key, req in self.merged_hist_reqs(self.get_sub_eras()).items() } self.check_inputs(targets) base_dir_local = stack.enter_context(self.stage_inputs(targets)) @@ -176,7 +174,7 @@ def plot_variable(self, variable, axis=1): try: import FLAF.Common.Setup as Setup - hists = Setup.Setup(self.ana_path(), self.period, self.version).hists + hists = Setup.Setup(self.ana_path(), self.era[0], self.version).hists var_list = hists[variable].get("var_list") if var_list and len(var_list) > 1: return var_list[axis] @@ -294,7 +292,7 @@ def plot_rebinned_shapes(self, base_dir, config, output_dir): for variable in self.get_required_variables(): inputs = [ p - for era in self.get_sub_periods() + for era in self.get_sub_eras() for p in glob.glob( os.path.join(base_dir, era, variable, f"{variable}.root") ) @@ -358,14 +356,14 @@ def plot_rebinned_shapes(self, base_dir, config, output_dir): # The plotted shapes are the sum over every sub-era, so the label has to # name the combination: HistPlotter reads config/plot/.yaml for the # luminosity, and a sub-era's file states that sub-era's luminosity alone. - # --period stays a real era -- it builds the Setup, which does not know + # --era stays a real era -- it builds the Setup, which does not know # meta-era names. "--year", self.datacard_era, "--ana_path", self.ana_path(), - "--period", - self.period, + "--era", + self.era[0], "--LAWrunVersion", self.version, "--wantSignals", diff --git a/law/MergedHists.py b/law/MergedHists.py index 41a5709..24d8b20 100644 --- a/law/MergedHists.py +++ b/law/MergedHists.py @@ -23,7 +23,7 @@ def output(self): return self.remote_target( self.input_hists_version, "Hists_merged", - self.period, + self.era[0], self.variable, f"{self.variable}.root", fs=self.fs_HistTuple, diff --git a/law/PreprocessShapesTask.py b/law/PreprocessShapesTask.py index 01235a2..f6337c0 100644 --- a/law/PreprocessShapesTask.py +++ b/law/PreprocessShapesTask.py @@ -33,17 +33,17 @@ class PreprocessShapesTask(StatInferenceTask, HTCondorWorkflow, law.LocalWorkflo n_cpus = copy_param(HTCondorWorkflow.n_cpus, 1) # As on CreateDatacardsTask: preprocessing a meta-era needs its members, and - # self.period must stay a real era because FLAF's Setup resolves it against config/. + # self.era[0] must stay a real era because FLAF's Setup resolves it against config/. meta_era = luigi.Parameter(default="") - def get_sub_periods(self): - return self.get_era_groups().get(self.datacard_era, [self.period]) + def get_sub_eras(self): + return self.get_era_groups().get(self.datacard_era, [self.era[0]]) def input_hist_reqs(self): return { f"MergedHists_{era}_{variable}": req for (era, variable), req in self.merged_hist_reqs( - self.get_sub_periods() + self.get_sub_eras() ).items() } @@ -78,7 +78,7 @@ def run(self): with contextlib.ExitStack() as stack: targets = { key: req.output() - for key, req in self.merged_hist_reqs(self.get_sub_periods()).items() + for key, req in self.merged_hist_reqs(self.get_sub_eras()).items() } self.check_inputs(targets) base_dir_local = stack.enter_context(self.stage_inputs(targets)) diff --git a/law/ResonantLimitsAndHistPlotTask.py b/law/ResonantLimitsAndHistPlotTask.py index 7ca5fc5..eedb516 100644 --- a/law/ResonantLimitsAndHistPlotTask.py +++ b/law/ResonantLimitsAndHistPlotTask.py @@ -22,13 +22,13 @@ class ResonantLimitsAndHistPlotTask(StatInferenceTask): workflow = luigi.Parameter(default=law.parameter.NO_STR) def get_plottable_eras(self): - """Real production periods, for FLAF's HistPlotTask. + """Real production eras, for FLAF's HistPlotTask. Deliberately not the same set as ResonantLimitsTask.get_eras(), which returns the eras that carry their own datacards -- for a configuration with an `era_groups:` entry that is the meta-era, and this is its members. The two are complements: limits are set on the combination, while the input histograms can only be plotted - for periods that really exist, since HistPlotTask resolves `period` against + for eras that really exist, since HistPlotTask resolves `era` against config//. So a group era is replaced by its members rather than dropped. Filtering it out @@ -37,11 +37,11 @@ def get_plottable_eras(self): success having required no HistPlotTask. """ era_groups = self.get_era_groups() - eras = self.get_config_data().get("eras", [self.period]) + eras = self.get_config_data().get("eras", [self.era[0]]) plottable = [] for e in eras: for real_era in era_groups.get(e, [e]): - # A configuration may list a group and its members both; each period is + # A configuration may list a group and its members both; each era is # plotted once. if real_era not in plottable: plottable.append(real_era) @@ -54,7 +54,7 @@ def requires(self): # under hists_version -- req() would otherwise copy this task's own version # and schedule a fresh production of them under it. reqs.append( - HistPlotTask.req(self, period=e, version=self.input_hists_version) + HistPlotTask.req(self, era=(e,), version=self.input_hists_version) ) return reqs diff --git a/law/ResonantLimitsTask.py b/law/ResonantLimitsTask.py index 646601f..633e220 100644 --- a/law/ResonantLimitsTask.py +++ b/law/ResonantLimitsTask.py @@ -34,9 +34,9 @@ def _create_datacards_req(self, era, **kwargs): era_groups = self.get_era_groups() if era in era_groups: return CreateDatacardsTask.req( - self, period=era_groups[era][0], meta_era=era, **kwargs + self, era=(era_groups[era][0],), meta_era=era, **kwargs ) - return CreateDatacardsTask.req(self, period=era, **kwargs) + return CreateDatacardsTask.req(self, era=(era,), **kwargs) def requires(self): return [self._create_datacards_req(e, branches=()) for e in self.get_eras()] diff --git a/law/StatInferenceTask.py b/law/StatInferenceTask.py index 0dab693..e2c7319 100644 --- a/law/StatInferenceTask.py +++ b/law/StatInferenceTask.py @@ -65,17 +65,17 @@ def get_config_data(self): @property def datacard_era(self): """The era this task's products belong to: its meta-era if it has one, else - `period`. Equal to `period` for every task that does not declare `meta_era`. + `era`. Equal to `era` for every task that does not declare `meta_era`. """ - return getattr(self, "meta_era", "") or self.period + return getattr(self, "meta_era", "") or self.era[0] def store_parts(self): - """Keyed on datacard_era, not period. + """Keyed on datacard_era, not era. - A meta-era instance is constructed with `period` set to one of its own sub-eras - -- FLAF's Setup only knows real periods -- so keying on `period` gave - CreateDatacardsTask(period=Run3_2022) and - CreateDatacardsTask(period=Run3_2022, meta_era=Run3_Early) the same store path. + A meta-era instance is constructed with `era` set to one of its own sub-eras + -- FLAF's Setup only knows real eras -- so keying on `era` gave + CreateDatacardsTask(era=Run3_2022) and + CreateDatacardsTask(era=Run3_2022, meta_era=Run3_Early) the same store path. The declared outputs never collided (they key on datacard_era already), but law's HTCondor control files live here: htcondor_output_directory() is local_path(), and the submission file is named only after the branches, which @@ -115,7 +115,7 @@ def get_all_eras(self): different datasets, not a double count. Only combining them into one card would be -- see get_top_level_eras(). """ - return self.get_config_data().get("eras", [self.period]) + return self.get_config_data().get("eras", [self.era[0]]) def get_top_level_eras(self): """The eras that may be *combined* with each other: group eras plus any standalone @@ -184,14 +184,14 @@ def get_required_variables(self): def merged_hist_reqs(self, eras): """{(era, variable): MergedHists} -- every merged input file for those eras. - Read by CreateDatacardsTask over the sub-periods of the era it builds cards for. + Read by CreateDatacardsTask over the sub-eras of the era it builds cards for. """ # Deferred: MergedHists subclasses this class, so importing it at module level # would be circular. from .MergedHists import MergedHists return { - (era, variable): MergedHists.req(self, period=era, variable=variable) + (era, variable): MergedHists.req(self, era=(era,), variable=variable) for era in eras for variable in self.get_required_variables() }