Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 14 additions & 16 deletions law/CreateDatacardsTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,28 @@ class CreateDatacardsTask(StatInferenceTask, HTCondorWorkflow, law.LocalWorkflow
Runs dc_make/create_datacards.py on whatever `<era>/<variable>/<variable>.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.
"""

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:
# it is the only one holding every sub-era's histograms at once, which is what the
# 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.
Expand All @@ -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()
}

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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")
)
Expand Down Expand Up @@ -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/<year>.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",
Expand Down
2 changes: 1 addition & 1 deletion law/MergedHists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions law/PreprocessShapesTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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))
Expand Down
10 changes: 5 additions & 5 deletions law/ResonantLimitsAndHistPlotTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<era>/.

So a group era is replaced by its members rather than dropped. Filtering it out
Expand All @@ -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)
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions law/ResonantLimitsTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
Expand Down
20 changes: 10 additions & 10 deletions law/StatInferenceTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
Loading