diff --git a/.github/workflows/cross-section-check.yaml b/.github/workflows/cross-section-check.yaml index 9c8e2060..29d4d582 100644 --- a/.github/workflows/cross-section-check.yaml +++ b/.github/workflows/cross-section-check.yaml @@ -47,4 +47,4 @@ jobs: - name: Check cross-sections if: ${{ steps.changed_files.outputs.has_changes == 'true' }} - run: python3 test/checkCrossSections.py Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 + run: python3 test/checkCrossSections.py Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 Run3_2026 diff --git a/.github/workflows/ds-consistency-check.yaml b/.github/workflows/ds-consistency-check.yaml index 6e2c4fa8..87f741c9 100644 --- a/.github/workflows/ds-consistency-check.yaml +++ b/.github/workflows/ds-consistency-check.yaml @@ -45,8 +45,8 @@ jobs: - name: Check dataset configs consistency if: ${{ steps.changed_files.outputs.has_ds_configs == 'true' }} - run: python3 test/checkDatasetConfigConsistency.py --exception config/dataset_exceptions.yaml Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 + run: python3 test/checkDatasetConfigConsistency.py --exception config/dataset_exceptions.yaml Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 Run3_2026 - name: Check dataset naming if: ${{ steps.changed_files.outputs.has_ds_configs == 'true' }} - run: python3 test/checkDatasetNaming.py --rules config/dataset_naming_rules.yaml Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 + run: python3 test/checkDatasetNaming.py --rules config/dataset_naming_rules.yaml Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 Run3_2026 diff --git a/AnaProd/MergeAnaTuples.py b/AnaProd/MergeAnaTuples.py index d7f89272..793d23c4 100644 --- a/AnaProd/MergeAnaTuples.py +++ b/AnaProd/MergeAnaTuples.py @@ -55,17 +55,13 @@ """) -def combineAnaCaches(anaCaches, processors): - """ - Combine multiple anaCaches into one. - Merges denominators, runtimes, and any processor-provided sections (like DY_stitching). - """ - if len(anaCaches) == 0: - raise RuntimeError("addAnaCaches: no anaCaches provided") +def _combine_denominator_map(anaCaches, processors, key): denominator = {} anaCache_processors = set() for anaCache in anaCaches: - for source, source_entry in anaCache["denominator"].items(): + if key not in anaCache: + raise RuntimeError(f"combineAnaCaches: cache is missing '{key}'") + for source, source_entry in anaCache[key].items(): if source not in denominator: denominator[source] = {} for scale in getScales(source): @@ -82,24 +78,35 @@ def combineAnaCaches(anaCaches, processors): entries = [] for anaCache in anaCaches: if ( - source in anaCache["denominator"] - and scale in anaCache["denominator"][source] - and processor in anaCache["denominator"][source][scale] + source in anaCache[key] + and scale in anaCache[key][source] + and processor in anaCache[key][source][scale] ): - entries.append( - anaCache["denominator"][source][scale][processor] - ) + entries.append(anaCache[key][source][scale][processor]) else: raise RuntimeError( - f"combineAnaCaches: missing entry for {source}/{scale}/{processor} in one of the caches" + f"combineAnaCaches: missing entry for {key}/{source}/{scale}/{processor} in one of the caches" ) denominator[source][scale][processor] = processors[ processor ].onAnaCache_combineAnaCaches(entries) + return denominator + +def combineAnaCaches(anaCaches, processors): + """ + Combine multiple anaCaches into one. + Merges denominators, runtimes, and any processor-provided sections (like DY_stitching). + """ + if len(anaCaches) == 0: + raise RuntimeError("addAnaCaches: no anaCaches provided") anaCacheSum = { - "denominator": denominator, + "denominator": _combine_denominator_map(anaCaches, processors, "denominator"), } + if any("denominator_cmb" in anaCache for anaCache in anaCaches): + anaCacheSum["denominator_cmb"] = _combine_denominator_map( + anaCaches, processors, "denominator_cmb" + ) return anaCacheSum diff --git a/AnaProd/anaTupleProducer.py b/AnaProd/anaTupleProducer.py index fdb07bdc..98c3f521 100644 --- a/AnaProd/anaTupleProducer.py +++ b/AnaProd/anaTupleProducer.py @@ -16,6 +16,7 @@ import FLAF.Common.ReportTools as ReportTools import FLAF.Common.triggerSel as Triggers from FLAF.Common.Setup import Setup +from FLAF.Common.shared_mc import shared_mc_in_era_expr, shared_mc_split from Corrections.Corrections import Corrections from Corrections.lumi import LumiFilter from Corrections.CorrectionsCore import central, getScales, getSystName @@ -180,19 +181,30 @@ def createAnatuple( if "pu" in corrections.to_apply and compute_unc_variations: shape_sources += puWeightProducer.uncSource - report["denominator"] = {} - for shape_unc_source in shape_sources: - report["denominator"][shape_unc_source] = {} - for shape_unc_scale in getScales(shape_unc_source): - report["denominator"][shape_unc_source][shape_unc_scale] = {} - for p_name, p_instance in processor_instances.items(): - report["denominator"][shape_unc_source][shape_unc_scale][ - p_name - ] = p_instance.onAnaCache_initializeDenomEntry() + shared_mc = None if isData else setup.global_params.get("shared_mc") + shared_mc_expr = None + if shared_mc: + split_mod, lo, hi, _ = shared_mc_split(period, shared_mc) + shared_mc_expr = shared_mc_in_era_expr(split_mod, lo, hi) + + def initializeDenomReport(key): + report[key] = {} + for shape_unc_source in shape_sources: + report[key][shape_unc_source] = {} + for shape_unc_scale in getScales(shape_unc_source): + report[key][shape_unc_source][shape_unc_scale] = {} + for p_name, p_instance in processor_instances.items(): + report[key][shape_unc_source][shape_unc_scale][ + p_name + ] = p_instance.onAnaCache_initializeDenomEntry() + + initializeDenomReport("denominator") + if shared_mc_expr: + initializeDenomReport("denominator_cmb") gen_weight_name = "weight_gen" - def updateDenomEntry(rdf): + def updateDenomEntry(rdf, report_key, branch_prefix): for p_instance in processor_instances.values(): rdf = p_instance.onAnaCache_prepareDataFrame(rdf) @@ -203,10 +215,10 @@ def updateDenomEntry(rdf): if "pu" in corrections.to_apply: weights_to_apply.append(f"weight_pu_{shape_unc_scale}") for p_name, p_instance in processor_instances.items(): - output_branch_name = f"weight_denom_{p_name}_{shape_unc_name}" - report["denominator"][shape_unc_source][shape_unc_scale][p_name] = ( + output_branch_name = f"{branch_prefix}_{p_name}_{shape_unc_name}" + report[report_key][shape_unc_source][shape_unc_scale][p_name] = ( p_instance.onAnaCache_updateDenomEntry( - report["denominator"][shape_unc_source][shape_unc_scale][ + report[report_key][shape_unc_source][shape_unc_scale][ p_name ], rdf, @@ -228,7 +240,14 @@ def updateDenomEntry(rdf): data_frame = data_frame.Define(gen_weight_name, genWeight_def) if "pu" in corrections.to_apply: data_frame = corrections.pu.getWeight(data_frame) - updateDenomEntry(data_frame) + updateDenomEntry(data_frame, "denominator", "weight_denom") + if shared_mc_expr: + data_frame = data_frame.Define("__shared_mc_in_era", shared_mc_expr) + updateDenomEntry( + data_frame.Filter("__shared_mc_in_era"), + "denominator_cmb", + "weight_denom_cmb", + ) # if isData: json_dict_for_cache['RunLumi'] = unique_run_lumi if range is not None: @@ -388,19 +407,23 @@ def updateDenomEntry(rdf): report["run_lumi_ranges"] = runLumiRanges - for shape_unc_source in shape_sources: - for shape_unc_scale in getScales(shape_unc_source): - for p_name, p_instance in processor_instances.items(): - report["denominator"][shape_unc_source][shape_unc_scale][p_name] = ( - p_instance.onAnaCache_materializeDenomEntry( - report["denominator"][shape_unc_source][shape_unc_scale][p_name] + denom_keys = ["denominator"] + if "denominator_cmb" in report: + denom_keys.append("denominator_cmb") + for denom_key in denom_keys: + for shape_unc_source in shape_sources: + for shape_unc_scale in getScales(shape_unc_source): + for p_name, p_instance in processor_instances.items(): + report[denom_key][shape_unc_source][shape_unc_scale][p_name] = ( + p_instance.onAnaCache_materializeDenomEntry( + report[denom_key][shape_unc_source][shape_unc_scale][p_name] + ) ) - ) - report["denominator"][shape_unc_source][shape_unc_scale][p_name] = ( - p_instance.onAnaCache_finalizeDenomEntry( - report["denominator"][shape_unc_source][shape_unc_scale][p_name] + report[denom_key][shape_unc_source][shape_unc_scale][p_name] = ( + p_instance.onAnaCache_finalizeDenomEntry( + report[denom_key][shape_unc_source][shape_unc_scale][p_name] + ) ) - ) hist_time = ROOT.TH1D(f"time", f"time", 1, 0, 1) end_time = datetime.datetime.now() diff --git a/Analysis/HistProducerFromNTuple.py b/Analysis/HistProducerFromNTuple.py index d57282c2..1df76138 100644 --- a/Analysis/HistProducerFromNTuple.py +++ b/Analysis/HistProducerFromNTuple.py @@ -13,6 +13,12 @@ from FLAF.Common.Setup import Setup from FLAF.RunKit.run_tools import ps_call from FLAF.Analysis.HistTupleProducer import DefineBinnedColumn +from FLAF.Analysis.histFromNtupleBatch import ( + count_booked_hists, + iter_hist_batches, + n_cut_slots, + unc_scale_pairs, +) def find_keys(inFiles_list): @@ -185,6 +191,14 @@ def BuildAllHistActions( parser.add_argument("--LAWrunVersion", required=True, type=str) parser.add_argument("--nMT", type=int, default=8) parser.add_argument("--user-custom", type=str, default=None) + parser.add_argument( + "--max-hists", + type=int, + default=None, + help="Max histograms booked in one RDataFrame pass, counting every " + "(variable, selection, unc, scale) including Up/Down. " + "0 disables batching. Default: hist_from_ntuple_max_hists from config, else 4000.", + ) args = parser.parse_args() ROOT.EnableImplicitMT(args.nMT) @@ -281,33 +295,62 @@ def BuildAllHistActions( ) if all_trees: - # Open a tmp ROOT file per variable, then register all histogram actions sharing one - # filtered RDataFrame node per selection across variables (see BuildAllHistActions). - # Collecting every action before triggering lets ROOT execute them in a single - # event-loop pass over the input files. - # Write each variable's histograms directly into its final, compressed output file. - # SaveHist persists objects via WriteTObject as the actions run, so once the single - # event loop has executed we just close the files -- no per-variable hadd recompress - # pass (209 == LZMA level 9, matching the previous `hadd -f209` output compression). + # Open one compressed output file per variable. SaveHist writes into it as + # each batch's event loop runs (209 == LZMA level 9). var_tmp_files = {} for var in vars_to_process: out_path = os.path.join(args.outDir, f"{var}.root") out_root_file = ROOT.TFile(out_path, "RECREATE", "", 209) var_tmp_files[var] = (out_path, out_root_file) - all_save_fns = BuildAllHistActions( - uncs_to_compute, - unc_cfg_dict, - all_trees, - vars_to_process, - key_filter_dict, - further_cuts, - treeName, - var_tmp_files, + if args.max_hists is not None: + max_hists = args.max_hists + else: + max_hists = int(setup.global_params.get("hist_from_ntuple_max_hists", 4000)) + n_total = count_booked_hists( + max(1, len(vars_to_process)), + max(1, len(key_filter_dict)), + n_cut_slots(further_cuts), + max(1, len(unc_scale_pairs(uncs_to_compute))), + ) + batches = list( + iter_hist_batches( + uncs_to_compute, + key_filter_dict, + further_cuts, + vars_to_process, + max_hists, + ) + ) + print( + f"Booking {n_total} histograms " + f"({len(vars_to_process)} vars × {len(key_filter_dict)} keys × " + f"{n_cut_slots(further_cuts)} cuts × " + f"{len(unc_scale_pairs(uncs_to_compute))} unc/scales); " + f"max_hists={max_hists} → {len(batches)} batch(es)" ) - for fn in all_save_fns: - fn() + for batch_idx, (b_uncs, b_keys, b_cuts, b_vars) in enumerate(batches, start=1): + n_batch = count_booked_hists( + max(1, len(b_vars)), + max(1, len(b_keys)), + n_cut_slots(b_cuts), + max(1, len(unc_scale_pairs(b_uncs))), + ) + print(f"Histogram batch {batch_idx}/{len(batches)}: {n_batch} hists") + save_fns = BuildAllHistActions( + b_uncs, + unc_cfg_dict, + all_trees, + b_vars, + b_keys, + b_cuts, + treeName, + var_tmp_files, + ) + for fn in save_fns: + fn() + del save_fns for var in vars_to_process: _, out_root_file = var_tmp_files[var] diff --git a/Analysis/histFromNtupleBatch.py b/Analysis/histFromNtupleBatch.py new file mode 100644 index 00000000..4949557a --- /dev/null +++ b/Analysis/histFromNtupleBatch.py @@ -0,0 +1,119 @@ +"""Split HistFromNtuple histogram booking so one RDataFrame pass stays within a hist budget. + +The booked-histogram count is +``n_vars × n_keys × n_cuts × n_scale_slots``, where ``n_scale_slots`` counts +every (uncertainty, scale) pair (Central plus each Up/Down). When that product +exceeds ``max_hists``, the work is partitioned so each batch books at most +``max_hists`` histograms. Axes are split in this order: variables, then extra +cuts, then selection keys, then (unc, scale). +""" + + +def n_cut_slots(further_cuts): + return max(1, len(further_cuts) if further_cuts else 0) + + +def unc_scale_pairs(uncs_to_compute): + pairs = [] + for unc, scales in uncs_to_compute.items(): + for scale in scales: + pairs.append((unc, scale)) + return pairs + + +def pairs_to_uncs(pairs): + out = {} + for unc, scale in pairs: + out.setdefault(unc, []).append(scale) + return out + + +def count_booked_hists(n_vars, n_keys, n_cuts, n_scales): + return n_vars * n_keys * n_cuts * n_scales + + +def _chunks(items, size): + size = max(1, int(size)) + if not items: + yield items + return + for i in range(0, len(items), size): + yield items[i : i + size] + + +def iter_hist_batches( + uncs_to_compute, key_filter_dict, further_cuts, vars_to_process, max_hists +): + """Yield ``(uncs_dict, keys_dict, cuts_dict, vars_list)`` batches. + + Each batch books at most ``max_hists`` histograms. ``max_hists < 1`` disables + splitting (one batch with everything). + """ + pairs = unc_scale_pairs(uncs_to_compute) + keys = list(key_filter_dict.items()) + cuts = list(further_cuts.items()) if further_cuts else [] + vars_list = list(vars_to_process) + n_total = count_booked_hists( + max(1, len(vars_list)), + max(1, len(keys)), + n_cut_slots(further_cuts), + max(1, len(pairs)), + ) + if max_hists < 1 or n_total <= max_hists: + yield uncs_to_compute, key_filter_dict, further_cuts, vars_list + return + + for b_pairs, b_keys, b_cuts, b_vars in _split_axes( + pairs, keys, cuts, vars_list, max_hists + ): + yield pairs_to_uncs(b_pairs), dict(b_keys), dict(b_cuts), b_vars + + +def _n_hists(pairs, keys, cuts, vars_list): + return count_booked_hists( + max(1, len(vars_list)), + max(1, len(keys)), + max(1, len(cuts) if cuts else 1), + max(1, len(pairs)), + ) + + +def _split_axes(pairs, keys, cuts, vars_list, max_hists): + if _n_hists(pairs, keys, cuts, vars_list) <= max_hists: + yield pairs, keys, cuts, vars_list + return + + per_var = _n_hists(pairs, keys, cuts, [None]) + if per_var <= max_hists: + for d in _chunks(vars_list, max_hists // per_var): + yield pairs, keys, cuts, d + return + + for d in _chunks(vars_list, 1): + if cuts: + per_cut = _n_hists(pairs, keys, [None], d) + if per_cut <= max_hists: + for c in _chunks(cuts, max_hists // per_cut): + yield pairs, keys, c, d + continue + for c in _chunks(cuts, 1): + yield from _split_keys_and_scales(pairs, keys, c, d, max_hists) + else: + yield from _split_keys_and_scales(pairs, keys, cuts, d, max_hists) + + +def _split_keys_and_scales(pairs, keys, cuts, vars_list, max_hists): + if _n_hists(pairs, keys, cuts, vars_list) <= max_hists: + yield pairs, keys, cuts, vars_list + return + per_key = _n_hists(pairs, [None], cuts, vars_list) + if per_key <= max_hists: + for b in _chunks(keys, max_hists // per_key): + yield pairs, b, cuts, vars_list + return + for b in _chunks(keys, 1): + if _n_hists(pairs, b, cuts, vars_list) <= max_hists: + yield pairs, b, cuts, vars_list + continue + for a in _chunks(pairs, 1): + yield a, b, cuts, vars_list diff --git a/Analysis/tasks.py b/Analysis/tasks.py index 8b9f771b..cfd54e18 100644 --- a/Analysis/tasks.py +++ b/Analysis/tasks.py @@ -304,9 +304,16 @@ def _build_branch_map(self): payload_producers = self.global_params.get("payload_producers") if payload_producers: + needed_cache_producers = { + cfg["normCacheProducer"] + for cfg in self.global_params.get("corrections", {}).values() + if isinstance(cfg, dict) and cfg.get("normCacheProducer") + } for producer_name, producer_cfg in payload_producers.items(): if not producer_cfg.get("is_global", False): continue + if producer_name not in needed_cache_producers: + continue if producer_cfg.get("needs_aggregation", False): aggregate_list.append(producer_name) else: @@ -512,13 +519,10 @@ class HistFromNtupleProducerTask(Task, HTCondorWorkflow, law.LocalWorkflow): max_runtime = copy_param(HTCondorWorkflow.max_runtime, 10.0) n_cpus = copy_param(HTCondorWorkflow.n_cpus, 2) variables = luigi.Parameter(default="") - # Number of input ntuple files processed per job. A job reads its chunk of files once - # and fills the histograms for ALL active variables in a single RDataFrame event-loop - # pass (one traversal fills every booked histogram, so the dominant per-event cost -- - # I/O + filter/define evaluation -- is shared across variables). Work is therefore split - # by FILES, never by variable: a per-variable split re-reads the same events once per - # variable batch, which is what made big datasets (e.g. TTtoLNu2Q with ~500 files read - # 10x) exceed the wall-time limit. Big datasets are parallelized by chunking their files. + # Number of input ntuple files processed per job. LAW branches stay split by + # files. If the booked-histogram count (variables x selections x unc/scale, + # including Up/Down) exceeds hist_from_ntuple_max_hists, the producer repeats + # the event loop in batches inside the same job. n_files_per_job = luigi.IntParameter(default=20) @property @@ -611,13 +615,10 @@ def _build_branch_map(self): ) in HistTupleBranchMap.items(): dataset_to_branches.setdefault(histTuple_dataset_name, []).append(prod_br) - # Each (dataset, file-chunk) is its own branch/job: a job localizes its chunk of - # input files once and produces the histograms for ALL active variables in a single - # event-loop pass. Variables are NOT split across jobs (the event loop reads each - # event once and fills every booked histogram in that pass, so adding variables is - # nearly free; splitting them would instead re-read the same events per batch). Big - # datasets are parallelized by chunking their files into groups of n_files_per_job, - # so no single job has to read all ~500 files of e.g. TTtoLNu2Q. + # Each (dataset, file-chunk) is its own branch/job. Big datasets are + # parallelized by chunking files (n_files_per_job). Histogram-count + # batching, if needed, happens inside the producer — not as extra LAW + # branches — so inputs are localized once. n_files = max(1, self.n_files_per_job) idx = 0 for dataset_name, prod_br_list in sorted(dataset_to_branches.items()): @@ -701,10 +702,8 @@ def run(self): shutil.rmtree(job_home) return - # Localize this chunk's input files concurrently, then run the producer once over all - # of them. A chunk is small by construction (n_files_per_job), and the producer fills - # every variable in a single event-loop pass, writing one .root per variable -- - # so there is nothing to stage in waves or merge afterwards. + # Localize this chunk's input files concurrently, then run the producer + # over them. The producer may re-loop the same files in histogram batches. max_dl = max(1, int(self.global_params.get("max_simultaneous_downloads", 8))) out_dir = os.path.join(job_home, "hists") os.makedirs(out_dir, exist_ok=True) @@ -752,6 +751,9 @@ def _localize(inp): cmd.extend(["--customisations", self.customisations]) if self.user_custom: cmd.extend(["--user-custom", self.user_custom]) + max_hists = self.global_params.get("hist_from_ntuple_max_hists") + if max_hists is not None: + cmd.extend(["--max-hists", str(int(max_hists))]) cmd.extend(local_inputs) ps_call(cmd, verbose=1) @@ -888,10 +890,8 @@ def run(self): uncNames = ["Central"] unc_cfg_dict = self.setup.weights_config - uncs_to_exclude = ( - self.global_params["uncs_to_exclude"][self.period] - if "uncs_to_exclude" in self.global_params.keys() - else [] + uncs_to_exclude = (self.global_params.get("uncs_to_exclude") or {}).get( + self.period, [] ) compute_unc_histograms = ( customisation_dict["compute_unc_histograms"] == "True" diff --git a/Common/Setup.py b/Common/Setup.py index fd1bdb25..640b93ee 100644 --- a/Common/Setup.py +++ b/Common/Setup.py @@ -77,6 +77,8 @@ def __init__( f"No configuration files {file_names} found in paths {paths}." ) config_dict = yaml.safe_load(yaml_str) + if config_dict is None: + config_dict = {} if special_items_prefix is not None: config_dict = { k: v @@ -284,6 +286,19 @@ def __init__( extra_file=user_custom_file, ) + reuse_mc_era = self.global_params.get("reuse_mc_from_era") + if reuse_mc_era and "shared_mc" not in self.global_params: + reuse_global = Config( + f"global_{reuse_mc_era}", + [ + os.path.join(self.ana_path, "FLAF", "config", reuse_mc_era), + os.path.join(self.ana_path, "config", reuse_mc_era), + ], + ["global.yaml"], + ) + if "shared_mc" in reuse_global: + self.global_params["shared_mc"] = reuse_global["shared_mc"] + apply_customisations(self.global_params, customisations) phys_models = Config( @@ -451,6 +466,20 @@ def collect_base_processes(p_name, parent_name=None): self.parent_processes[process_name] = processes[process_name] all_datasets = Config("datasets", self.config_path_order, ["datasets.yaml"]) + # Copy MC/signal datasets from another era (2025 reuses 2024 Summer24 MC). + reuse_mc_era = self.global_params.get("reuse_mc_from_era") + if reuse_mc_era: + reuse_paths = [ + os.path.join(self.ana_path, "FLAF", "config", reuse_mc_era), + os.path.join(self.ana_path, "config", reuse_mc_era), + ] + reused = Config(f"datasets_{reuse_mc_era}", reuse_paths, ["datasets.yaml"]) + for name, desc in reused.items(): + if name in all_datasets.config_dict: + continue + if isinstance(desc, dict) and "eraLetter" in desc: + continue + all_datasets.config_dict[name] = desc active_datasets = {} for process_name, process in self.base_processes.items(): for dataset_name in process.get("datasets", []): diff --git a/Common/shared_mc.py b/Common/shared_mc.py new file mode 100644 index 00000000..6bf0dbe7 --- /dev/null +++ b/Common/shared_mc.py @@ -0,0 +1,44 @@ +def shared_mc_split(era, shared_mc): + """Residue split of one MC sample across the eras in ``shared_mc``. + + Config shape:: + + shared_mc: + split_modulus: 38 + eras: + Run3_2024: [0, 16] + Run3_2025: [17, 33] + Run3_2026: [34, 37] + + Returns ``(split_mod, lo, hi, frac)``. An event is assigned to ``era`` when + ``lo <= (event % split_mod) <= hi``. ``frac`` is that residue share. + """ + eras = shared_mc.get("eras") + if not eras: + raise RuntimeError("shared_mc requires an 'eras' map of [lo, hi] ranges") + if era not in eras: + raise RuntimeError(f"shared_mc has no era '{era}'") + split_mod = int(shared_mc["split_modulus"]) + if split_mod <= 0: + raise RuntimeError("shared_mc split_modulus must be positive") + rng = eras[era] + if not isinstance(rng, (list, tuple)) or len(rng) != 2: + raise RuntimeError(f"shared_mc era '{era}' must be a [lo, hi] range") + lo, hi = int(rng[0]), int(rng[1]) + if not (0 <= lo <= hi < split_mod): + raise RuntimeError( + f"shared_mc era '{era}' range [{lo}, {hi}] is outside " + f"[0, {split_mod - 1}]" + ) + frac = (hi - lo + 1) / float(split_mod) + return split_mod, lo, hi, frac + + +def shared_mc_in_era(event, split_mod, lo, hi): + return lo <= (int(event) % split_mod) <= hi + + +def shared_mc_in_era_expr(split_mod, lo, hi): + """C++ expression: 1 if this event is assigned to the era range, else 0.""" + residue = f"(static_cast(event) % {int(split_mod)}ULL)" + return f"static_cast({residue} >= {int(lo)}ULL && {residue} <= {int(hi)}ULL)" diff --git a/config/Run3_2024/datasets.yaml b/config/Run3_2024/datasets.yaml index e6704b91..851dd1e0 100644 --- a/config/Run3_2024/datasets.yaml +++ b/config/Run3_2024/datasets.yaml @@ -279,19 +279,19 @@ ParkingVBF0_Run2024D: eraLetter: D nanoAOD: v15: /ParkingVBF0/Run2024D-MINIv6NANOv15-v1/NANOAOD -ParkingVBF0/Run2024E: +ParkingVBF0_Run2024E: eraLetter: E nanoAOD: v15: /ParkingVBF0/Run2024E-MINIv6NANOv15-v1/NANOAOD -ParkingVBF0/Run2024F: +ParkingVBF0_Run2024F: eraLetter: F nanoAOD: v15: /ParkingVBF0/Run2024F-MINIv6NANOv15-v3/NANOAOD -ParkingVBF0/Run2024G: +ParkingVBF0_Run2024G: eraLetter: G nanoAOD: v15: /ParkingVBF0/Run2024G-MINIv6NANOv15-v3/NANOAOD -ParkingVBF0/Run2024H: +ParkingVBF0_Run2024H: eraLetter: H nanoAOD: v15: /ParkingVBF0/Run2024H-MINIv6NANOv15-v2/NANOAOD @@ -703,11 +703,11 @@ DYto2Tau_M_10to50_amcatnloFXFX: generator: amcatnlo nanoAOD: v15: /DYto2Tau-2Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM -# DYto2Tau_M_50_amcatnloFXFX: -# crossSection: DYto2L_M_50_amcatnloFXFX_singleFlavor -# generator: amcatnlo -# nanoAOD: - # v15: /DYto2Tau-2Jets_Bin-MLL-50_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v7/NANOAODSIM +DYto2Tau_M_50_amcatnloFXFX: + crossSection: DYto2L_M_50_amcatnloFXFX_singleFlavor + generator: amcatnlo + nanoAOD: + v15: /DYto2Tau-2Jets_Bin-MLL-50_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v7/NANOAODSIM ### other samples with 1 pT value and amcatnlo fxfx ?? # # DYto2E_M_50_PTLL_100_amcatnloFXFX: @@ -1013,19 +1013,19 @@ DYto2E_MLL_10to50_madgraphMLM: crossSection: DYto2L_M_10to50_amcatnloFXFX_singleFlavor generator: madgraphMLM nanoAOD: - v12: /DYto2E-4Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + v15: /DYto2E-4Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM DYto2Mu_MLL_10to50_madgraphMLM: crossSection: DYto2L_M_10to50_amcatnloFXFX_singleFlavor generator: madgraphMLM nanoAOD: - v12: /DYto2Mu-4Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + v15: /DYto2Mu-4Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM DYto2Tau_MLL_10to50_madgraphMLM: crossSection: DYto2L_M_10to50_amcatnloFXFX_singleFlavor generator: madgraphMLM nanoAOD: - v12: /DYto2Tau-4Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + v15: /DYto2Tau-4Jets_Bin-MLL-10to50_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM ############## GGH ############## @@ -1070,6 +1070,21 @@ GluGluHto2Wto2L2Nu_M125: generator: powheg nanoAOD: v15: /GluGluHto2Wto2L2Nu_Par-M-125_TuneCP5_13p6TeV_powheg-jhugen-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +GluGlutoContinto2Zto4E: + crossSection: GluGlutoContinto2Zto4E + generator: powheg + nanoAOD: + v15: /GluGlutoContinto2Zto4E_TuneCP5_13p6TeV_mcfm-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +GluGlutoContinto2Zto4Mu: + crossSection: GluGlutoContinto2Zto4Mu + generator: powheg + nanoAOD: + v15: /GluGlutoContinto2Zto4Mu_TuneCP5_13p6TeV_mcfm-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +GluGlutoContinto2Zto4Tau: + crossSection: GluGlutoContinto2Zto4Tau + generator: powheg + nanoAOD: + v15: /GluGlutoContinto2Zto4Tau_TuneCP5_13p6TeV_mcfm-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM ## QCD ### QCD_HT1000to1200_MLM: crossSection: QCD_HT1000to1200_MLM @@ -1241,7 +1256,7 @@ TTZZ_ZZto4B: crossSection: TTZZ_ZZto4B generator: madgraph nanoAOD: - v15: /TTZZ-ZZto4B_TuneCP5_13p6TeV_madgraph-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM/ + v15: /TTZZ-ZZto4B_TuneCP5_13p6TeV_madgraph-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM TTZ_Zto2Q: crossSection: TTZ_Zto2Q generator: amcatnlo @@ -1268,12 +1283,12 @@ VBFHto2Tau_UncorrelatedDecay_UnFiltered: crossSection: VBFHto2Tau generator: powheg nanoAOD: - v15: /VBFH-Hto2TauUncorrelatedDecay_Fil-TauFilter_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + v15: /VBFH-Hto2TauUncorrelatedDecay_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM VBFHto2Tau_UncorrelatedDecay_Filtered: crossSection: VBFHto2Tau_Filtered generator: powheg nanoAOD: - v15: /VBFH-Hto2TauUncorrelatedDecay_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + v15: /VBFH-Hto2TauUncorrelatedDecay_Fil-TauFilter_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM VBFHto2Wto2L2Nu_M125: crossSection: VBFHToWWTo2L2Nu_M125 generator: powheg @@ -1285,11 +1300,21 @@ WminusH_Hto2B_WtoLNu: generator: powheg nanoAOD: v15: /WminusH-WtoLNu-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +WminusH_Hto2B_Wto2Q: + crossSection: WminusH_Hbb_Wqq + generator: powheg + nanoAOD: + v15: /WminusH-Wto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM WplusH_Hto2B_WtoLNu: crossSection: WplusH_Hbb_Wlnu generator: powheg nanoAOD: v15: /WplusH-WtoLNu-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +WplusH_Hto2B_Wto2Q: + crossSection: WplusH_Hbb_Wqq + generator: powheg + nanoAOD: + v15: /WplusH-Wto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM WminusHto2Tau_UncorrelatedDecay_Filtered: crossSection: WminusHto2Tau_Filtered generator: powheg @@ -1315,11 +1340,11 @@ ZHto2Tau_UncorrelatedDecay_Filtered: generator: powheg nanoAOD: v15: /ZH-Hto2TauUncorrelatedDecay_Fil-TauFilter_Par-M-125_TuneCP5_13p6TeV_powhegMINNLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v6/NANOAODSIM -# ZHto2Tau_UncorrelatedDecay_UnFiltered: -# crossSection: ZHto2Tau -# generator: powheg -# nanoAOD: - # v15: N/A +ZHto2Tau_UncorrelatedDecay_UnFiltered: + crossSection: ZHto2Tau + generator: powheg + nanoAOD: + v15: /ZH-Hto2TauUncorrelatedDecay_Par-M-125_TuneCP5_13p6TeV_powhegMINNLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v6/NANOAODSIM ZH_Hto2B_Zto2L: crossSection: ZH_Hbb_Zll generator: powheg @@ -1346,11 +1371,11 @@ WtoLNu_1J_madgraphMLM: generator: madgraphMLM nanoAOD: v15: /WtoLNu-4Jets_Bin-1J_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM -WtoLNu_2J_amcatnloFXFX: - crossSection: WtoLNu_2J_amcatnloFXFX - generator: amcatnlo - nanoAOD: - v15: +# WtoLNu_2J_amcatnloFXFX: +# crossSection: WtoLNu_2J_amcatnloFXFX +# generator: amcatnlo +# nanoAOD: +# v15: WtoLNu_2J_madgraphMLM: crossSection: WtoLNu_2J_madgraphMLM generator: madgraphMLM @@ -1486,11 +1511,13 @@ WW: WZ: crossSection: WZ generator: pythia8 - nanoAOD: /WZ_TuneCP5_13p6TeV_pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + nanoAOD: + v15: /WZ_TuneCP5_13p6TeV_pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM ZZ: crossSection: ZZ generator: pythia8 - nanoAOD: /ZZ_TuneCP5_13p6TeV_pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + nanoAOD: + v15: /ZZ_TuneCP5_13p6TeV_pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM ## VV non inclusive ## WWto2L2Nu_powheg: crossSection: WWto2L2Nu diff --git a/config/Run3_2024/global.yaml b/config/Run3_2024/global.yaml index 62234c0c..0358a6ad 100644 --- a/config/Run3_2024/global.yaml +++ b/config/Run3_2024/global.yaml @@ -1,5 +1,17 @@ era: Run3_2024 luminosity: 109948.18 # pb-1, https://twiki.cern.ch/twiki/bin/view/CMS/PdmVRun3Analysis#DATA_AN2 command brilcalc lumi -b "STABLE BEAMS" --normtag /cvmfs/cms-bril.cern.ch/cms-lumi-pog/Normtags/normtag_PHYSICS.json -u /pb -i Cert_Collisions2024_378981_386951_Golden.json +# Same Summer24 MC is processed once per year (jet/PU/tau corrections differ). +# weight_base = all events, this year's lumi, full-sample denominator. +# weight_base_cmb = in-era events, in-era denominator; 0 otherwise. +# Residue map 17:17:4 / 38 matches recorded lumis (109948.18 : 110730.86 : 25843.26) +# better than 9:9:2 / 20 (2026 was 4.6% low). Actual event split is not assumed. +shared_mc: + split_modulus: 38 + eras: + Run3_2024: [ 0, 16 ] + Run3_2025: [ 17, 33 ] + Run3_2026: [ 34, 37 ] +weight_base_branch: weight_base nano_version: v15 crossSectionsFile: FLAF/config/crossSections13p6TeV.yaml MET_flags: # https://twiki.cern.ch/twiki/bin/view/CMS/MissingETOptionalFiltersRun2#Run_3_2024_data_and_MC_Recommend diff --git a/config/Run3_2025/global.yaml b/config/Run3_2025/global.yaml index 36e30b2b..a16bbe14 100644 --- a/config/Run3_2025/global.yaml +++ b/config/Run3_2025/global.yaml @@ -1,5 +1,9 @@ era: Run3_2025 luminosity: 110730.86 # pb-1, https://twiki.cern.ch/twiki/bin/view/CMS/PdmVRun3Analysis#DATA_AN2 brilcalc lumi -b "STABLE BEAMS" -i Cert_Collisions2025_391658_398903_Golden.json (no normtag available) +# Same Summer24 MC as 2024, processed with 2025 corrections (jet/PU/tau differ). +# shared_mc is inherited from reuse_mc_from_era. +reuse_mc_from_era: Run3_2024 +weight_base_branch: weight_base nano_version: v15 crossSectionsFile: FLAF/config/crossSections13p6TeV.yaml MET_flags: # https://twiki.cern.ch/twiki/bin/view/CMS/MissingETOptionalFiltersRun2#Run_3_2024_data_and_MC_Recommend diff --git a/config/Run3_2026/datasets.yaml b/config/Run3_2026/datasets.yaml new file mode 100644 index 00000000..c0729b66 --- /dev/null +++ b/config/Run3_2026/datasets.yaml @@ -0,0 +1,501 @@ +############## data ############## PromptReco NanoAOD from DAS (2026-08-13) +EGamma0_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /EGamma0/Run2026A-PromptReco-v1/NANOAOD +EGamma0_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /EGamma0/Run2026B-PromptReco-v1/NANOAOD +EGamma0_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /EGamma0/Run2026C-PromptReco-v1/NANOAOD +EGamma0_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /EGamma0/Run2026D-PromptReco-v1/NANOAOD +EGamma1_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /EGamma1/Run2026A-PromptReco-v1/NANOAOD +EGamma1_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /EGamma1/Run2026B-PromptReco-v1/NANOAOD +EGamma1_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /EGamma1/Run2026C-PromptReco-v1/NANOAOD +EGamma1_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /EGamma1/Run2026D-PromptReco-v1/NANOAOD +EGamma2_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /EGamma2/Run2026A-PromptReco-v1/NANOAOD +EGamma2_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /EGamma2/Run2026B-PromptReco-v1/NANOAOD +EGamma2_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /EGamma2/Run2026C-PromptReco-v1/NANOAOD +EGamma2_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /EGamma2/Run2026D-PromptReco-v1/NANOAOD +EGamma3_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /EGamma3/Run2026A-PromptReco-v1/NANOAOD +EGamma3_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /EGamma3/Run2026B-PromptReco-v1/NANOAOD +EGamma3_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /EGamma3/Run2026C-PromptReco-v1/NANOAOD +EGamma3_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /EGamma3/Run2026D-PromptReco-v1/NANOAOD +EGamma4_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /EGamma4/Run2026A-PromptReco-v1/NANOAOD +EGamma4_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /EGamma4/Run2026B-PromptReco-v1/NANOAOD +EGamma4_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /EGamma4/Run2026C-PromptReco-v1/NANOAOD +EGamma4_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /EGamma4/Run2026D-PromptReco-v1/NANOAOD +EGamma5_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /EGamma5/Run2026A-PromptReco-v1/NANOAOD +EGamma5_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /EGamma5/Run2026B-PromptReco-v1/NANOAOD +EGamma5_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /EGamma5/Run2026C-PromptReco-v1/NANOAOD +EGamma5_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /EGamma5/Run2026D-PromptReco-v1/NANOAOD +JetMET0_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /JetMET0/Run2026A-PromptReco-v1/NANOAOD +JetMET0_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /JetMET0/Run2026B-PromptReco-v1/NANOAOD +JetMET0_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /JetMET0/Run2026C-PromptReco-v1/NANOAOD +JetMET0_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /JetMET0/Run2026D-PromptReco-v1/NANOAOD +JetMET1_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /JetMET1/Run2026A-PromptReco-v1/NANOAOD +JetMET1_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /JetMET1/Run2026B-PromptReco-v1/NANOAOD +JetMET1_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /JetMET1/Run2026C-PromptReco-v1/NANOAOD +JetMET1_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /JetMET1/Run2026D-PromptReco-v1/NANOAOD +JetMET2_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /JetMET2/Run2026C-PromptReco-v1/NANOAOD +JetMET3_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /JetMET3/Run2026C-PromptReco-v1/NANOAOD +JetMET4_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /JetMET4/Run2026C-PromptReco-v1/NANOAOD +JetMET5_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /JetMET5/Run2026C-PromptReco-v1/NANOAOD +Muon0_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /Muon0/Run2026A-PromptReco-v1/NANOAOD +Muon0_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /Muon0/Run2026B-PromptReco-v1/NANOAOD +Muon0_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /Muon0/Run2026C-PromptReco-v1/NANOAOD +Muon0_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /Muon0/Run2026D-PromptReco-v1/NANOAOD +Muon1_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /Muon1/Run2026A-PromptReco-v1/NANOAOD +Muon1_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /Muon1/Run2026B-PromptReco-v1/NANOAOD +Muon1_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /Muon1/Run2026C-PromptReco-v1/NANOAOD +Muon1_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /Muon1/Run2026D-PromptReco-v1/NANOAOD +Muon2_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /Muon2/Run2026A-PromptReco-v1/NANOAOD +Muon2_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /Muon2/Run2026B-PromptReco-v1/NANOAOD +Muon2_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /Muon2/Run2026C-PromptReco-v1/NANOAOD +Muon2_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /Muon2/Run2026D-PromptReco-v1/NANOAOD +Muon3_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /Muon3/Run2026A-PromptReco-v1/NANOAOD +Muon3_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /Muon3/Run2026B-PromptReco-v1/NANOAOD +Muon3_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /Muon3/Run2026C-PromptReco-v1/NANOAOD +Muon3_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /Muon3/Run2026D-PromptReco-v1/NANOAOD +MuonEG_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /MuonEG/Run2026A-PromptReco-v1/NANOAOD +MuonEG_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /MuonEG/Run2026B-PromptReco-v1/NANOAOD +MuonEG_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /MuonEG/Run2026C-PromptReco-v1/NANOAOD +MuonEG_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /MuonEG/Run2026D-PromptReco-v1/NANOAOD +ParkingHH0_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingHH0/Run2026A-PromptReco-v1/NANOAOD +ParkingHH0_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingHH0/Run2026B-PromptReco-v1/NANOAOD +ParkingHH0_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingHH0/Run2026C-PromptReco-v1/NANOAOD +ParkingHH0_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingHH0/Run2026D-PromptReco-v1/NANOAOD +ParkingHH1_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingHH1/Run2026A-PromptReco-v1/NANOAOD +ParkingHH1_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingHH1/Run2026B-PromptReco-v1/NANOAOD +ParkingHH1_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingHH1/Run2026C-PromptReco-v1/NANOAOD +ParkingHH1_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingHH1/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF0_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF0/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF0_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF0/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF0_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF0/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF0_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF0/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF1_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF1/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF1_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF1/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF1_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF1/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF1_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF1/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF2_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF2/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF2_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF2/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF2_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF2/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF2_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF2/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF3_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF3/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF3_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF3/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF3_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF3/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF3_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF3/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF4_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF4/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF4_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF4/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF4_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF4/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF4_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF4/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF5_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF5/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF5_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF5/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF5_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF5/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF5_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF5/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF6_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF6/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF6_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF6/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF6_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF6/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF6_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF6/Run2026D-PromptReco-v1/NANOAOD +ParkingVBF7_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF7/Run2026A-PromptReco-v1/NANOAOD +ParkingVBF7_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF7/Run2026B-PromptReco-v1/NANOAOD +ParkingVBF7_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF7/Run2026C-PromptReco-v1/NANOAOD +ParkingVBF7_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /ParkingVBF7/Run2026D-PromptReco-v1/NANOAOD +Tau_Run2026A_v1: + eraLetter: A + eraVersion: v1 + nanoAOD: + v15: /Tau/Run2026A-PromptReco-v1/NANOAOD +Tau_Run2026B_v1: + eraLetter: B + eraVersion: v1 + nanoAOD: + v15: /Tau/Run2026B-PromptReco-v1/NANOAOD +Tau_Run2026C_v1: + eraLetter: C + eraVersion: v1 + nanoAOD: + v15: /Tau/Run2026C-PromptReco-v1/NANOAOD +Tau_Run2026D_v1: + eraLetter: D + eraVersion: v1 + nanoAOD: + v15: /Tau/Run2026D-PromptReco-v1/NANOAOD diff --git a/config/Run3_2026/global.yaml b/config/Run3_2026/global.yaml new file mode 100644 index 00000000..28461c15 --- /dev/null +++ b/config/Run3_2026/global.yaml @@ -0,0 +1,18 @@ +era: Run3_2026 +luminosity: 25843.26 # pb-1, brilcalc lumi -b "STABLE BEAMS" -u /pb -i Cert_Collisions2026_401624_403937_Golden.json (no normtag available) +# Same Summer24 MC as 2024. Some 2026 corrections are not yet available. +# shared_mc is inherited from reuse_mc_from_era. 2026 data is PromptReco NanoAOD. +reuse_mc_from_era: Run3_2024 +weight_base_branch: weight_base +nano_version: v15 +crossSectionsFile: FLAF/config/crossSections13p6TeV.yaml +MET_flags: # https://twiki.cern.ch/twiki/bin/view/CMS/MissingETOptionalFiltersRun2#Run_3_2024_data_and_MC_Recommend + - Flag_goodVertices + - Flag_globalSuperTightHalo2016Filter + - Flag_EcalDeadCellTriggerPrimitiveFilter + - Flag_BadPFMuonFilter + - Flag_BadPFMuonDzFilter + - Flag_hfNoisyHitsFilter + - Flag_eeBadScFilter + - Flag_ecalBadCalibFilter +lumiFile: Corrections/data/golden_json/Cert_Collisions2026_401624_403937_Golden.json diff --git a/config/dataset_exceptions.yaml b/config/dataset_exceptions.yaml index a74c3836..f7dd902f 100644 --- a/config/dataset_exceptions.yaml +++ b/config/dataset_exceptions.yaml @@ -1,24 +1,42 @@ -# no MC for 2025: +# no MC for 2025/2026 (reuse_mc_from_era): # ^(A-Z)*.: # - Run3_2025 +# - Run3_2026 # DY ^DYto2(E|Mu|Tau)_M_50_[012]J_amcatnloFXFX: - Run3_2025 -^DYto2(E|Mu|Tau)_M_(10to50|50)_amcatnloFXFX: + - Run3_2026 +^DYto2(E|Mu)_M_(10to50|50)_amcatnloFXFX: - Run3_2025 + - Run3_2026 +^DYto2Tau_M_10to50_amcatnloFXFX: + - Run3_2025 + - Run3_2026 +DYto2Tau_M_50_amcatnloFXFX: + - Run3_2022 + - Run3_2022EE + - Run3_2023 + - Run3_2023BPix + - Run3_2025 + - Run3_2026 ^DYto2(E|Mu|Tau)_MLL_*.: - Run3_2025 + - Run3_2026 ^DYto2L_M_50_PTLL_*.: - Run3_2025 + - Run3_2026 ^DYto2L_M_(10to50|50)_amcatnloFXFX: - Run3_2024 - Run3_2025 + - Run3_2026 ^DYto2L_M_50_[012]J_amcatnloFXFX: - Run3_2024 - Run3_2025 + - Run3_2026 ^DYto2(E|Mu)_M_50_PTLL_.*_amcatnloFXFX: - Run3_2024 - Run3_2025 + - Run3_2026 ^DYto2.*_M_50_amcatnloFXFX_ext1: - Run3_2023 - Run3_2023BPix @@ -26,10 +44,12 @@ ^DYto2Tau_M_50_(0|1|2)J_Filtered_amcatnloFXFX: - Run3_2024 - Run3_2025 + - Run3_2026 ^DYto2L_MLL_10to50_madgraphMLM: - Run3_2024 - Run3_2025 + - Run3_2026 ^DYto2(E|Mu|Tau)_MLL_10to50_madgraphMLM: - Run3_2022 - Run3_2022EE @@ -39,6 +59,7 @@ # H ^GluGluH*.: - Run3_2025 + - Run3_2026 GluGluHto2B_M125_ext1: - Run3_2022 - Run3_2022EE @@ -49,17 +70,21 @@ GluGluHto2Tau_M125: - Run3_2023 ^ggZH_.*: - Run3_2025 + - Run3_2026 ^ggZH_.*_ext1: - Run3_2023 - Run3_2023BPix - Run3_2024 -^GluGlutoContinto.*: +^GluGlutoContinto2Zto2(E2Mu|E2Nu|E2Tau|Mu2Nu|Mu2Tau): - Run3_2022 - Run3_2024 +^GluGlutoContinto2Zto4(E|Mu|Tau): + - Run3_2022 GluGlutoContinto2Zto2Mu2Tau: - Run3_2023 ^VBFH*.: - Run3_2025 + - Run3_2026 VBFHto2B_M125_ext1: - Run3_2022 - Run3_2022EE @@ -72,12 +97,12 @@ VBFHto2B_M125_ext1: - Run3_2024 ^ZH.*: - Run3_2025 + - Run3_2026 ^ZH_Hto2B.*_ext1: - Run3_2023 - Run3_2023BPix - Run3_2024 -^W(plus|minus)H_Hto2B_Wto2Q$: - - Run3_2024 + # QCD QCD_HT40to70_MLM: @@ -91,8 +116,10 @@ QCD_PT_300_EMEnriched: ^QCD_PT_.*: - Run3_2024 - Run3_2025 + - Run3_2026 ^QCD_HT.*: - Run3_2025 + - Run3_2026 # ST ^T(bar|)W.*_ext1: - Run3_2023 @@ -103,12 +130,14 @@ TT: - Run3_2023 - Run3_2024 - Run3_2025 + - Run3_2026 TT_ext1: - Run3_2022EE - Run3_2023 - Run3_2023BPix - Run3_2024 - Run3_2025 + - Run3_2026 TTto2L2Nu_ext1: - Run3_2023 - Run3_2023BPix @@ -123,31 +152,39 @@ TTtoLNu2Q_ext1: - Run3_2024 ^TTto(2L2Nu|LNu2Q|4Q): - Run3_2025 + - Run3_2026 TTZ_Zto2Q: - Run3_2023 - Run3_2023BPix - Run3_2025 + - Run3_2026 TTWZ: - Run3_2022 - Run3_2025 + - Run3_2026 ^TTZ(Z|H)_Z(Z|H)to4B: - Run3_2022 - Run3_2022EE - Run3_2023 - Run3_2023BPix - Run3_2025 + - Run3_2026 TTZZ: - Run3_2024 - Run3_2025 + - Run3_2026 TTZH: - Run3_2024 - Run3_2025 + - Run3_2026 ^TT(WH|WW): - Run3_2025 + - Run3_2026 ^TTHto*: - Run3_2025 + - Run3_2026 # VBF VBFHto2Tau_M125: @@ -156,6 +193,7 @@ VBFHto2Tau_M125: # W ^W(minus|plus|to)*.: - Run3_2025 + - Run3_2026 WtoLNu_madgraphMLM_ext1: - Run3_2023 - Run3_2023BPix @@ -164,13 +202,15 @@ WtoLNu_madgraphMLM: - Run3_2024 WtoLNu_amcatnloFXFX: - Run3_2024 -^WtoLNu_(0|1)J_amcatnloFXFX: +^WtoLNu_(0|1|2)J_amcatnloFXFX: - Run3_2024 # VV ZZ: - Run3_2025 + - Run3_2026 ^ZZto*.: - Run3_2025 + - Run3_2026 ^WW.*_ext1: - Run3_2023 - Run3_2023BPix @@ -186,6 +226,7 @@ ZZ: # VVV ZZZ: - Run3_2025 + - Run3_2026 # change name in W samples.. WtoENu_amcatnloFXFX: - Run3_2022 @@ -214,19 +255,22 @@ WtoTauNu_amcatnloFXFX: - Run3_2023 - Run3_2023BPix - Run3_2025 + - Run3_2026 ^T(barB|Bbar)toL(minus|plus)Nu(B|Bbar)_s_channel_4FS: - Run3_2025 + - Run3_2026 ^T(barBQ|BbarQ)_t_channel_4FS: - Run3_2025 + - Run3_2026 ^T(barWplus|Wminus)to(2L2Nu|4Q|LNu2Q): - Run3_2025 + - Run3_2026 -ZHto2Tau_UncorrelatedDecay_UnFiltered: - - Run3_2024 ^Zto2Q_HT_(400to600|600to800|800toInf|200to400): - Run3_2024 - Run3_2025 + - Run3_2026 ^Zto2Q_HT_(100to400|400to800|800to1500|1500to2500|2500toInf): - Run3_2022 @@ -234,6 +278,8 @@ ZHto2Tau_UncorrelatedDecay_UnFiltered: - Run3_2023 - Run3_2023BPix - Run3_2025 + - Run3_2026 ^Zto2Nu_HT*.: - Run3_2025 + - Run3_2026 diff --git a/docs/analyses.md b/docs/analyses.md index 119be3db..73d0a494 100644 --- a/docs/analyses.md +++ b/docs/analyses.md @@ -37,5 +37,5 @@ the [walkthrough](workflow/walkthrough.md#stage-2-compute-analysis-observables-h ## H→μμ A single-Higgs analysis with the leanest submodule set (no `StatInference`/`inference`). Its CI -runs over **all** Run 3 eras (`H_mumu_eras: ALL`), and its CI process names are lower-case +runs over every Run 3 era (explicit `*_eras` list), and its CI process names are lower-case (`custom_CI_signal`, …) — see [Processes & models](configuration/processes-and-models.md). diff --git a/docs/ci/integration-pipeline.md b/docs/ci/integration-pipeline.md index 5f69367b..4c353580 100644 --- a/docs/ci/integration-pipeline.md +++ b/docs/ci/integration-pipeline.md @@ -57,7 +57,7 @@ variables: HH_bbtautau_active: "1" # "1" = run this analysis, "0" = skip HH_bbtautau_task: "FLAF.Analysis.tasks.HistPlotTask" HH_bbtautau_args: "--branches 0 --test 1000" - HH_bbtautau_eras: "Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix" + HH_bbtautau_eras: "Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 Run3_2026" HH_bbtautau_processes: "custom_CI_Signal custom_CI_Background custom_CI_Data" ci_backend: "gitlab" # "gitlab" (default) or "github" TEST_TIMEOUT: "4h" diff --git a/docs/concepts/configuration.md b/docs/concepts/configuration.md index 14b7beb9..512647e9 100644 --- a/docs/concepts/configuration.md +++ b/docs/concepts/configuration.md @@ -55,7 +55,7 @@ the current run, so all tasks see a consistent configuration. | File | Lives in | Holds | |---|---|---| | `user_custom.yaml` | analysis `config/` | **Your** personal, uncommitted settings: storage, model, options. [Guide](../configuration/user-custom.md). | -| `global.yaml` | both layers | Global settings: anaTuple/histTuple definitions, corrections, payload producers, signal types. | +| `global.yaml` | both layers | Global settings: anaTuple/histTuple definitions, corrections, payload producers, signal types. For 2024+ this also holds `weight_base_branch` (`weight_base` for a single year, `weight_base_cmb` for the 24+25+26 combination). `shared_mc` is declared on `Run3_2024` and inherited when `reuse_mc_from_era` is set. | | `datasets.yaml` | per-era, both layers | Dataset (sample) definitions. [Guide](../configuration/datasets.md). | | `processes.yaml` | analysis `config/` | Logical processes built from datasets. [Guide](../configuration/processes-and-models.md). | | `phys_models.yaml` | analysis `config/` | Which processes are background/signal/data for a model. [Guide](../configuration/processes-and-models.md). | diff --git a/docs/concepts/data-flow.md b/docs/concepts/data-flow.md index 2821d900..fef23ad3 100644 --- a/docs/concepts/data-flow.md +++ b/docs/concepts/data-flow.md @@ -32,7 +32,7 @@ flowchart TD |---|---|---| | **InputFileTask** | A Rucio query for the requested datasets and era. | The concrete list of NanoAOD files to process. Runs first and cheaply; everything else keys off it. | | **AnaTupleFileTask** | One NanoAOD file (one branch per file). | One **anaTuple**: a slimmed/skimmed analysis ntuple with the objects, weights and flags the analysis needs. Runs inside CMSSW via `AnaProd/anaTupleProducer.py`. | -| **AnaTupleMergeTask** | The per-file anaTuples for a dataset. | One merged anaTuple per dataset (data merged across runs). | +| **AnaTupleMergeTask** | The per-file anaTuples for a dataset. | One merged anaTuple per dataset (data merged across runs). For 2024+ shared-MC eras this is also where `weight_base` and `weight_base_cmb` are written. | | **HistTupleProducerTask** | Merged anaTuples. | **histTuples**: ntuples with the heavier analysis **observables** computed (the "payload producers"). | | **HistFromNtupleProducerTask** | histTuples. | **Histograms** of the requested variables, including systematic variations. Branches over variables. | | **HistMergerTask** | Per-piece histograms. | Merged histograms per process, ready for plotting and fitting. | diff --git a/docs/concepts/eras.md b/docs/concepts/eras.md index ea5b4bea..801e3496 100644 --- a/docs/concepts/eras.md +++ b/docs/concepts/eras.md @@ -12,7 +12,8 @@ data-taking period; choosing one selects the matching datasets, corrections and | `Run3_2023` | 2023, pre-BPix | 13.6 TeV | v13 | | `Run3_2023BPix` | 2023, post-BPix install | 13.6 TeV | v13 | | `Run3_2024` | 2024 | 13.6 TeV | v15 | -| `Run3_2025` | 2025 (future) | 13.6 TeV | — | +| `Run3_2025` | 2025 | 13.6 TeV | v15 | +| `Run3_2026` | 2026 | 13.6 TeV | v15 | ## Run 2 eras (legacy) @@ -37,15 +38,47 @@ Each sub-era has its own corrections and luminosity, which is exactly why the the correct NanoAOD version path on DAS. - **NanoAOD version** — the table above; the dataset entries point at the right `vNN` campaign. - **Corrections** — pileup, b-tagging, trigger and other scale factors are era-specific. -- **Signals** — resonant/non-resonant signals exist for some eras and not others (for instance, - several signal families are not produced for `Run3_2024`). +- **Signals** — resonant/non-resonant signals exist for some eras and not others. For + `Run3_2024`, VBF and non-resonant ggF HH are on DAS (new `Par-` naming); resonant + Radion/BulkGraviton and X→YH→2B2W are not. +- **2024/2025/2026 shared MC** — there is no dedicated 2025 or 2026 MC campaign. + All three years use the Summer24 NanoAOD, but **jet, PU and tau corrections + differ**, so AnaTuple production runs once per era. `Run3_2025` and `Run3_2026` + set `reuse_mc_from_era: Run3_2024` so the 2024 MC dataset list (and the + `shared_mc` split) is reused. Each production stores `weight_base` (all events, + this year's luminosity; use for a single-year run) and `weight_base_cmb` (the + same events split by residue between 2024, 2025 and 2026; use for a combined + run). The split applies only to MC; data keeps the full year in + `weight_base_cmb`. Select which branch histograms use with + `weight_base_branch`. + `shared_mc` lives only on the source era (`Run3_2024`): a 17:17:4 residue + *target* over modulus 38 (`Run3_2024: [ 0, 16 ]`, `Run3_2025: [ 17, 33 ]`, + `Run3_2026: [ 34, 37 ]`), matching the recorded luminosities + 109948.18 : 110730.86 : 25843.26. The actual event split need not match + that target. AnaTupleFileTask therefore stores two denominators: the + full-sample sum (for `weight_base`) and the in-era sum (for + `weight_base_cmb`). Each weight is + `gen × lumi × xs × PU / its_denominator`, so both yields stay `L·σ`. + HistTuple multiplies the AnaTuple column named by `weight_base_branch` + (`weight_base` for a single-year run, `weight_base_cmb` for the combined + 24+25+26 run). + Until official UParTAK4 shape files exist, 2024+ era overlays omit + `btag.normCacheProducer`, so HistTuple does not depend on the global + `BtagShape` cache. `modes.: none` still loads the correction (needed + for WP-id branches) but does not apply scale factors. Missing + `uncs_to_exclude` era keys default to an empty list. + The 2026 EGM folder has scale/smear files but no `electron.json`, so + Electron-ID-SF is loaded from the 2025 file and evaluated with year + `2025Prompt` (not `2026Prompt`). ## Running several eras A task runs **one era at a time**. To cover multiple eras, launch the task once per era (often scripted), or, in CI, list them in the `*_eras` variable (e.g. -`Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix`, or `ALL`). See the -[integration pipeline](../ci/integration-pipeline.md). +`Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 Run3_2026`). See the +[integration pipeline](../ci/integration-pipeline.md). For a 2024+2025+2026 +combination, run each era with `weight_base_branch: weight_base_cmb` and add +the histograms. !!! warning "`--period` must match an existing era directory" If you pass an era that has no `config//` (or whose datasets are not defined), config diff --git a/docs/configuration/datasets.md b/docs/configuration/datasets.md index 91de612e..819afcad 100644 --- a/docs/configuration/datasets.md +++ b/docs/configuration/datasets.md @@ -14,6 +14,14 @@ framework and the analysis are concatenated, so all datasets for an era are avai This keeps the common SM samples in one shared place while each analysis owns its signals. +`Run3_2025` and `Run3_2026` have no dedicated MC campaign. Set +`reuse_mc_from_era: Run3_2024` in those eras' `FLAF/config//global.yaml` and +Setup copies every 2024 MC (and analysis-signal) dataset that is not already +defined and is not data (`eraLetter`), and inherits `shared_mc` from that era. +Those `datasets.yaml` files therefore only need that year's data plus any +era-local custom/CI samples. 2026 PromptReco NanoAOD (eras A–D) is listed in +`FLAF/config/Run3_2026/datasets.yaml`. + ## Dataset entry format ```yaml @@ -66,7 +74,7 @@ resolvable cross-section, that names are well-formed, etc.: ```sh python3 test/checkDatasetConfigConsistency.py \ --exception config/dataset_exceptions.yaml \ - Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 + Run3_2022 Run3_2022EE Run3_2023 Run3_2023BPix Run3_2024 Run3_2025 Run3_2026 ``` Run it after editing any `datasets.yaml`. Known, intentional exceptions live in @@ -75,10 +83,21 @@ Run it after editing any `datasets.yaml`. Known, intentional exceptions live in ## Adding a new era 1. Create `FLAF/config//` with at least `datasets.yaml` and `global.yaml`. -2. Create `/config//` with the analysis-specific overrides and signals. -3. Add the era to `test-setup-loading.yaml` in each affected analysis (so CI loads `Setup.py` for +2. Add the era to the C++ `Period` enum in `FLAF/include/AnalysisTools.h` and to the + `PeriodToHHbTagInput` maps in `FLAF/include/HHbTagScores.h`. AnaTuple production + JIT-compiles `Period::` (`anaTupleProducer.py`); a missing enumerator fails + immediately with `no member named '' in 'Period'`. +3. Create `/config//` with the analysis-specific overrides and signals. + In `triggers.yaml`, every `jsonTRGcorrection_key` map must include the + Corrections period name (`2026_Summer24` for `Run3_2026`). Missing keys + fail MC jobs with `KeyError` in `TrigCorrProducer`. Until a 2026 + Electron-ID-SF JSON is published, `EleCorrProducer` evaluates that + correction with year `2025Prompt` (the only year key in the 2025 file + that `2026_Summer24` loads). A raw `2026Prompt` key fails HistTuple + with `Index not available in Category`. +4. Add the era to `test-setup-loading.yaml` in each affected analysis (so CI loads `Setup.py` for it and catches config errors early). -4. Add the era to the `*_eras` variable in the relevant `.github/integration_cfg.yaml` if it +5. Add the era to the `*_eras` variable in the relevant `.github/integration_cfg.yaml` if it should be part of CI runs. See [Integration pipeline](../ci/integration-pipeline.md). See also [Eras & periods](../concepts/eras.md). diff --git a/docs/configuration/processes-and-models.md b/docs/configuration/processes-and-models.md index 1f9a0cb7..e75df9b7 100644 --- a/docs/configuration/processes-and-models.md +++ b/docs/configuration/processes-and-models.md @@ -50,7 +50,9 @@ by `phys_model` in [`user_custom.yaml`](user-custom.md) (or `--model`). ### `TestModel` vs the production model - **`TestModel`** — a deliberately small set of processes, so the whole pipeline runs fast - end-to-end. Use it for development, local testing and CI. + end-to-end. Use it for development, local testing and CI. Every Run 3 era + (`Run3_2022` … `Run3_2026`) defines one background, one signal and one data + process under the CI names below. - **`BaseModel`** (or the analysis's named production model) — the full set used for real results. !!! tip "Process names differ slightly between analyses" diff --git a/docs/configuration/user-custom.md b/docs/configuration/user-custom.md index 96aeb557..8ec854ab 100644 --- a/docs/configuration/user-custom.md +++ b/docs/configuration/user-custom.md @@ -37,6 +37,7 @@ Replace ``/`` with yours (e.g. `k` / `kandroso`). With just this, | `store_noncentral` | bool | Whether to keep the non-central (systematic-shift) outputs, not just the central one. | | `remove_merged_inputs` | bool | If `true`, `HistMergerTask` deletes each variable's per-chunk split histograms (`HistFromNtupleProducerTask` outputs) after merging, to save space, leaving a tiny per-chunk `.merged` marker in place of each. Safe: the producer stays "complete" for exactly the chunks that were merged (it finds the split *or* its marker), so the task graph stays consistent (no re-run); a chunk that was never produced has no marker and is still produced. Default `false` — intermediates are kept. | | `variables` | list | Restrict which variables are produced/plotted. Omit for the full set. | +| `hist_from_ntuple_max_hists` | int | Max histograms `HistFromNtupleProducerTask` books in one RDataFrame pass. The count is variables × selections × (Central + every Up/Down). Default `4000`; `0` disables batching. Lower this (do not raise CI memory) if a job OOMs. | !!! tip "`TestModel` is the fast path" `TestModel` selects a reduced set of processes so the pipeline runs quickly end-to-end. Use it diff --git a/docs/reference/tasks.md b/docs/reference/tasks.md index 44ddd783..fa730f83 100644 --- a/docs/reference/tasks.md +++ b/docs/reference/tasks.md @@ -40,9 +40,15 @@ producers"), writing **histTuples**. ### `HistFromNtupleProducerTask` Fills **histograms** of the requested variables from the histTuples, including systematic variations. **Branches over (dataset, file-chunk):** each job reads its chunk of input files -once and fills **all** active variables in a single event-loop pass. Large datasets are -parallelized by splitting their files into chunks (work is never split per variable, which -would re-read the same events once per variable). +and fills the active variables. Large datasets are parallelized by splitting their files +into chunks. + +If the number of histograms booked in one RDataFrame pass — variables × selections × +(Central + every Up/Down) — exceeds `hist_from_ntuple_max_hists` (default `4000`), the +producer repeats the event loop in batches instead of holding every histogram at once. +That keeps CI (8 GiB) from running out of memory when uncertainties are on. Set the +threshold in `global.yaml` / `user_custom.yaml`, or pass `--max-hists` to the producer +(`0` disables batching). LAW branches stay file-chunks; batching is inside the job. - **Parameters:** `--variables` (string; restrict which variables), `--n-files-per-job` (int, default `20`; input files processed per branch). diff --git a/docs/workflow/walkthrough.md b/docs/workflow/walkthrough.md index 4d717873..3d13e832 100644 --- a/docs/workflow/walkthrough.md +++ b/docs/workflow/walkthrough.md @@ -76,8 +76,9 @@ law run FLAF.Analysis.tasks.HistTupleProducerTask --period $ERA --version $VER - ## Stage 3 — Fill and merge histograms `HistFromNtupleProducerTask` fills **histograms** of the requested variables from the histTuples — -**one branch per (dataset, file-chunk)**, filling all variables in a single pass — including -systematic variations. `HistMergerTask` merges the pieces into per-process histograms ready for +**one branch per (dataset, file-chunk)**, including systematic variations. If the booked +histogram count (with Up/Down) exceeds `hist_from_ntuple_max_hists`, the producer fills them +in batches. `HistMergerTask` merges the pieces into per-process histograms ready for plotting and fitting. ```sh diff --git a/include/AnalysisTools.h b/include/AnalysisTools.h index 63a8023e..d24a821e 100644 --- a/include/AnalysisTools.h +++ b/include/AnalysisTools.h @@ -37,7 +37,8 @@ enum class Period : int { Run3_2023 = 7, Run3_2023BPix = 8, Run3_2024 = 9, - Run3_2025 = 10 + Run3_2025 = 10, + Run3_2026 = 11 }; enum class SampleType : int { diff --git a/include/HHbTagScores.h b/include/HHbTagScores.h index 7386964e..99fd1461 100644 --- a/include/HHbTagScores.h +++ b/include/HHbTagScores.h @@ -22,6 +22,7 @@ inline int PeriodToHHbTagInput(int version, Period period) { {{1, Period::Run3_2023BPix}, 2018}, {{1, Period::Run3_2024}, 2018}, {{1, Period::Run3_2025}, 2018}, + {{1, Period::Run3_2026}, 2018}, // v2 {{2, Period::Run2_2016_HIPM}, 2016}, @@ -34,6 +35,7 @@ inline int PeriodToHHbTagInput(int version, Period period) { {{2, Period::Run3_2023BPix}, 2018}, {{2, Period::Run3_2024}, 2018}, {{2, Period::Run3_2025}, 2018}, + {{2, Period::Run3_2026}, 2018}, // v3 {{3, Period::Run2_2016_HIPM}, 0}, @@ -46,6 +48,7 @@ inline int PeriodToHHbTagInput(int version, Period period) { {{3, Period::Run3_2023BPix}, 3}, {{3, Period::Run3_2024}, 3}, {{3, Period::Run3_2025}, 3}, + {{3, Period::Run3_2026}, 3}, }; auto iter = periodHHBtag.find(std::make_pair(version, period)); if (iter == periodHHBtag.end()) { @@ -91,7 +94,7 @@ struct HHBtagWrapper { int version{-1}; }; - static void Initialize(const std::string &path, int version) { + static void Initialize(const std::string& path, int version) { std::array models; for (size_t n = 0; n < 2; ++n) { std::ostringstream ss_model; @@ -101,32 +104,32 @@ struct HHBtagWrapper { _Get().tagger = std::make_unique(models); _Get().version = version; } - static const Handle &Get() { - auto &hh_btag = HHBtagWrapper::_Get(); + static const Handle& Get() { + auto& hh_btag = HHBtagWrapper::_Get(); if (!hh_btag.tagger) throw std::runtime_error("HHBtag is not initialized."); return hh_btag; } private: - static Handle &_Get() { + static Handle& _Get() { static Handle hh_btag; return hh_btag; } }; -RVecF GetHHBtagScore(const RVecB &Jet_sel, - const RVecI &Jet_idx, - const RVecLV &jet_p4, - const RVecF &Jet_deepFlavour, - const float &met_pt, - const float &met_phi, - const HTTCand<2> &HTT_Cand, - const int &period, +RVecF GetHHBtagScore(const RVecB& Jet_sel, + const RVecI& Jet_idx, + const RVecLV& jet_p4, + const RVecF& Jet_deepFlavour, + const float& met_pt, + const float& met_phi, + const HTTCand<2>& HTT_Cand, + const int& period, int event) { const ULong64_t parity = event % 2; RVecI JetIdxOrdered = ReorderObjects(Jet_deepFlavour, Jet_idx); - const auto &handle = HHBtagWrapper::Get(); + const auto& handle = HHBtagWrapper::Get(); int channelId = ChannelToHHbTagInput(handle.version, HTT_Cand.channel()); RVecF all_scores(JetIdxOrdered.size(), -1.); std::vector jet_pt; diff --git a/test/test_hist_from_ntuple_batch.py b/test/test_hist_from_ntuple_batch.py new file mode 100644 index 00000000..8d082e8c --- /dev/null +++ b/test/test_hist_from_ntuple_batch.py @@ -0,0 +1,106 @@ +import os +import sys +import unittest + +flaf_repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +flaf_parent = os.path.dirname(flaf_repo) +if flaf_parent not in sys.path: + sys.path.insert(0, flaf_parent) + +from FLAF.Analysis.histFromNtupleBatch import ( + count_booked_hists, + iter_hist_batches, + n_cut_slots, + unc_scale_pairs, +) + + +class TestHistFromNtupleBatch(unittest.TestCase): + def test_count_includes_up_down(self): + uncs = { + "Central": ["Central"], + "JER": ["Up", "Down"], + "JES_Total": ["Up", "Down"], + } + self.assertEqual(len(unc_scale_pairs(uncs)), 5) + # 10 vars × 4 keys × 1 cut × 5 scales + self.assertEqual(count_booked_hists(10, 4, 1, 5), 200) + self.assertEqual(n_cut_slots({}), 1) + self.assertEqual(n_cut_slots({"a": 1, "b": 2}), 2) + + def test_under_threshold_is_one_batch(self): + uncs = {"Central": ["Central"], "JER": ["Up", "Down"]} + keys = {("e", "SR"): "e && SR", ("mu", "SR"): "mu && SR"} + vars_ = ["lep1_pt", "lep1_eta"] + batches = list(iter_hist_batches(uncs, keys, {}, vars_, max_hists=100)) + self.assertEqual(len(batches), 1) + b_uncs, b_keys, b_cuts, b_vars = batches[0] + self.assertEqual(b_uncs, uncs) + self.assertEqual(b_keys, keys) + self.assertEqual(b_vars, vars_) + + def test_disabled_threshold_is_one_batch(self): + uncs = {"Central": ["Central"]} + keys = {("e",): "e"} + vars_ = [f"v{i}" for i in range(50)] + batches = list(iter_hist_batches(uncs, keys, {}, vars_, max_hists=0)) + self.assertEqual(len(batches), 1) + self.assertEqual(batches[0][3], vars_) + + def test_split_by_variables(self): + uncs = {"Central": ["Central"], "JER": ["Up", "Down"]} # 3 scales + keys = {("e",): "e", ("mu",): "mu"} # 2 keys + # 3 × 2 × 1 = 6 hists/var; max 10 → 1 var per batch + vars_ = ["a", "b", "c"] + batches = list(iter_hist_batches(uncs, keys, {}, vars_, max_hists=10)) + self.assertEqual(len(batches), 3) + seen = [] + for b_uncs, b_keys, _, b_vars in batches: + self.assertEqual(b_uncs, uncs) + self.assertEqual(set(b_keys), set(keys)) + self.assertEqual(len(b_vars), 1) + seen.extend(b_vars) + self.assertEqual(seen, vars_) + + def test_one_var_over_budget_splits_keys(self): + uncs = {"Central": ["Central"]} + keys = {("k%d" % i,): "c" for i in range(10)} + vars_ = ["only"] + # 1 × 10 × 1 × 1 = 10; max 3 → 4 key-batches (3+3+3+1) + batches = list(iter_hist_batches(uncs, keys, {}, vars_, max_hists=3)) + self.assertGreater(len(batches), 1) + seen_keys = [] + for _, b_keys, _, b_vars in batches: + self.assertEqual(b_vars, ["only"]) + self.assertLessEqual(len(b_keys), 3) + seen_keys.extend(b_keys) + self.assertEqual(sorted(seen_keys), sorted(keys)) + + def test_each_batch_respects_budget(self): + uncs = { + "Central": ["Central"], + "JER": ["Up", "Down"], + "EleES": ["Up", "Down"], + } # 5 + keys = {("c%d" % i,): "x" for i in range(8)} # 8 + cuts = {"cutA": "a", "cutB": "b"} # 2 + vars_ = ["v1", "v2", "v3"] # 3 + # total = 3*8*2*5 = 240 + max_hists = 20 + batches = list(iter_hist_batches(uncs, keys, cuts, vars_, max_hists)) + self.assertGreater(len(batches), 1) + covered = 0 + for b_uncs, b_keys, b_cuts, b_vars in batches: + n = count_booked_hists( + len(b_vars), + max(1, len(b_keys)), + n_cut_slots(b_cuts), + max(1, len(unc_scale_pairs(b_uncs))), + ) + self.assertLessEqual(n, max_hists) + covered += n + self.assertEqual(covered, 240) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_histtuple_weight_base_branch.py b/test/test_histtuple_weight_base_branch.py new file mode 100644 index 00000000..b3c5ad06 --- /dev/null +++ b/test/test_histtuple_weight_base_branch.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""HistTuple-stage check: weight_base_branch selects one-era vs shared-MC. + +HistTuple multiplies the AnaTuple column named by ``weight_base_branch``. +``weight_base`` uses the full-sample denominator; ``weight_base_cmb`` uses +the in-era denominator. The residue share is only a target — the two +denominators keep both yields equal to L·σ even when the split is uneven. +""" + +import os +import sys +import unittest + +flaf_repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +flaf_parent = os.path.dirname(flaf_repo) +if flaf_parent not in sys.path: + sys.path.insert(0, flaf_parent) + +from FLAF.Common.shared_mc import shared_mc_in_era, shared_mc_split + +SHARED_MC = { + "split_modulus": 38, + "eras": { + "Run3_2024": [0, 16], + "Run3_2025": [17, 33], + "Run3_2026": [34, 37], + }, +} + + +def _two_denom_weights(events, genw, era): + split_mod, lo, hi, _ = shared_mc_split(era, SHARED_MC) + in_era = [shared_mc_in_era(e, split_mod, lo, hi) for e in events] + denom_all = sum(genw) + denom_cmb = sum(w for w, ok in zip(genw, in_era) if ok) + w_base = [w / denom_all for w in genw] + w_cmb = [ + (w / denom_cmb if ok and denom_cmb else 0.0) for w, ok in zip(genw, in_era) + ] + return w_base, w_cmb + + +class TestHistTupleWeightBaseBranch(unittest.TestCase): + def test_one_era_uses_full_denominator(self): + events = list(range(50)) + genw = [float(i + 1) for i in events] + for era in SHARED_MC["eras"]: + w_base, _ = _two_denom_weights(events, genw, era) + self.assertEqual(len(w_base), len(events)) + self.assertTrue(all(w > 0 for w in w_base)) + self.assertAlmostEqual(sum(w_base), 1.0) + + def test_shared_uses_in_era_denominator(self): + events = list(range(50)) + genw = [float(i + 1) for i in events] + for event_i, event in enumerate(events): + assigned = 0 + for era in SHARED_MC["eras"]: + _, w_cmb = _two_denom_weights(events, genw, era) + if w_cmb[event_i] != 0: + assigned += 1 + self.assertEqual(assigned, 1) + for era in SHARED_MC["eras"]: + _, w_cmb = _two_denom_weights(events, genw, era) + self.assertAlmostEqual(sum(w_cmb), 1.0) + + def test_flag_selects_different_columns(self): + events = list(range(50)) + genw = [float(i + 1) for i in events] + w_base, w_cmb = _two_denom_weights(events, genw, "Run3_2025") + self.assertNotEqual(w_base, w_cmb) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_shared_mc.py b/test/test_shared_mc.py new file mode 100644 index 00000000..2bf97b59 --- /dev/null +++ b/test/test_shared_mc.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Unit tests for the shared-MC residue split (2024/2025/2026).""" + +import os +import sys +import unittest + +# Parent of the FLAF repo so `import FLAF.Common...` resolves. +flaf_repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +flaf_parent = os.path.dirname(flaf_repo) +if flaf_parent not in sys.path: + sys.path.insert(0, flaf_parent) + +from FLAF.Common.shared_mc import shared_mc_in_era, shared_mc_split + +SHARED_MC = { + "split_modulus": 38, + "eras": { + "Run3_2024": [0, 16], + "Run3_2025": [17, 33], + "Run3_2026": [34, 37], + }, +} + + +class TestSharedMcSplit(unittest.TestCase): + def test_fractions_match_residue_share(self): + _, _, _, frac_24 = shared_mc_split("Run3_2024", SHARED_MC) + _, _, _, frac_25 = shared_mc_split("Run3_2025", SHARED_MC) + _, _, _, frac_26 = shared_mc_split("Run3_2026", SHARED_MC) + self.assertAlmostEqual(frac_24, 17 / 38) + self.assertAlmostEqual(frac_25, 17 / 38) + self.assertAlmostEqual(frac_26, 4 / 38) + self.assertAlmostEqual(frac_24 + frac_25 + frac_26, 1.0) + + def test_unknown_era(self): + with self.assertRaises(RuntimeError): + shared_mc_split("Run3_2023", SHARED_MC) + + def test_assignment_is_partition(self): + split_mod, lo24, hi24, _ = shared_mc_split("Run3_2024", SHARED_MC) + _, lo25, hi25, _ = shared_mc_split("Run3_2025", SHARED_MC) + _, lo26, hi26, _ = shared_mc_split("Run3_2026", SHARED_MC) + n24 = n25 = n26 = 0 + for event in range(split_mod): + in24 = shared_mc_in_era(event, split_mod, lo24, hi24) + in25 = shared_mc_in_era(event, split_mod, lo25, hi25) + in26 = shared_mc_in_era(event, split_mod, lo26, hi26) + self.assertEqual(int(in24) + int(in25) + int(in26), 1) + n24 += int(in24) + n25 += int(in25) + n26 += int(in26) + self.assertEqual(n24, 17) + self.assertEqual(n25, 17) + self.assertEqual(n26, 4) + + def test_two_denominators_match_when_split_is_uneven(self): + # 50 events: residue counts are 29:17:4, not 17:17:4. Weights are not flat. + events = list(range(50)) + genw = [float(i + 1) for i in events] + denom_all = sum(genw) + lumi_xs = 100.0 + sum_base = 0.0 + sums_cmb = {} + for era in SHARED_MC["eras"]: + split_mod, lo, hi, frac = shared_mc_split(era, SHARED_MC) + in_era = [shared_mc_in_era(e, split_mod, lo, hi) for e in events] + denom_cmb = sum(w for w, ok in zip(genw, in_era) if ok) + actual_frac = denom_cmb / denom_all + self.assertNotAlmostEqual(actual_frac, frac) + self.assertGreater(denom_cmb, 0) + sum_cmb = 0.0 + for w, ok in zip(genw, in_era): + sum_base += lumi_xs * w / denom_all + sum_cmb += lumi_xs * w / denom_cmb if ok else 0.0 + sums_cmb[era] = sum_cmb + self.assertAlmostEqual(sum_cmb, lumi_xs) + # Scaling the single-year weight by the residue fraction is biased. + biased = sum( + (lumi_xs * w / denom_all) / frac if ok else 0.0 + for w, ok in zip(genw, in_era) + ) + self.assertNotAlmostEqual(biased, lumi_xs) + # sum_base was accumulated once per era + self.assertAlmostEqual(sum_base / 3, lumi_xs) + + +if __name__ == "__main__": + unittest.main()