From d717b214c68b10c5c2766edeb454c81cfb4545e0 Mon Sep 17 00:00:00 2001 From: Konstantin Androsov Date: Fri, 19 Jun 2026 18:17:14 +0200 Subject: [PATCH 1/5] Support multiple eras in ResonantLimits tasks --- law/tasks.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/law/tasks.py b/law/tasks.py index 511cf63..da0d436 100644 --- a/law/tasks.py +++ b/law/tasks.py @@ -65,27 +65,48 @@ def run(self): class ResonantLimitsTask(Task): workflow = luigi.Parameter(default=law.parameter.NO_STR) + def get_eras(self): + statInf_entry = self.global_params["StatInference"] + config = os.path.join(self.ana_path(), statInf_entry["config"]) + import yaml + with open(config, "r") as f: + data = yaml.safe_load(f) + return data.get("eras", [self.period]) + def requires(self): - return [ CreateDatacardsTask.req(self, branches=()) ] + return [ CreateDatacardsTask.req(self, period=e, branches=()) for e in self.get_eras() ] def output(self): return self.local_target("dummy.txt") def run(self): - create_dc_br0 = CreateDatacardsTask.req(self, branch=0, branches=()) - output_dir = create_dc_br0.output().abspath - limits = yield MergeResonantLimits(version=self.version, datacards=os.path.join(output_dir, "*.txt")) + datacards = [] + for e in self.get_eras(): + create_dc_br0 = CreateDatacardsTask.req(self, period=e, branch=0, branches=()) + output_dir = create_dc_br0.output().abspath + datacards.append(os.path.join(output_dir, "*.txt")) + + limits = yield MergeResonantLimits(version=self.version, datacards=tuple(datacards)) print(f"Merged limits: {limits}") self.output().touch() class ResonantLimitsAndHistPlotTask(Task): workflow = luigi.Parameter(default=law.parameter.NO_STR) + + def get_eras(self): + statInf_entry = self.global_params["StatInference"] + config = os.path.join(self.ana_path(), statInf_entry["config"]) + import yaml + with open(config, "r") as f: + data = yaml.safe_load(f) + return data.get("eras", [self.period]) + def requires(self): - return [ - ResonantLimitsTask.req(self), - HistPlotTask.req(self), - ] + reqs = [ ResonantLimitsTask.req(self) ] + for e in self.get_eras(): + reqs.append(HistPlotTask.req(self, period=e)) + return reqs def output(self): return self.local_target("dummy.txt") From e4da8586b50d84c5696725c9778369bc89b8d492 Mon Sep 17 00:00:00 2001 From: Konstantin Androsov Date: Fri, 19 Jun 2026 19:39:06 +0200 Subject: [PATCH 2/5] Output limits and combined datacards to artifacts --- law/tasks.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/law/tasks.py b/law/tasks.py index da0d436..fdb944a 100644 --- a/law/tasks.py +++ b/law/tasks.py @@ -77,18 +77,57 @@ def requires(self): return [ CreateDatacardsTask.req(self, period=e, branches=()) for e in self.get_eras() ] def output(self): - return self.local_target("dummy.txt") + # By default the period will be set to 'combined' in ResonantLimitsAndHistPlotTask + # so this will map to data/CI/ResonantLimitsTask/combined/... + return { + "limits": self.local_target("limits.npz"), + "datacards": law.LocalDirectoryTarget(os.path.join(self.ana_data_path(), self.version, "Datacards", "combined")) + } def run(self): datacards = [] - for e in self.get_eras(): + eras = self.get_eras() + era_cards = {} + import glob + import re + + for e in eras: create_dc_br0 = CreateDatacardsTask.req(self, period=e, branch=0, branches=()) output_dir = create_dc_br0.output().abspath - datacards.append(os.path.join(output_dir, "*.txt")) + cards = glob.glob(os.path.join(output_dir, "*.txt")) + era_cards[e] = cards + datacards.extend(cards) limits = yield MergeResonantLimits(version=self.version, datacards=tuple(datacards)) print(f"Merged limits: {limits}") - self.output().touch() + + import shutil + self.output()["limits"].parent.touch() + shutil.copy2(limits.path, self.output()["limits"].path) + + out_dc_dir = self.output()["datacards"] + out_dc_dir.touch() + + masses = set() + for e, cards in era_cards.items(): + for c in cards: + m = re.search(r'_(\d+)\.txt$', c) + if m: + masses.add(m.group(1)) + + for mass in masses: + combine_args = [] + for e in eras: + for c in era_cards[e]: + if c.endswith(f"_{mass}.txt"): + combine_args.append(f"{e}={c}") + break + + if combine_args: + cmd = ["combineCards.py"] + combine_args + out_file = os.path.join(out_dc_dir.path, f"combined_{mass}.txt") + with open(out_file, "w") as f: + ps_call(cmd, env=self.cmssw_env, stdout=f) class ResonantLimitsAndHistPlotTask(Task): @@ -103,7 +142,7 @@ def get_eras(self): return data.get("eras", [self.period]) def requires(self): - reqs = [ ResonantLimitsTask.req(self) ] + reqs = [ ResonantLimitsTask.req(self, period="combined") ] for e in self.get_eras(): reqs.append(HistPlotTask.req(self, period=e)) return reqs From e422d64b217d4bdc695b06dc989f9e791ac75082 Mon Sep 17 00:00:00 2001 From: Konstantin Androsov Date: Fri, 19 Jun 2026 22:43:27 +0200 Subject: [PATCH 3/5] Fix Setup failure by removing dummy period override --- law/tasks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/law/tasks.py b/law/tasks.py index fdb944a..f98465a 100644 --- a/law/tasks.py +++ b/law/tasks.py @@ -65,6 +65,9 @@ def run(self): class ResonantLimitsTask(Task): workflow = luigi.Parameter(default=law.parameter.NO_STR) + def store_parts(self): + return (self.version, self.__class__.__name__, "combined") + def get_eras(self): statInf_entry = self.global_params["StatInference"] config = os.path.join(self.ana_path(), statInf_entry["config"]) @@ -77,8 +80,6 @@ def requires(self): return [ CreateDatacardsTask.req(self, period=e, branches=()) for e in self.get_eras() ] def output(self): - # By default the period will be set to 'combined' in ResonantLimitsAndHistPlotTask - # so this will map to data/CI/ResonantLimitsTask/combined/... return { "limits": self.local_target("limits.npz"), "datacards": law.LocalDirectoryTarget(os.path.join(self.ana_data_path(), self.version, "Datacards", "combined")) @@ -142,7 +143,7 @@ def get_eras(self): return data.get("eras", [self.period]) def requires(self): - reqs = [ ResonantLimitsTask.req(self, period="combined") ] + reqs = [ ResonantLimitsTask.req(self) ] for e in self.get_eras(): reqs.append(HistPlotTask.req(self, period=e)) return reqs From 76776f4a90192706d039d69d846a91b5e4a26ea6 Mon Sep 17 00:00:00 2001 From: Konstantin Androsov Date: Fri, 19 Jun 2026 23:17:46 +0200 Subject: [PATCH 4/5] Fix ps_call stdout TypeError by using subprocess.run --- law/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/law/tasks.py b/law/tasks.py index f98465a..2078152 100644 --- a/law/tasks.py +++ b/law/tasks.py @@ -125,10 +125,11 @@ def run(self): break if combine_args: + import subprocess cmd = ["combineCards.py"] + combine_args out_file = os.path.join(out_dc_dir.path, f"combined_{mass}.txt") with open(out_file, "w") as f: - ps_call(cmd, env=self.cmssw_env, stdout=f) + subprocess.run(cmd, env=self.cmssw_env, stdout=f, check=True) class ResonantLimitsAndHistPlotTask(Task): From 231f8197c99ac60cf2fbab4097d87f817ac515fa Mon Sep 17 00:00:00 2001 From: Konstantin Androsov Date: Sat, 20 Jun 2026 10:30:02 +0200 Subject: [PATCH 5/5] formatting --- law/tasks.py | 53 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/law/tasks.py b/law/tasks.py index 2078152..385a965 100644 --- a/law/tasks.py +++ b/law/tasks.py @@ -17,7 +17,7 @@ class CreateDatacardsTask(Task, HTCondorWorkflow, law.LocalWorkflow): n_cpus = copy_param(HTCondorWorkflow.n_cpus, 1) def workflow_requires(self): - return { "HistMerger": HistMergerTask.req(self, branches=()) } + return {"HistMerger": HistMergerTask.req(self, branches=())} def requires(self): merge_map = HistMergerTask.req(self, branch=-1, branches=()).create_branch_map() @@ -28,10 +28,12 @@ def requires(self): ] def create_branch_map(self): - return { 0: None } + return {0: None} def output(self): - path = os.path.join(self.ana_data_path(), self.version, "Datacards", self.period) + path = os.path.join( + self.ana_data_path(), self.version, "Datacards", self.period + ) return law.LocalDirectoryTarget(path) def run(self): @@ -39,7 +41,9 @@ def run(self): config = os.path.join(self.ana_path(), statInf_entry["config"]) hist_bins = os.path.join(self.ana_path(), statInf_entry["hist_bins"]) param_values = statInf_entry.get("param_values", []) - create_datacards_py = os.path.join(self.ana_path(), "StatInference", "dc_make", "create_datacards.py") + create_datacards_py = os.path.join( + self.ana_path(), "StatInference", "dc_make", "create_datacards.py" + ) base_input_dir_remote = self.input()[0].parent.parent.parent with base_input_dir_remote.localize("r") as base_dir_local: cmd = [ @@ -72,17 +76,25 @@ def get_eras(self): statInf_entry = self.global_params["StatInference"] config = os.path.join(self.ana_path(), statInf_entry["config"]) import yaml + with open(config, "r") as f: data = yaml.safe_load(f) return data.get("eras", [self.period]) def requires(self): - return [ CreateDatacardsTask.req(self, period=e, branches=()) for e in self.get_eras() ] + return [ + CreateDatacardsTask.req(self, period=e, branches=()) + for e in self.get_eras() + ] def output(self): return { "limits": self.local_target("limits.npz"), - "datacards": law.LocalDirectoryTarget(os.path.join(self.ana_data_path(), self.version, "Datacards", "combined")) + "datacards": law.LocalDirectoryTarget( + os.path.join( + self.ana_data_path(), self.version, "Datacards", "combined" + ) + ), } def run(self): @@ -91,31 +103,36 @@ def run(self): era_cards = {} import glob import re - + for e in eras: - create_dc_br0 = CreateDatacardsTask.req(self, period=e, branch=0, branches=()) + create_dc_br0 = CreateDatacardsTask.req( + self, period=e, branch=0, branches=() + ) output_dir = create_dc_br0.output().abspath cards = glob.glob(os.path.join(output_dir, "*.txt")) era_cards[e] = cards datacards.extend(cards) - limits = yield MergeResonantLimits(version=self.version, datacards=tuple(datacards)) + limits = yield MergeResonantLimits( + version=self.version, datacards=tuple(datacards) + ) print(f"Merged limits: {limits}") - + import shutil + self.output()["limits"].parent.touch() shutil.copy2(limits.path, self.output()["limits"].path) - + out_dc_dir = self.output()["datacards"] out_dc_dir.touch() - + masses = set() for e, cards in era_cards.items(): for c in cards: - m = re.search(r'_(\d+)\.txt$', c) + m = re.search(r"_(\d+)\.txt$", c) if m: masses.add(m.group(1)) - + for mass in masses: combine_args = [] for e in eras: @@ -123,9 +140,10 @@ def run(self): if c.endswith(f"_{mass}.txt"): combine_args.append(f"{e}={c}") break - + if combine_args: import subprocess + cmd = ["combineCards.py"] + combine_args out_file = os.path.join(out_dc_dir.path, f"combined_{mass}.txt") with open(out_file, "w") as f: @@ -134,17 +152,18 @@ def run(self): class ResonantLimitsAndHistPlotTask(Task): workflow = luigi.Parameter(default=law.parameter.NO_STR) - + def get_eras(self): statInf_entry = self.global_params["StatInference"] config = os.path.join(self.ana_path(), statInf_entry["config"]) import yaml + with open(config, "r") as f: data = yaml.safe_load(f) return data.get("eras", [self.period]) def requires(self): - reqs = [ ResonantLimitsTask.req(self) ] + reqs = [ResonantLimitsTask.req(self)] for e in self.get_eras(): reqs.append(HistPlotTask.req(self, period=e)) return reqs