diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e68bcc..dd4746f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -find_package(ROOT REQUIRED COMPONENTS Core MathCore Physics RIO Tree TreePlayer) +find_package(ROOT REQUIRED COMPONENTS Core Hist MathCore Physics RIO Tree TreePlayer) find_package(yaml-cpp REQUIRED) find_package(Python3 REQUIRED COMPONENTS Interpreter) enable_testing() @@ -25,15 +25,20 @@ add_library(nanoaodtools_cpp src/helpers/FatjetGenMatching.cpp src/helpers/JetMETCorrector.cpp src/helpers/JmeVariation.cpp + src/helpers/MuonCorrection.cpp + src/helpers/NloEWWeightProducer.cpp src/helpers/PuWeightProducer.cpp src/helpers/TopPtWeightProducer.cpp src/producers/HeavyFlavBaseProducer.cpp src/producers/HeavyFlavMinimalProducer.cpp src/producers/HeavyFlavMuonSampleProducer.cpp + src/producers/HeavyFlavQCDSampleProducer.cpp + src/producers/HeavyFlavZbbSampleProducer.cpp + src/producers/HeavyFlavZmmSampleProducer.cpp ) target_include_directories(nanoaodtools_cpp PUBLIC include) -target_link_libraries(nanoaodtools_cpp PUBLIC CMSJMECalculators ROOT::Core ROOT::MathCore ROOT::Physics ROOT::RIO ROOT::Tree ROOT::TreePlayer yaml-cpp) +target_link_libraries(nanoaodtools_cpp PUBLIC CMSJMECalculators ROOT::Core ROOT::Hist ROOT::MathCore ROOT::Physics ROOT::RIO ROOT::Tree ROOT::TreePlayer yaml-cpp) add_executable(nano_run app/nano_run.cpp) target_link_libraries(nano_run PRIVATE nanoaodtools_cpp) @@ -49,3 +54,25 @@ add_test( COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/muon_validation_test.py --source-dir ${CMAKE_SOURCE_DIR} --build-dir ${CMAKE_BINARY_DIR} --group muon --max-input-events 5000 ) set_tests_properties(muon_validation PROPERTIES TIMEOUT 7200 LABELS "muon;reference;systematics") + +add_executable(muon_external_equivalence tests/muon_external_equivalence.cpp) +target_link_libraries(muon_external_equivalence PRIVATE CMSJMECalculators) +add_test( + NAME muon_external_equivalence + COMMAND muon_external_equivalence + /cvmfs/cms-griddata.cern.ch/cat/metadata/MUO/Run3-24CDEReprocessingFGHIPrompt-Summer24-NanoAODv15/latest/muon_scalesmearing.json.gz + ${CMAKE_SOURCE_DIR}/data/muon-smearing/stdflat.json.gz +) +set_tests_properties(muon_external_equivalence PROPERTIES LABELS "muon;unit;systematics") + +add_test( + NAME vjets_ewk_json + COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/vjets_ewk_json_test.py --source-dir ${CMAKE_SOURCE_DIR} +) +set_tests_properties(vjets_ewk_json PROPERTIES LABELS "vjets;correctionlib;reference") + +add_test( + NAME condor_sample_name + COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/condor_sample_name_test.py --source-dir ${CMAKE_SOURCE_DIR} +) +set_tests_properties(condor_sample_name PROPERTIES LABELS "condor;runtime") diff --git a/README.md b/README.md index da00e9f..62649e3 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ The implemented channels are: - `muon`: a heavy-flavour muon control region targeting semileptonic ttbar-like phase space, enriched in boosted top/W jets. - `minimal`: a lightweight boosted-AK8 stream that runs the shared lepton cleaning, JME, and fatjet preparation, then keeps the leading cleaned AK8 jet above the configured `channels.minimal.leading_fatjet_pt_min` threshold. +- `qcd`: a dijet control region that retains the two leading cleaned AK8 jets and requires at least one qualified fatjet satisfying the soft-drop mass selection, with secondary-vertex matching enabled by default. The region is enriched in heavy-flavor QCD jets and provides gluon-enriched proxy samples for X->bb/cc calibration studies, including sfBDT-based methods. +- `zbb`: a 2024 NanoAOD v15 boosted dijet control region requiring two corrected AK8 jets with leading/subleading pT >= 400/200 GeV and |DeltaPhi| >= pi/2. It stores both probe jets and can require at least two secondary vertices. +- `zmm`: a 2024 NanoAOD v15 boosted `Z -> mumu` recoil control region requiring exactly two isolated opposite-sign muons with pT >= 60/30 GeV, dimuon pT >= 400 GeV, and 70 <= m(mumu) <= 110 GeV. It stores the leading corrected AK8 jet separated from both muons and provides muon scale/smearing variations and efficiency scale factors. Main files: @@ -105,6 +108,43 @@ build/nano_run \ --num-events 5000 ``` +The 2024 zbb and zmm channels use their matching runtime cards: + +```bash +build/nano_run \ + --input-files input.root \ + --output-file zmm_2024.root \ + --config configs/run/zmm_2024_v15.yaml \ + --channel zmm \ + --num-events 5000 + +build/nano_run \ + --input-files input.root \ + --output-file zbb_2024.root \ + --config configs/run/zbb_2024_v15.yaml \ + --channel zbb \ + --num-events 5000 +``` + +Their muon payloads are resolved by campaign from +`/cvmfs/cms-griddata.cern.ch/cat/metadata/MUO`. The 2024 card follows the +campaign's `latest` directory by default; override +`muon_corrections.campaigns.2024_NanoAODv15.version` to pin a dated release. +Scale and resolution corrections are applied with the vendored +`MuonVariationsCalculator`. Its deterministic smearing input hashes +`(pt, eta, phi, event seed)`, whereas the official payload's +`RandomSmearing` correction hashes `(event, luminosityBlock, phi)`. Both are +uniform deterministic random sources, but their event-by-event MC smearing +values are intentionally not identical. + +The W/Z NLO electroweak weights use the correctionlib payloads under +`data/vjets-ewk/2023-08-11`. The original ROOT files remain in that directory +as validation references; production reads only the `.json.gz` files. The zbb +and zmm cards preserve the input `LHEScaleWeight` and `LHEPdfWeight` vectors, +including their metadata, for later theory-uncertainty studies. Their NLO EW +outputs include symmetric 50% correction uncertainties. Other channels can +enable the same LHE preservation with `output.include_lhe_weights=true`. + `--input-files` accepts one file or a comma-separated list. Local paths, `root://...` paths, and `/store/...` paths are supported. If `--variations` is omitted, it defaults to `nominal`. Outputs are always written with a variation suffix, so the example above writes `muon_2018_test_nominal.root`. @@ -114,10 +154,16 @@ Useful options: ```bash --tree-name Events --set output.include_lhe_weights=true ---variations nominal,jes_up,jes_down +--variations nominal,muon_scale_up,muon_scale_down,muon_smear_up,muon_smear_down ``` -`--variations` takes a comma-separated list and writes one ROOT file per requested variation. Supported JME names currently include `nominal`, `jes_up`, `jes_down`, `jer_up`, `jer_down`, `met_up`, and `met_down`. +`--variations` takes a comma-separated list and writes one ROOT file per +requested variation. Supported names are `nominal`, `jes_up`, `jes_down`, +`jer_up`, `jer_down`, `met_up`, `met_down`, `muon_scale_up`, +`muon_scale_down`, `muon_smear_up`, and `muon_smear_down`. Muon variations +are available only for zmm MC and rerun the full dimuon selection, so their +event counts may differ. A muon variation uses nominal JME, and a JME +variation uses nominal muons. ## Run Validation @@ -138,6 +184,10 @@ build/nano_make_condor \ --num-events -1 ``` +For example, replace the sample, runtime card, and channel with +`configs/samples/zbb_2024_v15_MC.yaml`, `configs/run/zbb_2024_v15.yaml`, and +`zbb` to submit the 2024 zbb workflow. + This creates the requested Condor work directory, copies a merged config snapshot, packs the repository, and writes `submit.jdl`. Submit manually: @@ -155,9 +205,13 @@ After jobs finish, return to the repository root and merge Condor pieces with: build/nano_merge /path/to/output ``` +To merge only selected output variations, pass a comma-separated list, for example +`build/nano_merge /path/to/output --variations nominal`. If omitted, all supported +variations found under `pieces/` are merged. + Pass the base output directory, not the `pieces/` subdirectory. `nano_merge` reads input pieces from `/pieces/`. -It first writes merged ROOT files to a temporary directory, then copies all merged outputs back under `/`. +It writes merged ROOT files to a temporary directory and copies each one under `/` as soon as its merge completes. ## Adding Channels diff --git a/UPDATE.md b/UPDATE.md new file mode 100644 index 0000000..dfae972 --- /dev/null +++ b/UPDATE.md @@ -0,0 +1,215 @@ +# Staged Changes + +Recorded at: `2026-07-24 15:26:52 CEST` + +This document records only the 27 files currently staged in Git. Unstaged and untracked workspace files are excluded. + +## 1. Build and Runtime Interfaces + +- `CMakeLists.txt` + - Adds the ROOT `Hist` component and `ROOT::Hist` library. + - Compiles `MuonCorrection.cpp`, `NloEWWeightProducer.cpp`, `HeavyFlavZbbSampleProducer.cpp`, and `HeavyFlavZmmSampleProducer.cpp`. +- `app/nano_run.cpp` + - Adds the `zbb` and `zmm` channels to `nano_run`. + - Adds YAML parsing for `muon_corrections` and `nlo_ew`. + - The default Muon campaign version is `latest`; the default scale/smearing file is `muon_scalesmearing.json.gz` and the default SF file is `muon_Z.json.gz`. +- `app/nano_merge.cpp` + - Adds `--variations nominal,jes_up,...` to merge only selected variations. + - If omitted, all variations are merged; nominal files are also subject to the filter. + - Invalid, repeated, or incomplete variation arguments produce a usage error. +- `README.md` + - Adds descriptions and 2024 v15 run examples for the `zbb` and `zmm` channels. + - Documents the Muon payload path, the default `latest` campaign version, and how to pin a version. + - Adds an end-to-end example using `scripts/run_2024_z_channel.sh` and describes its arguments. + - Adds a Condor example using `zbb_2024_v15_MC.yaml`. + - Adds an example for merging with `nano_merge --variations nominal`. + +## 2. Common Configuration and Input Branches + +- `configs/base.yaml` + - Updates the AK4 JEC tags from `Summer24Prompt24_V3` to `Summer24Prompt24_V5`. + - Updates the AK4 JER tag from `Summer24Prompt24_JRV1_MC` to `Summer24Prompt24_JRV2_MC`. + - Adds Muon correction configuration: + - Payload root: `/cvmfs/cms-griddata.cern.ch/cat/metadata/MUO` + - Campaign: `2024_NanoAODv15` + - Payload subdirectory: `Run3-24CDEReprocessingFGHIPrompt-Summer24-NanoAODv15` + - Version: `latest` + - Files: `muon_scalesmearing.json.gz` and `muon_Z.json.gz` + - Adds NLO EW configuration: + - Payload directory: `data/vjets-ewk/2023-08-11` + - W file/main histogram: `WJetsCorr_collection_ewk.root` / `evj_pTV_kappa_EW` + - Z file/main histogram: `ZJetsCorr_collection_ewk.root` / `eej_pTV_kappa_EW` + - W uncertainties: `evj_pTV_d1kappa_EW`, `evj_pTV_d2kappa_EW`, `evj_pTV_d3kappa_EW` + - Z uncertainties: `eej_pTV_d1kappa_EW`, `eej_pTV_d2kappa_EW`, `eej_pTV_d3kappa_EW` +- `configs/common/read_branches_v9.yaml` and `configs/common/read_branches_v12.yaml` + - Add `LHE_Vpt`, `LHEPart_pdgId`, and `LHEPart_pt`. +- `configs/common/read_branches_v15.yaml` + - Adds `Muon_charge`, `Muon_highPtId`, `Muon_pfIsoId`, and `Muon_nTrackerLayers`. + - Adds `SV_pt`, `LHE_Vpt`, `LHEPart_pdgId`, and `LHEPart_pt`. + +## 3. New zbb Channel + +Related files: + +- `include/nano/producers/HeavyFlavZbbSampleProducer.h` +- `src/producers/HeavyFlavZbbSampleProducer.cpp` +- `configs/run/zbb_2024_v15.yaml` +- `configs/samples/zbb_2024_v15_DATA.yaml` +- `configs/samples/zbb_2024_v15_MC.yaml` + +The runtime card uses 2024 NanoAOD `v15`, inherits the base configuration, v15 branches, and stored tagger names, and uses the following preselection: + +```text +Sum$(abs(FatJet_eta)<2.4)>1 +``` + +Required triggers are `HLT_AK8PFJet380_SoftDropMass30` and `HLT_AK8PFJet500`. + +Event selection and processing: + +- Requires two JME-corrected AK8 jets. +- The two jets must have transverse momenta above `450` and `200 GeV`, respectively. +- The two leading jets must satisfy `|DeltaPhi| > pi/2`. +- The runtime card defaults to `require_sv_cut: true`; when enabled, at least two secondary vertices are required. +- A jet is marked as qualified only when it has exactly two linked subjets. +- At least one of the two probe jets must be qualified. +- Both leading probe jets are written to the output; `fj_2_*` branches are generated from the corresponding `fj_1_*` branches. +- Adds `passHTTrig`, recording `HLT_AK8PFJet380_SoftDropMass30`. +- Adds `genVpt`, recording the generator transverse momentum of a hard-process W/Z; if no hard-process copy is found, the maximum W/Z `pT` is used. + +Sample configuration: + +- DATA: `JetMET0` and `JetMET1`, covering Run2024 C–I and the I v2 NanoAOD v15 datasets. +- MC: + - `qcd-mg`: 8 QCD HT bins, from `200to400` through `2000`. + - `top`: 1 `TTto4Q` sample. + - `v-qq`: 3 W→qq and 3 Z→qq `PTQQ` bins. + - `vv-qq`: `WWto4Q`, `WZto4Q`, and `ZZto4Q`. + +## 4. New zmm Channel and Muon Corrections + +Related files: + +- `include/nano/producers/HeavyFlavZmmSampleProducer.h` +- `src/producers/HeavyFlavZmmSampleProducer.cpp` +- `include/nano/helpers/MuonCorrection.h` +- `src/helpers/MuonCorrection.cpp` +- `configs/run/zmm_2024_v15.yaml` +- `configs/samples/zmm_2024_v15_DATA.yaml` +- `configs/samples/zmm_2024_v15_MC.yaml` + +The runtime card uses 2024 NanoAOD `v15` and the following preselection: + +```text +Sum$(Muon_pt>50 && abs(Muon_eta)<2.4 && Muon_highPtId)>0 +&& Sum$(Muon_pt>25 && abs(Muon_eta)<2.4 && Muon_highPtId)>1 +``` + +Required triggers are `HLT_Mu50` and `HLT_TkMu50`. + +Event selection and output: + +- Applies the Muon scale correction; MC receives additional resolution smearing. +- Requires exactly two isolated muons passing the ID, `abs(eta)<2.4`, and `pfIsoId>1` requirements. +- The Muon ID is either `pt>15` with `looseId`, or `pt>30` with `highPtId != 0`. +- The two muons must have opposite charge, with leading/subleading `pT` above `60/30 GeV`. +- The dimuon system must have `pT > 450 GeV` and `70 < m < 110 GeV`. +- Selects corrected AK8 recoil jets with `DeltaR > 0.8` from both muons and keeps only the leading jet. +- Adds `passMuTrig`, `leptonicZ_pt`, `leptonicZ_mass`, and `genVpt`. +- Adds `pt`, `eta`, `phi`, `mass`, and `miniIso` branches for `muon0_*` and `muon1_*`. +- MC adds the following SFs and their `stat/syst` up/down variations: `muonHLTSF`, `muonIDSF`, `muonISOSF`, and `muonIDISOSF`. + +Muon correction implementation: + +- Looks up the campaign using `era + "_NanoAOD" + nano_version`. +- Reads data/MC scale, random smearing, and scale factors from correctionlib payloads. +- MC smearing uses the event number, luminosity block, and phi to generate reproducible random inputs, followed by an inverse Crystal Ball CDF. +- Scale factors are multiplied across the two muons; muons outside the eta or minimum-pT range return a factor of 1. +- Corrected pT is protected against non-finite values, excessive changes, and crossing `200 GeV`; invalid results fall back to the original pT. + +Sample configuration: + +- DATA: `Muon0` and `Muon1`, covering Run2024 C–I and the I v2 NanoAOD v15 datasets. +- MC: + - `DY`: 3 `PTLL` bins (200, 400, and 600). + - `top`: `TTto2L2Nu`, `TWminusto2L2Nu`, and `TbarWplusto2L2Nu`. + - `v-lep`: `WWto2L2Nu`, `WZto2L2Q`, and `ZZto2L2Q`. + +## 5. Common Outputs, NLO EW, and the HeavyFlav Base Class + +Related files: + +- `include/nano/producers/HeavyFlavBaseProducer.h` +- `src/producers/HeavyFlavBaseProducer.cpp` +- `include/nano/helpers/NloEWWeightProducer.h` +- `src/helpers/NloEWWeightProducer.cpp` + +`ProducerConfig` additions: + +- `MuonEraConfig` and the Muon campaign map. +- `NloEWConfig`, including W/Z files, the nominal histogram, and three uncertainty histograms for each boson. +- Boolean, numeric, and string maps in `channel_options`. + +New common HeavyFlav outputs and logic: + +- `LHE_Vpt`: for MC DY, reads the physical branch first; for W/Z, searches `LHEPart` by PDG ID `24/23` and falls back to the physical branch when needed; DATA is set to `-1`. +- `genVpt`: prefers W/Z particles with the hard-process flag in `GenPart`, otherwise uses the maximum W/Z `pT`. +- `nlo_ew_weight`, `nlo_ew_weight_up`, and `nlo_ew_weight_down`, all initialized to `1`. +- For zbb, generates the complete set of `fj_2_*` default branches automatically. +- Adds `fj_1_parTmass = rawMass * globalParT3_massCorrX2p`. +- When filling zbb jets, preserves the leading-jet values and writes the second jet to `fj_2_*`. + +NLO EW weight logic: + +- Samples starting with `Wto` or `WJetsTo` use the W payload. +- Samples starting with `Zto`, `ZJetsTo`, `DYto`, or `DYJetsTo` use the Z payload. +- Uses the hard-process W/Z `pT`; values below `100 GeV` retain the default weight. +- The nominal weight is `1 + kappa`; the three uncertainty histogram deviations are combined in quadrature for the up/down weights. +- ROOT histograms are cloned and detached from the source file; missing files, histograms, or uncertainty entries raise an error. + +## 6. Sample Cross Sections and Correction Data + +- `configs/samples/xsec_2024.conf` + - Adds 2024 zbb/zmm MC cross sections in pb. + - Covers `TTto4Q`, 8 QCD HT bins, W/Z→qq, four-quark VV, 3 DY `PTLL` bins, dileptonic/single-top samples, and leptonic VV samples. + - The values are documented as being ported from NanoHRTTools `samples_nanov15/xsec_2024.conf`. +- `data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.root` + - Adds the W+jets NLO EW nominal weight and three uncertainty histogram payloads. +- `data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.root` + - Adds the Z+jets/DY NLO EW nominal weight and three uncertainty histogram payloads. + +## 7. Staged File List + +The 27 staged files are: + +```text +CMakeLists.txt +README.md +app/nano_merge.cpp +app/nano_run.cpp +configs/base.yaml +configs/common/read_branches_v12.yaml +configs/common/read_branches_v15.yaml +configs/common/read_branches_v9.yaml +configs/run/zbb_2024_v15.yaml +configs/run/zmm_2024_v15.yaml +configs/samples/xsec_2024.conf +configs/samples/zbb_2024_v15_DATA.yaml +configs/samples/zbb_2024_v15_MC.yaml +configs/samples/zmm_2024_v15_DATA.yaml +configs/samples/zmm_2024_v15_MC.yaml +data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.root +data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.root +include/nano/helpers/MuonCorrection.h +include/nano/helpers/NloEWWeightProducer.h +include/nano/producers/HeavyFlavBaseProducer.h +include/nano/producers/HeavyFlavZbbSampleProducer.h +include/nano/producers/HeavyFlavZmmSampleProducer.h +src/helpers/MuonCorrection.cpp +src/helpers/NloEWWeightProducer.cpp +src/producers/HeavyFlavBaseProducer.cpp +src/producers/HeavyFlavZbbSampleProducer.cpp +src/producers/HeavyFlavZmmSampleProducer.cpp +``` + +The following are outside the scope of this document: `CMSJMECalculators`, `templates/condor/process.sh.in`, `scripts/`, and all other unstaged or untracked files. diff --git a/app/nano_make_condor.cpp b/app/nano_make_condor.cpp index e588cea..ff2df50 100644 --- a/app/nano_make_condor.cpp +++ b/app/nano_make_condor.cpp @@ -1,4 +1,5 @@ #include "runtime_common.h" +#include "nano/helpers/JmeVariation.h" #include #include @@ -72,7 +73,7 @@ CliOptions parse_args(int argc, char **argv) { } } if (opts.input_yaml.empty() || opts.job_dir.empty() || opts.output_dir.empty() || opts.config_file.empty()) { - throw std::runtime_error("Usage: nano_make_condor --input-yaml --job-dir --output-dir --config [--nfiles-per-job 1] [--variations nominal,jes_up,...]. If omitted, --variations defaults to nominal."); + throw std::runtime_error("Usage: nano_make_condor --input-yaml --job-dir --output-dir --config [--nfiles-per-job 1] [--variations nominal,jes_up,...,muon_smear_down]. If omitted, --variations defaults to nominal."); } return opts; } @@ -86,7 +87,18 @@ void validate_data_variations(const CliOptions &cli) { if (!cli.run_data || variations == "nominal") { return; } - throw std::runtime_error("--run-data does not support JME variations. If --variations is used with --run-data, it must be the single value 'nominal'; otherwise omit --variations."); + throw std::runtime_error("--run-data does not support systematic variations. Use only 'nominal'."); +} + +void validate_variation_scope(const CliOptions &cli) { + if (cli.channel == "zmm") { + return; + } + for (const auto variation : nano::parse_jme_variation_list(normalized_variations_arg(cli))) { + if (nano::is_muon_variation(variation)) { + throw std::runtime_error("Muon scale/smear variations are supported only for the zmm MC channel"); + } + } } std::string write_merged_config(const fs::path &path, const YAML::Node &settings) { @@ -226,6 +238,7 @@ int main(int argc, char **argv) { try { const auto cli = parse_args(argc, argv); validate_data_variations(cli); + validate_variation_scope(cli); auto settings = nano::runtime::load_config_with_extends(cli.config_file); for (const auto &[key, value] : cli.overrides) { nano::runtime::apply_override(settings, key, value); diff --git a/app/nano_merge.cpp b/app/nano_merge.cpp index a9761a8..d6d3529 100644 --- a/app/nano_merge.cpp +++ b/app/nano_merge.cpp @@ -24,21 +24,25 @@ namespace fs = std::filesystem; namespace { constexpr std::array kAllowedVariations = { - std::string_view{"nominal"}, - std::string_view{"jes_up"}, - std::string_view{"jes_down"}, - std::string_view{"jer_up"}, - std::string_view{"jer_down"}, - std::string_view{"met_up"}, - std::string_view{"met_down"}, + std::string_view{"nominal"}, std::string_view{"jes_up"}, + std::string_view{"jes_down"}, std::string_view{"jer_up"}, + std::string_view{"jer_down"}, std::string_view{"met_up"}, + std::string_view{"met_down"}, std::string_view{"muon_scale_up"}, + std::string_view{"muon_scale_down"}, std::string_view{"muon_smear_up"}, + std::string_view{"muon_smear_down"}, }; constexpr std::size_t kHaddChunkSize = 200; constexpr std::string_view kUsage = - "Usage: nano_merge [--resume-from ]\n" - " : base Condor output directory; piece files are read from /pieces\n" - " --resume-from: reuse a previous nano_merge temporary directory and skip groups whose temporary output already exists"; + "Usage: nano_merge [--variations ] [--resume-from ]\n" + " : base Condor output directory; piece files are read from " + "/pieces\n" + " --variations: comma-separated variation list; omitted means all " + "variations\n" + " --resume-from: reuse a previous nano_merge temporary directory; " + "completed groups already in the final or temporary " + "directory are skipped or published"; std::string shell_quote(const fs::path &path) { std::string s = path.string(); @@ -54,23 +58,22 @@ std::string shell_quote(const fs::path &path) { return out; } -std::set allowed_variations() { - return {std::begin(kAllowedVariations), std::end(kAllowedVariations)}; +fs::path temporary_base() { + const char *tmp = std::getenv("TMPDIR"); + return tmp != nullptr && tmp[0] != '\0' ? fs::path(tmp) : fs::path("/tmp"); } fs::path make_output_root() { - const char *tmp = std::getenv("TMPDIR"); - fs::path base = tmp != nullptr && tmp[0] != '\0' ? fs::path(tmp) : fs::path("/tmp"); auto now = std::chrono::system_clock::now().time_since_epoch(); - const auto seconds = std::chrono::duration_cast(now).count(); - return base / ("nano_merge_" + std::to_string(::getpid()) + "_" + std::to_string(seconds)); + const auto seconds = + std::chrono::duration_cast(now).count(); + return temporary_base() / ("nano_merge_" + std::to_string(::getpid()) + + "_" + std::to_string(seconds)); } bool is_safe_resume_dir(const fs::path &path) { - const char *tmp = std::getenv("TMPDIR"); - const fs::path temp_base = tmp != nullptr && tmp[0] != '\0' ? fs::path(tmp) : fs::path("/tmp"); std::error_code error; - const auto canonical_base = fs::weakly_canonical(temp_base, error); + const auto canonical_base = fs::weakly_canonical(temporary_base(), error); if (error) { return false; } @@ -100,30 +103,59 @@ using FileGroup = std::vector; struct CliOptions { fs::path output_dir; fs::path resume_dir; + std::set variations; + bool filter_variations = false; }; CliOptions parse_args(int argc, char **argv) { - if (argc != 2 && argc != 4) { + if (argc < 2) { throw std::runtime_error(std::string(kUsage)); } CliOptions options; options.output_dir = fs::path(argv[1]); - if (argc == 4) { - const std::string flag = argv[2]; - if (flag != "--resume-from") { - throw std::runtime_error(std::string(kUsage)); + bool has_resume = false; + bool has_variations = false; + for (int index = 2; index < argc; ++index) { + const std::string flag = argv[index]; + if (flag == "--resume-from") { + if (has_resume || ++index >= argc) { + throw std::runtime_error(std::string(kUsage)); + } + options.resume_dir = fs::path(argv[index]); + has_resume = true; + continue; } - options.resume_dir = fs::path(argv[3]); + if (flag == "--variations") { + if (has_variations || ++index >= argc) { + throw std::runtime_error(std::string(kUsage)); + } + const std::string value = argv[index]; + if (value.empty()) { + throw std::runtime_error(std::string(kUsage)); + } + std::stringstream values(value); + std::string variation; + while (std::getline(values, variation, ',')) { + if (variation.empty() || + std::find(kAllowedVariations.begin(), kAllowedVariations.end(), + variation) == kAllowedVariations.end()) { + throw std::runtime_error("Unknown variation in --variations: " + variation); + } + options.variations.insert(variation); + } + if (options.variations.empty()) { + throw std::runtime_error(std::string(kUsage)); + } + options.filter_variations = true; + has_variations = true; + continue; + } + throw std::runtime_error(std::string(kUsage)); } return options; } -struct HaddInput { - int index = 0; - fs::path path; -}; - -using HaddInputGroup = std::vector; +using HaddInputGroup = std::vector; std::string describe_system_status(int status) { std::ostringstream out; @@ -139,8 +171,8 @@ std::string describe_system_status(int status) { void run_hadd(const HaddInputGroup &files, const fs::path &output) { std::vector quoted; quoted.reserve(files.size()); - for (const auto &item : files) { - quoted.push_back(shell_quote(item.path)); + for (const auto &file : files) { + quoted.push_back(shell_quote(file)); } std::string cmd = "hadd -f " + shell_quote(output); @@ -151,7 +183,8 @@ void run_hadd(const HaddInputGroup &files, const fs::path &output) { const int status = std::system(cmd.c_str()); if (status != 0) { - throw std::runtime_error("hadd failed for " + output.string() + " (" + describe_system_status(status) + ")"); + throw std::runtime_error("hadd failed for " + output.string() + " (" + + describe_system_status(status) + ")"); } } @@ -159,7 +192,7 @@ HaddInputGroup to_hadd_inputs(const FileGroup &files) { HaddInputGroup out; out.reserve(files.size()); for (const auto &file : files) { - out.push_back({file.index, file.path}); + out.push_back(file.path); } return out; } @@ -169,37 +202,89 @@ fs::path partial_output_path(const fs::path &output, std::size_t batch_index) { return partial_dir / ("part_" + std::to_string(batch_index) + ".root"); } -void run_chunked_hadd(const FileGroup &files, const fs::path &output, bool resume_mode) { +fs::path partial_output_dir(const fs::path &output) { + return output.parent_path() / ".partials" / output.stem(); +} + +fs::path group_output_path(const std::string &nickname, + const std::string &variation, const fs::path &root) { + return variation.empty() + ? root / (nickname + ".root") + : root / variation / (nickname + "_" + variation + ".root"); +} + +void remove_group_temporary_files(const fs::path &temporary_output) { + fs::remove(temporary_output); + const auto partial_dir = partial_output_dir(temporary_output); + fs::remove_all(partial_dir); + std::error_code error; + fs::remove(partial_dir.parent_path(), + error); // Removes .partials only when it is empty. +} + +void publish_output(const fs::path &temporary_output, + const fs::path &final_output) { + fs::create_directories(final_output.parent_path()); + + std::error_code rename_error; + fs::rename(temporary_output, final_output, rename_error); + if (rename_error) { + // TMPDIR and the final output are commonly on different filesystems. Copy + // to a staging name first so an interrupted copy is never mistaken for a + // completed output by --resume-from. + auto staging = final_output; + staging += ".nano_merge_" + std::to_string(::getpid()) + ".tmp"; + try { + fs::copy_file(temporary_output, staging, + fs::copy_options::overwrite_existing); + fs::rename(staging, final_output); + fs::remove(temporary_output); + } catch (...) { + std::error_code cleanup_error; + fs::remove(staging, cleanup_error); + throw; + } + } + + remove_group_temporary_files(temporary_output); + std::cout << "[step] Published completed output: " << final_output << "\n"; +} + +void run_chunked_hadd(const FileGroup &files, const fs::path &output, + bool resume_mode) { if (files.size() <= kHaddChunkSize) { run_hadd(to_hadd_inputs(files), output); return; } const auto batches = (files.size() + kHaddChunkSize - 1U) / kHaddChunkSize; - std::cout << "[step] Splitting hadd into " << batches << " partial batches of up to " << kHaddChunkSize << " files\n"; + std::cout << "[step] Splitting hadd into " << batches + << " partial batches of up to " << kHaddChunkSize << " files\n"; HaddInputGroup partials; partials.reserve(batches); for (std::size_t batch = 0; batch < batches; ++batch) { const auto begin = batch * kHaddChunkSize; const auto end = std::min(files.size(), begin + kHaddChunkSize); const auto partial = partial_output_path(output, batch); - partials.push_back({static_cast(batch), partial}); + partials.push_back(partial); if (resume_mode && fs::exists(partial)) { - std::cout << "[step] Reusing existing partial output: " << partial << "\n"; + std::cout << "[step] Reusing existing partial output: " << partial + << "\n"; continue; } fs::create_directories(partial.parent_path()); HaddInputGroup chunk; chunk.reserve(end - begin); for (std::size_t idx = begin; idx < end; ++idx) { - chunk.push_back({files[idx].index, files[idx].path}); + chunk.push_back(files[idx].path); } run_hadd(chunk, partial); } run_hadd(partials, output); } -bool merge_or_copy(const FileGroup &files, const fs::path &output, bool resume_mode) { +bool merge_or_copy(const FileGroup &files, const fs::path &output, + bool resume_mode) { if (files.empty()) { return false; } @@ -218,40 +303,63 @@ bool merge_or_copy(const FileGroup &files, const fs::path &output, bool resume_m return true; } -bool merge_group(const std::string &nickname, const std::string &variation, const FileGroup &files, const fs::path &output_root, +bool merge_group(const std::string &nickname, const std::string &variation, + const FileGroup &files, const fs::path &output_root, bool resume_mode) { auto sorted_files = files; - std::sort(sorted_files.begin(), sorted_files.end(), [](const PieceEntry &a, const PieceEntry &b) { return a.index < b.index; }); - const auto output = variation.empty() ? output_root / (nickname + ".root") : output_root / variation / (nickname + "_" + variation + ".root"); + std::sort(sorted_files.begin(), sorted_files.end(), + [](const PieceEntry &a, const PieceEntry &b) { + return a.index < b.index; + }); + const auto output = group_output_path(nickname, variation, output_root); std::cout << "[step] Merging " << sorted_files.size() << " files for " - << (variation.empty() ? (nickname + " (nominal)") : (nickname + ", variation=" + variation)) << "\n" + << (variation.empty() ? (nickname + " (nominal)") + : (nickname + ", variation=" + variation)) + << "\n" << " output: " << output << "\n"; return merge_or_copy(sorted_files, output, resume_mode); } -void copy_tree_contents(const fs::path &from, const fs::path &to) { - fs::create_directories(to); - for (auto it = fs::recursive_directory_iterator(from); it != fs::recursive_directory_iterator(); ++it) { - const auto &entry = *it; - if (entry.is_directory() && entry.path().filename() == ".partials") { - it.disable_recursion_pending(); - continue; - } - const auto relative = fs::relative(entry.path(), from); - const auto target = to / relative; - if (entry.is_directory()) { - fs::create_directories(target); - continue; - } - if (!entry.is_regular_file()) { - continue; - } - fs::create_directories(target.parent_path()); - fs::copy_file(entry.path(), target, fs::copy_options::overwrite_existing); +struct MergeStats { + std::size_t merged = 0; + std::size_t copied = 0; + std::size_t skipped = 0; + std::size_t outputs = 0; +}; + +void process_group(const std::string &nickname, const std::string &variation, + const FileGroup &files, const fs::path &temporary_root, + const fs::path &output_dir, bool resume_mode, + MergeStats &stats) { + if (files.empty()) { + return; + } + + ++stats.outputs; + const auto temporary_output = + group_output_path(nickname, variation, temporary_root); + const auto final_output = group_output_path(nickname, variation, output_dir); + if (resume_mode && fs::exists(final_output)) { + std::cout << "[step] Reusing existing final output: " << final_output + << "\n"; + remove_group_temporary_files(temporary_output); + ++stats.skipped; + return; + } + + const bool did_write = + merge_group(nickname, variation, files, temporary_root, resume_mode); + publish_output(temporary_output, final_output); + if (!did_write) { + ++stats.skipped; + } else if (files.size() == 1) { + ++stats.copied; + } else { + ++stats.merged; } } -} // namespace +} // namespace int main(int argc, char **argv) { try { @@ -263,14 +371,15 @@ int main(int argc, char **argv) { return 1; } const bool resume_mode = !cli.resume_dir.empty(); - if (resume_mode && (!fs::exists(cli.resume_dir) || !fs::is_directory(cli.resume_dir))) { - std::cerr << "Resume directory must exist and be a directory: " << cli.resume_dir << "\n"; + if (resume_mode && + (!fs::exists(cli.resume_dir) || !fs::is_directory(cli.resume_dir))) { + std::cerr << "Resume directory must exist and be a directory: " + << cli.resume_dir << "\n"; return 1; } if (resume_mode && !is_safe_resume_dir(cli.resume_dir)) { std::cerr << "Resume directory must be a nano_merge_* directory under " - << (std::getenv("TMPDIR") != nullptr && std::getenv("TMPDIR")[0] != '\0' ? std::getenv("TMPDIR") : "/tmp") << ": " - << cli.resume_dir << "\n"; + << temporary_base() << ": " << cli.resume_dir << "\n"; return 1; } @@ -280,7 +389,9 @@ int main(int argc, char **argv) { return 1; } - const std::set allowed = allowed_variations(); + const auto selected = [&cli](const std::string &variation) { + return !cli.filter_variations || cli.variations.count(variation) != 0; + }; // nickname -> files without variation std::map no_variation; // variation -> nickname -> files @@ -307,15 +418,22 @@ int main(int argc, char **argv) { const std::string nickname = match[1].str(); const int idx = std::stoi(match[2].str()); const std::string variation = match[3].str(); - if (allowed.count(variation) == 0) { + if (std::find(kAllowedVariations.begin(), kAllowedVariations.end(), + variation) == kAllowedVariations.end()) { std::cerr << "Skipping unknown variation: " << filename << "\n"; continue; } + if (!selected(variation)) { + continue; + } by_variation[variation][nickname].push_back({idx, entry.path()}); ++total_root; continue; } if (std::regex_match(filename, match, no_var_re)) { + if (!selected("nominal")) { + continue; + } const std::string nickname = match[1].str(); const int idx = std::stoi(match[2].str()); no_variation[nickname].push_back({idx, entry.path()}); @@ -330,67 +448,43 @@ int main(int argc, char **argv) { return 1; } - const fs::path output_root = resume_mode ? cli.resume_dir : make_output_root(); + const fs::path output_root = + resume_mode ? cli.resume_dir : make_output_root(); fs::create_directories(output_root); if (resume_mode) { - std::cout << "Resuming with temporary output dir: " << output_root << "\n"; + std::cout << "Resuming with temporary output dir: " << output_root + << "\n"; } else { std::cout << "Created output dir: " << output_root << "\n"; } - std::size_t merged_count = 0; - std::size_t copied_count = 0; - std::size_t skipped_count = 0; - std::size_t written_files = 0; + MergeStats stats; for (const auto &[nickname, files] : no_variation) { - if (files.empty()) { - continue; - } - ++written_files; - const bool did_write = merge_group(nickname, "", files, output_root, resume_mode); - if (!did_write) { - ++skipped_count; - } else if (files.size() == 1) { - ++copied_count; - } else { - ++merged_count; - } + process_group(nickname, "", files, output_root, output_dir, resume_mode, + stats); } for (const auto &[variation, per_nick] : by_variation) { std::cout << "Preparing variation: " << variation << "\n"; for (const auto &[nickname, files] : per_nick) { - if (files.empty()) { - continue; - } - ++written_files; - const bool did_write = merge_group(nickname, variation, files, output_root, resume_mode); - if (!did_write) { - ++skipped_count; - } else if (files.size() == 1) { - ++copied_count; - } else { - ++merged_count; - } + process_group(nickname, variation, files, output_root, output_dir, + resume_mode, stats); } } std::cout << "Summary:\n"; std::cout << " input files: " << total_root << "\n"; - std::cout << " merged groups: " << merged_count << "\n"; - std::cout << " copied singleton groups: " << copied_count << "\n"; - std::cout << " skipped existing groups: " << skipped_count << "\n"; - std::cout << " output files written: " << written_files << "\n"; - std::cout << "All outputs written under: " << output_root << "\n"; - std::cout << "[step] Copying merged outputs from temporary dir to final output dir\n" - << " from: " << output_root << "\n" - << " to: " << output_dir << "\n"; - copy_tree_contents(output_root, output_dir); - std::cout << "Final merged outputs copied under: " << output_dir << "\n"; + std::cout << " merged groups: " << stats.merged << "\n"; + std::cout << " copied singleton groups: " << stats.copied << "\n"; + std::cout << " skipped existing groups: " << stats.skipped << "\n"; + std::cout << " output files written: " << stats.outputs << "\n"; + std::cout << "All completed outputs published under: " << output_dir + << "\n"; std::error_code cleanup_error; fs::remove_all(output_root, cleanup_error); if (cleanup_error) { - std::cerr << "Warning: failed to remove temporary output dir " << output_root << ": " << cleanup_error.message() << "\n"; + std::cerr << "Warning: failed to remove temporary output dir " + << output_root << ": " << cleanup_error.message() << "\n"; } else { std::cout << "Removed temporary output dir: " << output_root << "\n"; } diff --git a/app/nano_run.cpp b/app/nano_run.cpp index 56290b2..7c29daf 100644 --- a/app/nano_run.cpp +++ b/app/nano_run.cpp @@ -3,10 +3,14 @@ #include "nano/io/RootOutputFile.h" #include "nano/producers/HeavyFlavMinimalProducer.h" #include "nano/producers/HeavyFlavMuonSampleProducer.h" +#include "nano/producers/HeavyFlavQCDSampleProducer.h" +#include "nano/producers/HeavyFlavZbbSampleProducer.h" +#include "nano/producers/HeavyFlavZmmSampleProducer.h" #include "runtime_common.h" #include +#include #include #include #include @@ -21,6 +25,8 @@ namespace fs = std::filesystem; namespace { +constexpr std::array kLheWeightBranches = {"LHEScaleWeight", "LHEPdfWeight"}; + bool parse_double_strict(const std::string &text, double &value) { char *end = nullptr; errno = 0; @@ -112,7 +118,7 @@ CliOptions parse_args(int argc, char **argv) { } if (opts.input_files.empty() || opts.output_file.empty() || opts.config_file.empty()) { - throw std::runtime_error("Usage: nano_run --input-files --output-file --config [--channel muon|minimal] [--num-events -1] [--run-data] [--variations nominal,jes_up,...] [--set key=value]. If omitted, --variations defaults to nominal."); + throw std::runtime_error("Usage: nano_run --input-files --output-file --config [--channel muon|minimal|qcd|zbb|zmm] [--num-events -1] [--run-data] [--variations nominal,jes_up,...,muon_smear_down] [--set key=value]. If omitted, --variations defaults to nominal."); } return opts; } @@ -126,7 +132,18 @@ void validate_data_variations(const CliOptions &cli) { if (!cli.run_data || variations == "nominal") { return; } - throw std::runtime_error("--run-data does not support JME variations. If --variations is used with --run-data, it must be the single value 'nominal'; otherwise omit --variations."); + throw std::runtime_error("--run-data does not support systematic variations. Use only 'nominal'."); +} + +void validate_variation_scope(const CliOptions &cli, const std::vector &variations) { + if (cli.channel == "zmm") { + return; + } + for (const auto variation : variations) { + if (nano::is_muon_variation(variation)) { + throw std::runtime_error("Muon scale/smear variations are supported only for the zmm MC channel"); + } + } } nano::ProducerConfig make_config(const YAML::Node &settings, const std::string &channel) { @@ -203,6 +220,8 @@ nano::ProducerConfig make_config(const YAML::Node &settings, const std::string & if (config.read_branches.empty()) { throw std::runtime_error("Missing or empty read_branches list in config"); } + const auto additional_read_branches = nano::runtime::yaml_string_list(settings, "additional_read_branches"); + config.read_branches.insert(config.read_branches.end(), additional_read_branches.begin(), additional_read_branches.end()); if (settings["output"] && settings["output"]["include_lhe_weights"]) { config.include_lhe_weights = settings["output"]["include_lhe_weights"].as(); } @@ -210,18 +229,20 @@ nano::ProducerConfig make_config(const YAML::Node &settings, const std::string & // read_branches safety checks: // - Runtime cards should explicitly list every physical NanoAOD branch the // channel reads. - // - required_triggers, stored_tagger_names, and optional LHEScaleWeight can - // imply extra physical branches; auto-add them for backward compatibility, - // but warn so the card can be made explicit. - // - If LHEScaleWeight is listed while output.include_lhe_weights is disabled, + // - required_triggers, stored_tagger_names, and optional LHE weight vectors can + // imply extra physical branches; auto-add them for backward compatibility. + // - If an LHE weight vector is listed while output.include_lhe_weights is disabled, // remove it so a stale read_branches entry does not silently read an unused // large vector branch. if (!config.include_lhe_weights) { - const auto old_size = config.read_branches.size(); - config.read_branches.erase(std::remove(config.read_branches.begin(), config.read_branches.end(), "LHEScaleWeight"), - config.read_branches.end()); - if (config.read_branches.size() != old_size) { - std::cerr << "Warning: removing LHEScaleWeight from read_branches because output.include_lhe_weights is false.\n"; + for (const auto branch_name : kLheWeightBranches) { + const auto old_size = config.read_branches.size(); + config.read_branches.erase(std::remove(config.read_branches.begin(), config.read_branches.end(), branch_name), + config.read_branches.end()); + if (config.read_branches.size() != old_size) { + std::cerr << "Warning: removing " << branch_name + << " from read_branches because output.include_lhe_weights is false.\n"; + } } } if (!settings["stored_tagger_names"]) { @@ -245,10 +266,12 @@ nano::ProducerConfig make_config(const YAML::Node &settings, const std::string & config.read_branches.push_back(branch_name); } } - if (config.include_lhe_weights && seen.insert("LHEScaleWeight").second) { - std::cerr << "Warning: adding missing branch LHEScaleWeight to read_branches because output.include_lhe_weights is true. " - "Please list it explicitly in read_branches.\n"; - config.read_branches.push_back("LHEScaleWeight"); + if (config.include_lhe_weights) { + for (const auto branch_name : kLheWeightBranches) { + if (seen.insert(std::string(branch_name)).second) { + config.read_branches.emplace_back(branch_name); + } + } } for (const auto &branch_name : config.read_branches) { if (config.nano_branch_types.count(branch_name) != 0U) { @@ -315,6 +338,30 @@ nano::ProducerConfig make_config(const YAML::Node &settings, const std::string & item.second["correction_key"].as(), }; } + if (settings["muon_corrections"]) { + const auto node = settings["muon_corrections"]; + config.muon_payload_dir = node["payload_dir"].as(); + config.muon_smearing_file = node["smearing_file"].as(); + config.muon_smearing_tool = node["smearing_tool"] ? node["smearing_tool"].as() : "stdflat"; + for (const auto &item : node["campaigns"]) { + config.muon_eras[item.first.as()] = { + item.second["payload_subdir"].as(), + item.second["version"] ? item.second["version"].as() : "latest", + item.second["scale_smearing_file"] ? item.second["scale_smearing_file"].as() : "muon_scalesmearing.json.gz", + item.second["sf_file"] ? item.second["sf_file"].as() : "muon_Z.json.gz", + }; + } + } + if (settings["nlo_ew"]) { + const auto node = settings["nlo_ew"]; + config.nlo_ew = { + node["payload_dir"].as(), + node["w_file"].as(), + node["z_file"].as(), + node["w_correction"].as(), + node["z_correction"].as(), + }; + } if (settings["jet_veto_map"]) { const auto node = settings["jet_veto_map"]; config.jet_veto_map_enabled = node["enabled"] ? node["enabled"].as() : false; @@ -333,12 +380,21 @@ nano::ProducerConfig make_config(const YAML::Node &settings, const std::string & } std::unique_ptr make_producer(const nano::ProducerConfig &config) { + if (config.channel == "zbb") { + return std::make_unique(config); + } + if (config.channel == "zmm") { + return std::make_unique(config); + } if (config.channel == "muon") { return std::make_unique(config); } if (config.channel == "minimal") { return std::make_unique(config); } + if (config.channel == "qcd") { + return std::make_unique(config); + } throw std::runtime_error("Unsupported channel: " + config.channel); } @@ -370,8 +426,23 @@ std::vector process_one_file_variations(const std::string &input_fi if (!tree) { throw std::runtime_error("Missing tree " + cli.tree_name + " in " + input_file); } + if (!tree->GetBranch("genWeight")) { + for (const auto variation : variations) { + if (nano::is_muon_variation(variation)) { + throw std::runtime_error("Muon scale/smear variations are supported only for the zmm MC channel"); + } + } + } const auto config = make_config(settings, cli.channel); + if (config.include_lhe_weights && tree->GetBranch("genWeight")) { + for (const auto branch_name : kLheWeightBranches) { + if (!tree->GetBranch(std::string(branch_name).c_str())) { + std::cerr << "Warning: MC input is missing optional " << branch_name + << "; the output vector will be empty.\n"; + } + } + } const auto lumi_mask = cli.run_data ? std::make_unique(nano::runtime::LumiMask::from_file(data_lumi_mask_path(settings, config))) : nullptr; auto producer_base = make_producer(config); @@ -394,6 +465,14 @@ std::vector process_one_file_variations(const std::string &input_fi } auto output = std::make_unique(path); output->book_events(producer->output()); + if (config.include_lhe_weights) { + for (const auto branch_name : kLheWeightBranches) { + const auto *input_branch = tree->GetBranch(std::string(branch_name).c_str()); + if (input_branch) { + output->set_branch_title(branch_name, input_branch->GetTitle()); + } + } + } outputs.push_back({variation, std::move(path), std::move(output), 0U}); } @@ -445,6 +524,7 @@ int main(int argc, char **argv) { } const auto variations = nano::parse_jme_variation_list(normalized_variations_arg(cli)); + validate_variation_scope(cli, variations); if (inputs.size() == 1U) { process_one_file_variations(inputs.front(), cli.output_file, cli, settings, variations); return 0; diff --git a/configs/base.yaml b/configs/base.yaml index dfd71d6..a6baad9 100644 --- a/configs/base.yaml +++ b/configs/base.yaml @@ -19,14 +19,17 @@ channels: minimal: leading_fatjet_pt_min: 400.0 + qcd: + apply_sv_criteria: true + fill_sv: true # Output-level optional branches. Keep large vector branches disabled by # default and enable them in a channel card or with `--set` when needed. output: - # Copy the input `LHEScaleWeight` vector into nominal output. This is useful - # for MC validation or scale-envelope studies. If enabled for data or an input - # without the branch, the output branch is present and filled with an empty - # vector. + # Copy the input `LHEScaleWeight` and `LHEPdfWeight` vectors into nominal + # output. This is useful for MC validation or theory-uncertainty studies. If + # enabled for data or an input without the branch, the output branch is + # present and filled with an empty vector. include_lhe_weights: false # Certified luminosity masks used when `nano_run --run-data` is enabled. @@ -341,9 +344,9 @@ jec: jes_uncertainties: [Total] jet: algo: AK4PFPuppi - jec_tag_mc: Summer24Prompt24_V3_MC - jec_tag_data: Summer24Prompt24_V3_DATA - jer_tag_mc: Summer24Prompt24_JRV1_MC + jec_tag_mc: Summer24Prompt24_V5_MC + jec_tag_data: Summer24Prompt24_V5_DATA + jer_tag_mc: Summer24Prompt24_JRV2_MC fatjet: algo: AK8PFPuppi jec_tag_mc: inherit @@ -355,16 +358,16 @@ jec: jec_tag_data: inherit jer_tag_mc: inherit 2025_NanoAODv15: - payload_subdir: Run3-25Prompt-Winter25-NanoAODv15 + payload_subdir: Run3-25Prompt-Summer24-NanoAODv15 jet_jerc_file: jet_jerc.json.gz fatjet_jerc_file: fatJet_jerc.json.gz met_xy_corr_era: "" jes_uncertainties: [Total] jet: algo: AK4PFPuppi - jec_tag_mc: Winter25Prompt25_V3_MC - jec_tag_data: Winter25Prompt25_V3_DATA - jer_tag_mc: Summer24Prompt25_JRV1_MC + jec_tag_mc: Summer24Prompt25_V3_MC + jec_tag_data: Summer24Prompt25_V3_DATA + jer_tag_mc: Summer24Prompt25_JRV2_MC fatjet: algo: AK8PFPuppi jec_tag_mc: inherit @@ -441,6 +444,28 @@ pu: payload_subdir: Run3-25Prompt-Summer24-NanoAODv15/latest/puWeights_2025pp_Golden_Summer24_25ns_69200ub.json.gz correction_key: Collisions25_goldenJSON +# Muon scale, smearing, and efficiency payloads. Campaign versions are kept +# explicit so a runtime card can pin a dated payload for reproducibility. +muon_corrections: + payload_dir: /cvmfs/cms-griddata.cern.ch/cat/metadata/MUO + # CMSJMECalculators expects a deterministic four-input stdflat correction. + smearing_file: data/muon-smearing/stdflat.json.gz + smearing_tool: stdflat + campaigns: + 2024_NanoAODv15: + payload_subdir: Run3-24CDEReprocessingFGHIPrompt-Summer24-NanoAODv15 + version: latest + scale_smearing_file: muon_scalesmearing.json.gz + sf_file: muon_Z.json.gz + +# NLO electroweak corrections for W/Z+jets and DY samples. +nlo_ew: + payload_dir: data/vjets-ewk/2023-08-11 + w_file: WJetsCorr_collection_ewk.json.gz + z_file: ZJetsCorr_collection_ewk.json.gz + w_correction: evj_pTV_EW + z_correction: eej_pTV_EW + # Numeric year label written to the output `year` branch. The mapping follows # the historical Python producer convention, where split eras such as 2022EE # and 2023BPix use half-step labels. diff --git a/configs/common/nano_branches_v15.yaml b/configs/common/nano_branches_v15.yaml index e2e0782..109dab3 100644 --- a/configs/common/nano_branches_v15.yaml +++ b/configs/common/nano_branches_v15.yaml @@ -2542,6 +2542,9 @@ nano_branches: "HLT_PFHT1050": type: "bool" root_type: "Bool_t" + "HLT_PFHT900": + type: "bool" + root_type: "Bool_t" "HLT_PFHT180": type: "bool" root_type: "Bool_t" diff --git a/configs/common/nano_branches_v9.yaml b/configs/common/nano_branches_v9.yaml index f173b9c..fb27951 100644 --- a/configs/common/nano_branches_v9.yaml +++ b/configs/common/nano_branches_v9.yaml @@ -2099,6 +2099,9 @@ nano_branches: "HLT_PFHT1050": type: "bool" root_type: "Bool_t" + "HLT_PFHT900": + type: "bool" + root_type: "Bool_t" "HLT_PFHT180": type: "bool" root_type: "Bool_t" diff --git a/configs/common/read_branches_v12.yaml b/configs/common/read_branches_v12.yaml index 7b16781..69540a6 100644 --- a/configs/common/read_branches_v12.yaml +++ b/configs/common/read_branches_v12.yaml @@ -129,3 +129,6 @@ read_branches: - GenPart_status - GenPart_statusFlags - GenPart_genPartIdxMother + - LHE_Vpt + - LHEPart_pdgId + - LHEPart_pt diff --git a/configs/common/read_branches_v15.yaml b/configs/common/read_branches_v15.yaml index 0e2f91d..890713d 100644 --- a/configs/common/read_branches_v15.yaml +++ b/configs/common/read_branches_v15.yaml @@ -27,10 +27,14 @@ read_branches: - Muon_eta - Muon_phi - Muon_mass + - Muon_charge - Muon_dxy - Muon_dz - Muon_tightId - Muon_looseId + - Muon_highPtId + - Muon_pfIsoId + - Muon_nTrackerLayers - Muon_miniPFRelIso_all - Electron_pt - Electron_eta @@ -165,3 +169,7 @@ read_branches: - GenPart_status - GenPart_statusFlags - GenPart_genPartIdxMother + - SV_pt + - LHE_Vpt + - LHEPart_pdgId + - LHEPart_pt diff --git a/configs/common/read_branches_v9.yaml b/configs/common/read_branches_v9.yaml index c7c7caf..b8a3a5b 100644 --- a/configs/common/read_branches_v9.yaml +++ b/configs/common/read_branches_v9.yaml @@ -135,3 +135,6 @@ read_branches: - GenPart_status - GenPart_statusFlags - GenPart_genPartIdxMother + - LHE_Vpt + - LHEPart_pdgId + - LHEPart_pt diff --git a/configs/run/qcd_2016APV_v15.yaml b/configs/run/qcd_2016APV_v15.yaml new file mode 100644 index 0000000..aab002a --- /dev/null +++ b/configs/run/qcd_2016APV_v15.yaml @@ -0,0 +1,47 @@ +# QCD channel runtime card for 2016APV NanoAOD v15. +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/read_branches_v15_run2.yaml + +era: "2016APV" +nano_version: "v15" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - globalParT3_QCD + - globalParT3_Xbb + - globalParT3_Xcc + - globalParT3_Xcs + - globalParT3_Xqq + - globalParT3_massCorrGeneric + - globalParT3_massCorrX2p + - particleNetLegacy_QCD + - particleNetLegacy_Xbb + - particleNetLegacy_Xcc + - particleNetLegacy_Xqq + - particleNetLegacy_mass + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT900 diff --git a/configs/run/qcd_2016APV_v9.yaml b/configs/run/qcd_2016APV_v9.yaml new file mode 100644 index 0000000..2e87779 --- /dev/null +++ b/configs/run/qcd_2016APV_v9.yaml @@ -0,0 +1,39 @@ +# QCD channel runtime card for 2016APV NanoAOD v9. +extends: + - ../base.yaml + - ../common/nano_branches_v9.yaml + - ../common/read_branches_v9.yaml + +era: "2016APV" +nano_version: "v9" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - deepTagMD_HbbvsQCD + - deepTagMD_ZHbbvsQCD + - deepTagMD_ZHccvsQCD + - particleNetMD_QCD + - particleNetMD_Xbb + - particleNetMD_Xcc + - particleNetMD_Xqq + - particleNet_HbbvsQCD + - particleNet_HccvsQCD + - particleNet_QCD + - particleNet_mass + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT900 diff --git a/configs/run/qcd_2016_v15.yaml b/configs/run/qcd_2016_v15.yaml new file mode 100644 index 0000000..cea7f51 --- /dev/null +++ b/configs/run/qcd_2016_v15.yaml @@ -0,0 +1,47 @@ +# QCD channel runtime card for 2016 NanoAOD v15. +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/read_branches_v15_run2.yaml + +era: "2016" +nano_version: "v15" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - globalParT3_QCD + - globalParT3_Xbb + - globalParT3_Xcc + - globalParT3_Xcs + - globalParT3_Xqq + - globalParT3_massCorrGeneric + - globalParT3_massCorrX2p + - particleNetLegacy_QCD + - particleNetLegacy_Xbb + - particleNetLegacy_Xcc + - particleNetLegacy_Xqq + - particleNetLegacy_mass + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT900 diff --git a/configs/run/qcd_2016_v9.yaml b/configs/run/qcd_2016_v9.yaml new file mode 100644 index 0000000..83dad53 --- /dev/null +++ b/configs/run/qcd_2016_v9.yaml @@ -0,0 +1,39 @@ +# QCD channel runtime card for 2016 NanoAOD v9. +extends: + - ../base.yaml + - ../common/nano_branches_v9.yaml + - ../common/read_branches_v9.yaml + +era: "2016" +nano_version: "v9" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - deepTagMD_HbbvsQCD + - deepTagMD_ZHbbvsQCD + - deepTagMD_ZHccvsQCD + - particleNetMD_QCD + - particleNetMD_Xbb + - particleNetMD_Xcc + - particleNetMD_Xqq + - particleNet_HbbvsQCD + - particleNet_HccvsQCD + - particleNet_QCD + - particleNet_mass + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT900 diff --git a/configs/run/qcd_2017_v15.yaml b/configs/run/qcd_2017_v15.yaml new file mode 100644 index 0000000..a532b6b --- /dev/null +++ b/configs/run/qcd_2017_v15.yaml @@ -0,0 +1,47 @@ +# QCD channel runtime card for 2017 NanoAOD v15. +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/read_branches_v15_run2.yaml + +era: "2017" +nano_version: "v15" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - globalParT3_QCD + - globalParT3_Xbb + - globalParT3_Xcc + - globalParT3_Xcs + - globalParT3_Xqq + - globalParT3_massCorrGeneric + - globalParT3_massCorrX2p + - particleNetLegacy_QCD + - particleNetLegacy_Xbb + - particleNetLegacy_Xcc + - particleNetLegacy_Xqq + - particleNetLegacy_mass + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2017_v9.yaml b/configs/run/qcd_2017_v9.yaml new file mode 100644 index 0000000..f3242f7 --- /dev/null +++ b/configs/run/qcd_2017_v9.yaml @@ -0,0 +1,39 @@ +# QCD channel runtime card for 2017 NanoAOD v9. +extends: + - ../base.yaml + - ../common/nano_branches_v9.yaml + - ../common/read_branches_v9.yaml + +era: "2017" +nano_version: "v9" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - deepTagMD_HbbvsQCD + - deepTagMD_ZHbbvsQCD + - deepTagMD_ZHccvsQCD + - particleNetMD_QCD + - particleNetMD_Xbb + - particleNetMD_Xcc + - particleNetMD_Xqq + - particleNet_HbbvsQCD + - particleNet_HccvsQCD + - particleNet_QCD + - particleNet_mass + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2018_v15.yaml b/configs/run/qcd_2018_v15.yaml new file mode 100644 index 0000000..6bd29a7 --- /dev/null +++ b/configs/run/qcd_2018_v15.yaml @@ -0,0 +1,47 @@ +# QCD channel runtime card for 2018 NanoAOD v15. +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/read_branches_v15_run2.yaml + +era: "2018" +nano_version: "v15" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - globalParT3_QCD + - globalParT3_Xbb + - globalParT3_Xcc + - globalParT3_Xcs + - globalParT3_Xqq + - globalParT3_massCorrGeneric + - globalParT3_massCorrX2p + - particleNetLegacy_QCD + - particleNetLegacy_Xbb + - particleNetLegacy_Xcc + - particleNetLegacy_Xqq + - particleNetLegacy_mass + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2018_v9.yaml b/configs/run/qcd_2018_v9.yaml new file mode 100644 index 0000000..10f201b --- /dev/null +++ b/configs/run/qcd_2018_v9.yaml @@ -0,0 +1,39 @@ +# QCD channel runtime card for 2018 NanoAOD v9. +extends: + - ../base.yaml + - ../common/nano_branches_v9.yaml + - ../common/read_branches_v9.yaml + +era: "2018" +nano_version: "v9" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - deepTagMD_HbbvsQCD + - deepTagMD_ZHbbvsQCD + - deepTagMD_ZHccvsQCD + - particleNetMD_QCD + - particleNetMD_Xbb + - particleNetMD_Xcc + - particleNetMD_Xqq + - particleNet_HbbvsQCD + - particleNet_HccvsQCD + - particleNet_QCD + - particleNet_mass + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2022EE_v12.yaml b/configs/run/qcd_2022EE_v12.yaml new file mode 100644 index 0000000..5acf6c7 --- /dev/null +++ b/configs/run/qcd_2022EE_v12.yaml @@ -0,0 +1,35 @@ +# QCD channel runtime card for 2022EE NanoAOD v12. +extends: + - ../base.yaml + - ../common/nano_branches_v12.yaml + - ../common/read_branches_v12.yaml + +era: "2022EE" +nano_version: "v12" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2022_v12.yaml b/configs/run/qcd_2022_v12.yaml new file mode 100644 index 0000000..559f22f --- /dev/null +++ b/configs/run/qcd_2022_v12.yaml @@ -0,0 +1,35 @@ +# QCD channel runtime card for 2022 NanoAOD v12. +extends: + - ../base.yaml + - ../common/nano_branches_v12.yaml + - ../common/read_branches_v12.yaml + +era: "2022" +nano_version: "v12" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2023BPix_v12.yaml b/configs/run/qcd_2023BPix_v12.yaml new file mode 100644 index 0000000..c1e2eb1 --- /dev/null +++ b/configs/run/qcd_2023BPix_v12.yaml @@ -0,0 +1,35 @@ +# QCD channel runtime card for 2023BPix NanoAOD v12. +extends: + - ../base.yaml + - ../common/nano_branches_v12.yaml + - ../common/read_branches_v12.yaml + +era: "2023BPix" +nano_version: "v12" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2023_v12.yaml b/configs/run/qcd_2023_v12.yaml new file mode 100644 index 0000000..6d549e4 --- /dev/null +++ b/configs/run/qcd_2023_v12.yaml @@ -0,0 +1,35 @@ +# QCD channel runtime card for 2023 NanoAOD v12. +extends: + - ../base.yaml + - ../common/nano_branches_v12.yaml + - ../common/read_branches_v12.yaml + +era: "2023" +nano_version: "v12" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2024_v15.yaml b/configs/run/qcd_2024_v15.yaml new file mode 100644 index 0000000..ac55ce9 --- /dev/null +++ b/configs/run/qcd_2024_v15.yaml @@ -0,0 +1,47 @@ +# QCD channel runtime card for 2024 NanoAOD v15. +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/read_branches_v15.yaml + +era: "2024" +nano_version: "v15" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - globalParT3_QCD + - globalParT3_Xbb + - globalParT3_Xcc + - globalParT3_Xcs + - globalParT3_Xqq + - globalParT3_massCorrGeneric + - globalParT3_massCorrX2p + - particleNetLegacy_QCD + - particleNetLegacy_Xbb + - particleNetLegacy_Xcc + - particleNetLegacy_Xqq + - particleNetLegacy_mass + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/qcd_2025_v15.yaml b/configs/run/qcd_2025_v15.yaml new file mode 100644 index 0000000..f9e8cf4 --- /dev/null +++ b/configs/run/qcd_2025_v15.yaml @@ -0,0 +1,47 @@ +# QCD channel runtime card for 2025 NanoAOD v15. +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/read_branches_v15.yaml + +era: "2025" +nano_version: "v15" +preselection: nFatJet>1 + +# Use a reduced tagger list tailored to the QCD channel. +stored_tagger_names: + - globalParT3_QCD + - globalParT3_Xbb + - globalParT3_Xcc + - globalParT3_Xcs + - globalParT3_Xqq + - globalParT3_massCorrGeneric + - globalParT3_massCorrX2p + - particleNetLegacy_QCD + - particleNetLegacy_Xbb + - particleNetLegacy_Xcc + - particleNetLegacy_Xqq + - particleNetLegacy_mass + - particleNetWithMass_HbbvsQCD + - particleNetWithMass_HccvsQCD + - particleNetWithMass_QCD + - particleNet_QCD + - particleNet_XbbVsQCD + - particleNet_XccVsQCD + - particleNet_massCorr + +additional_read_branches: + - SV_pt + - SV_eta + - SV_phi + - SV_mass + - SV_ntracks + - SV_dxy + - SV_dxySig + - SV_dlen + - SV_dlenSig + - SV_chi2 + - SV_pAngle + +required_triggers: + - HLT_PFHT1050 diff --git a/configs/run/zbb_2024_v15.yaml b/configs/run/zbb_2024_v15.yaml new file mode 100644 index 0000000..89ca79a --- /dev/null +++ b/configs/run/zbb_2024_v15.yaml @@ -0,0 +1,26 @@ +# Zbb channel runtime card for 2024 v15. + +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/stored_tagger_names_v15.yaml + - ../common/read_branches_v15.yaml + +era: "2024" +nano_version: "v15" + +preselection: Sum$(abs(FatJet_eta)<2.4)>1 + +required_triggers: + - HLT_AK8PFJet380_SoftDropMass30 + +output: + include_lhe_weights: true + +additional_read_branches: + - LHEScaleWeight + - LHEPdfWeight + +channels: + zbb: + require_sv_cut: true diff --git a/configs/run/zmm_2024_v15.yaml b/configs/run/zmm_2024_v15.yaml new file mode 100644 index 0000000..b4d9f5f --- /dev/null +++ b/configs/run/zmm_2024_v15.yaml @@ -0,0 +1,23 @@ +# Zmm channel runtime card for 2024 v15. + +extends: + - ../base.yaml + - ../common/nano_branches_v15.yaml + - ../common/stored_tagger_names_v15.yaml + - ../common/read_branches_v15.yaml + +era: "2024" +nano_version: "v15" + +preselection: Sum$(Muon_pt>50 && abs(Muon_eta)<2.4 && Muon_highPtId)>0 && Sum$(Muon_pt>25 && abs(Muon_eta)<2.4 && Muon_highPtId)>1 + +required_triggers: + - HLT_Mu50 + - HLT_TkMu50 + +output: + include_lhe_weights: true + +additional_read_branches: + - LHEScaleWeight + - LHEPdfWeight diff --git a/configs/samples/qcd_2016APV_v9_DATA.yaml b/configs/samples/qcd_2016APV_v9_DATA.yaml new file mode 100644 index 0000000..7da061a --- /dev/null +++ b/configs/samples/qcd_2016APV_v9_DATA.yaml @@ -0,0 +1,7 @@ +# DATA samples +jetht: +- [/JetHT/Run2016B-ver2_HIPM_UL2016_MiniAODv2_NanoAODv9-v2/NANOAOD, +/JetHT/Run2016C-HIPM_UL2016_MiniAODv2_NanoAODv9-v2/NANOAOD, +/JetHT/Run2016D-HIPM_UL2016_MiniAODv2_NanoAODv9-v2/NANOAOD, +/JetHT/Run2016E-HIPM_UL2016_MiniAODv2_NanoAODv9-v2/NANOAOD, +/JetHT/Run2016F-HIPM_UL2016_MiniAODv2_NanoAODv9-v2/NANOAOD] diff --git a/configs/samples/qcd_2016APV_v9_MC.yaml b/configs/samples/qcd_2016APV_v9_MC.yaml new file mode 100644 index 0000000..e85ce0b --- /dev/null +++ b/configs/samples/qcd_2016APV_v9_MC.yaml @@ -0,0 +1,27 @@ +# MC samples +qcd-mg: + - /QCD_HT200to300_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /QCD_HT300to500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /QCD_HT500to700_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /QCD_HT700to1000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /QCD_HT1000to1500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /QCD_HT1500to2000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /QCD_HT2000toInf_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + +# qcd-herwig: +# - /QCD_Pt-15to7000_TuneCH3_Flat_13TeV_herwig7/RunIISummer19UL16NanoAODAPVv2-106X_mcRun2_asymptotic_preVFP_v9-v1/NANOAODSIM + +top: + - /TTToHadronic_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /ST_tW_top_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /ST_tW_antitop_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /ST_t-channel_top_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + - /ST_t-channel_antitop_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v1/NANOAODSIM + +v-qq: + - /WJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v2/NANOAODSIM + - /WJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v2/NANOAODSIM + - /WJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v2/NANOAODSIM + - /ZJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v2/NANOAODSIM + - /ZJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v2/NANOAODSIM + - /ZJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODAPVv9-106X_mcRun2_asymptotic_preVFP_v11-v2/NANOAODSIM diff --git a/configs/samples/qcd_2016_v9_DATA.yaml b/configs/samples/qcd_2016_v9_DATA.yaml new file mode 100644 index 0000000..a2fb44f --- /dev/null +++ b/configs/samples/qcd_2016_v9_DATA.yaml @@ -0,0 +1,5 @@ +# DATA samples +jetht: +- [/JetHT/Run2016F-UL2016_MiniAODv2_NanoAODv9-v1/NANOAOD, +/JetHT/Run2016G-UL2016_MiniAODv2_NanoAODv9-v1/NANOAOD, +/JetHT/Run2016H-UL2016_MiniAODv2_NanoAODv9-v1/NANOAOD] diff --git a/configs/samples/qcd_2016_v9_MC.yaml b/configs/samples/qcd_2016_v9_MC.yaml new file mode 100644 index 0000000..1fba120 --- /dev/null +++ b/configs/samples/qcd_2016_v9_MC.yaml @@ -0,0 +1,31 @@ +# MC samples +qcd-mg: + - /QCD_HT200to300_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /QCD_HT300to500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /QCD_HT500to700_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /QCD_HT700to1000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /QCD_HT1000to1500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /QCD_HT1500to2000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /QCD_HT2000toInf_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + +# qcd-herwig: +# - /QCD_Pt-15to7000_TuneCH3_Flat_13TeV_herwig7/RunIISummer19UL16NanoAODv2-106X_mcRun2_asymptotic_v15-v1/NANOAODSIM + +top: + - /TTToHadronic_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /ST_tW_top_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /ST_tW_antitop_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /ST_t-channel_top_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + - /ST_t-channel_antitop_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v1/NANOAODSIM + +v-qq: + - /WJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v2/NANOAODSIM + - /WJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v2/NANOAODSIM + - /WJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v2/NANOAODSIM + - /ZJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v2/NANOAODSIM + - /ZJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v2/NANOAODSIM + - /ZJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL16NanoAODv9-106X_mcRun2_asymptotic_v17-v2/NANOAODSIM + +## ext2 +# v-qq: +# - ZJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8 diff --git a/configs/samples/qcd_2017_v9_DATA.yaml b/configs/samples/qcd_2017_v9_DATA.yaml new file mode 100644 index 0000000..6471d77 --- /dev/null +++ b/configs/samples/qcd_2017_v9_DATA.yaml @@ -0,0 +1,7 @@ +# DATA samples +jetht: +- [/JetHT/Run2017B-UL2017_MiniAODv2_NanoAODv9-v1/NANOAOD, +/JetHT/Run2017C-UL2017_MiniAODv2_NanoAODv9-v1/NANOAOD, +/JetHT/Run2017D-UL2017_MiniAODv2_NanoAODv9-v1/NANOAOD, +/JetHT/Run2017E-UL2017_MiniAODv2_NanoAODv9-v1/NANOAOD, +/JetHT/Run2017F-UL2017_MiniAODv2_NanoAODv9-v1/NANOAOD] diff --git a/configs/samples/qcd_2017_v9_MC.yaml b/configs/samples/qcd_2017_v9_MC.yaml new file mode 100644 index 0000000..9c4d57c --- /dev/null +++ b/configs/samples/qcd_2017_v9_MC.yaml @@ -0,0 +1,28 @@ +# MC samples +qcd-mg: + - /QCD_HT200to300_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /QCD_HT300to500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /QCD_HT500to700_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /QCD_HT700to1000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /QCD_HT1000to1500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /QCD_HT1500to2000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /QCD_HT2000toInf_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + +# qcd-herwig: +# # still uses 19UL Nano +# - /QCD_Pt-15to7000_TuneCH3_Flat_13TeV_herwig7/RunIISummer19UL17NanoAODv2-106X_mc2017_realistic_v8-v1/NANOAODSIM + +top: + - /TTToHadronic_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /ST_tW_top_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /ST_tW_antitop_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /ST_t-channel_top_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + - /ST_t-channel_antitop_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v1/NANOAODSIM + +v-qq: + - /WJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v2/NANOAODSIM + - /WJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v2/NANOAODSIM + - /WJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v2/NANOAODSIM + - /ZJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v2/NANOAODSIM + - /ZJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v2/NANOAODSIM + - /ZJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL17NanoAODv9-106X_mc2017_realistic_v9-v2/NANOAODSIM diff --git a/configs/samples/qcd_2018_v9_DATA.yaml b/configs/samples/qcd_2018_v9_DATA.yaml new file mode 100644 index 0000000..50f859b --- /dev/null +++ b/configs/samples/qcd_2018_v9_DATA.yaml @@ -0,0 +1,6 @@ +# DATA samples +jetht: +- [/JetHT/Run2018A-UL2018_MiniAODv2_NanoAODv9_GT36-v1/NANOAOD, +/JetHT/Run2018B-UL2018_MiniAODv2_NanoAODv9_GT36-v1/NANOAOD, +/JetHT/Run2018C-UL2018_MiniAODv2_NanoAODv9_GT36-v1/NANOAOD, +/JetHT/Run2018D-UL2018_MiniAODv2_NanoAODv9_GT36-v1/NANOAOD] diff --git a/configs/samples/qcd_2018_v9_MC.yaml b/configs/samples/qcd_2018_v9_MC.yaml new file mode 100644 index 0000000..004a032 --- /dev/null +++ b/configs/samples/qcd_2018_v9_MC.yaml @@ -0,0 +1,28 @@ +# MC samples +qcd-mg: + - /QCD_HT200to300_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /QCD_HT300to500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /QCD_HT500to700_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /QCD_HT700to1000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM + - /QCD_HT1000to1500_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /QCD_HT1500to2000_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /QCD_HT2000toInf_TuneCP5_PSWeights_13TeV-madgraph-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + +# qcd-herwig: +# # still uses 19UL Nano +# - /QCD_Pt-15to7000_TuneCH3_Flat_13TeV_herwig7/RunIISummer19UL18NanoAODv2-106X_upgrade2018_realistic_v15_L1v1-v1/NANOAODSIM + +top: + - /TTToHadronic_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /ST_tW_top_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /ST_tW_antitop_5f_NoFullyHadronicDecays_TuneCP5_13TeV-powheg-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /ST_t-channel_top_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + - /ST_t-channel_antitop_4f_InclusiveDecays_TuneCP5_13TeV-powheg-madspin-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v1/NANOAODSIM + +v-qq: + - /WJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM + - /WJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM + - /WJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM + - /ZJetsToQQ_HT-400to600_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM + - /ZJetsToQQ_HT-600to800_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM + - /ZJetsToQQ_HT-800toInf_TuneCP5_13TeV-madgraphMLM-pythia8/RunIISummer20UL18NanoAODv9-106X_upgrade2018_realistic_v16_L1v1-v2/NANOAODSIM diff --git a/configs/samples/xsec_2024.conf b/configs/samples/xsec_2024.conf new file mode 100644 index 0000000..bd00288 --- /dev/null +++ b/configs/samples/xsec_2024.conf @@ -0,0 +1,50 @@ +# Cross sections in pb for the 2024 zbb and zmm samples. +# Values are ported from NanoHRTTools samples_nanov15/xsec_2024.conf. + +419.7 /TTto4Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM # 923.6 * 0.6741^2 + +# 13.6 TeV inclusive single-top values times BR(W->qq)=0.6741. +99.2275 /TBbarQto2Q-t-channel-4FS_TuneCP5_13p6TeV_powheg-madspin-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +59.5433 /TbarBQto2Q-t-channel-4FS_TuneCP5_13p6TeV_powheg-madspin-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +4.53534 /TBbarto2Q-s-channel_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +2.85549 /TbarBto2Q-s-channel_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +# The 79.07 pb tW total is split equally; both W bosons decay hadronically. +17.9651 /TWminusto4Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +17.9651 /TbarWplusto4Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +1961000 /QCD-4Jets_Bin-HT-200to400_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +96690 /QCD-4Jets_Bin-HT-400to600_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +13620 /QCD-4Jets_Bin-HT-600to800_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +3049 /QCD-4Jets_Bin-HT-800to1000_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +891.3 /QCD-4Jets_Bin-HT-1000to1200_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +384.6 /QCD-4Jets_Bin-HT-1200to1500_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +126.8 /QCD-4Jets_Bin-HT-1500to2000_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +26.63 /QCD-4Jets_Bin-HT-2000_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +164.5 /Wto2Q-2Jets_Bin-PTQQ-200_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +8.999 /Wto2Q-2Jets_Bin-PTQQ-400_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM +2.258 /Wto2Q-2Jets_Bin-PTQQ-600_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +70.27 /Zto2Q-2Jets_Bin-PTQQ-200_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +3.779 /Zto2Q-2Jets_Bin-PTQQ-400_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +0.5092 /Zto2Q-2Jets_Bin-PTQQ-600_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +50.79 /WWto4Q-1Jets-4FS_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +24.75 /WZto4Q-1Jets-4FS_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +7.807 /ZZto4Q-1Jets_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +0.5248 /GluGluH-Hto2B_Bin-PT-200_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +4.180 /VBFH-Hto2B_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2_ext1-v2/NANOAODSIM +0.3917 /WminusH-Wto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +0.6226 /WplusH-Wto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +0.5958 /ZH-Zto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +0.04776 /GluGluZH-Zto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +10.77 /DYto2L-2Jets_Bin-MLL-50-PTLL-200_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v1/NANOAODSIM +0.5921 /DYto2L-2Jets_Bin-MLL-50-PTLL-400_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +0.08097 /DYto2L-2Jets_Bin-MLL-50-PTLL-600_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM +98.04 /TTto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM +4.665 /TWminusto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +4.665 /TbarWplusto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +13.35 /WWto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +7.568 /WZto2L2Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +6.788 /ZZto2L2Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM diff --git a/configs/samples/zbb_2024_v15_DATA.yaml b/configs/samples/zbb_2024_v15_DATA.yaml new file mode 100644 index 0000000..e6ef766 --- /dev/null +++ b/configs/samples/zbb_2024_v15_DATA.yaml @@ -0,0 +1,18 @@ +# DATA samples for the 2024 zbb control region. +jetmet: +- [/JetMET0/Run2024C-MINIv6NANOv15-v1/NANOAOD, + /JetMET0/Run2024D-MINIv6NANOv15-v1/NANOAOD, + /JetMET0/Run2024E-MINIv6NANOv15-v1/NANOAOD, + /JetMET0/Run2024F-MINIv6NANOv15-v2/NANOAOD, + /JetMET0/Run2024G-MINIv6NANOv15-v2/NANOAOD, + /JetMET0/Run2024H-MINIv6NANOv15-v2/NANOAOD, + /JetMET0/Run2024I-MINIv6NANOv15-v2/NANOAOD, + /JetMET0/Run2024I-MINIv6NANOv15_v2-v1/NANOAOD] +- [/JetMET1/Run2024C-MINIv6NANOv15-v1/NANOAOD, + /JetMET1/Run2024D-MINIv6NANOv15-v1/NANOAOD, + /JetMET1/Run2024E-MINIv6NANOv15-v1/NANOAOD, + /JetMET1/Run2024F-MINIv6NANOv15-v2/NANOAOD, + /JetMET1/Run2024G-MINIv6NANOv15-v2/NANOAOD, + /JetMET1/Run2024H-MINIv6NANOv15-v2/NANOAOD, + /JetMET1/Run2024I-MINIv6NANOv15-v1/NANOAOD, + /JetMET1/Run2024I-MINIv6NANOv15_v2-v2/NANOAOD] diff --git a/configs/samples/zbb_2024_v15_MC.yaml b/configs/samples/zbb_2024_v15_MC.yaml new file mode 100644 index 0000000..4f4d79c --- /dev/null +++ b/configs/samples/zbb_2024_v15_MC.yaml @@ -0,0 +1,40 @@ +# MC samples for the 2024 zbb control region. +qcd-mg: +- /QCD-4Jets_Bin-HT-200to400_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-400to600_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-600to800_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-800to1000_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-1000to1200_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-1200to1500_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-1500to2000_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /QCD-4Jets_Bin-HT-2000_TuneCP5_13p6TeV_madgraphMLM-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +top: +- /TTto4Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TBbarQto2Q-t-channel-4FS_TuneCP5_13p6TeV_powheg-madspin-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TbarBQto2Q-t-channel-4FS_TuneCP5_13p6TeV_powheg-madspin-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TBbarto2Q-s-channel_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TbarBto2Q-s-channel_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TWminusto4Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TbarWplusto4Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +v-qq: +- /Wto2Q-2Jets_Bin-PTQQ-200_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /Wto2Q-2Jets_Bin-PTQQ-400_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM +- /Wto2Q-2Jets_Bin-PTQQ-600_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /Zto2Q-2Jets_Bin-PTQQ-200_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /Zto2Q-2Jets_Bin-PTQQ-400_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /Zto2Q-2Jets_Bin-PTQQ-600_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +vv-qq: +- /WWto4Q-1Jets-4FS_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /WZto4Q-1Jets-4FS_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /ZZto4Q-1Jets_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +higgs: +- /GluGluH-Hto2B_Bin-PT-200_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /VBFH-Hto2B_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2_ext1-v2/NANOAODSIM +- /WminusH-Wto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /WplusH-Wto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /ZH-Zto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powhegMINLO-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /GluGluZH-Zto2Q-Hto2B_Par-M-125_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM diff --git a/configs/samples/zmm_2024_v15_DATA.yaml b/configs/samples/zmm_2024_v15_DATA.yaml new file mode 100644 index 0000000..35be624 --- /dev/null +++ b/configs/samples/zmm_2024_v15_DATA.yaml @@ -0,0 +1,18 @@ +# DATA samples for the 2024 zmm control region. +muon: +- [/Muon0/Run2024C-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024D-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024E-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024F-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024G-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024H-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024I-MINIv6NANOv15-v1/NANOAOD, + /Muon0/Run2024I-MINIv6NANOv15_v2-v1/NANOAOD] +- [/Muon1/Run2024C-MINIv6NANOv15-v1/NANOAOD, + /Muon1/Run2024D-MINIv6NANOv15-v1/NANOAOD, + /Muon1/Run2024E-MINIv6NANOv15-v1/NANOAOD, + /Muon1/Run2024F-MINIv6NANOv15-v1/NANOAOD, + /Muon1/Run2024G-MINIv6NANOv15-v2/NANOAOD, + /Muon1/Run2024H-MINIv6NANOv15-v2/NANOAOD, + /Muon1/Run2024I-MINIv6NANOv15-v1/NANOAOD, + /Muon1/Run2024I-MINIv6NANOv15_v2-v1/NANOAOD] diff --git a/configs/samples/zmm_2024_v15_MC.yaml b/configs/samples/zmm_2024_v15_MC.yaml new file mode 100644 index 0000000..3f3bfaf --- /dev/null +++ b/configs/samples/zmm_2024_v15_MC.yaml @@ -0,0 +1,15 @@ +# MC samples for the 2024 zmm control region. +DY: +- /DYto2L-2Jets_Bin-MLL-50-PTLL-200_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v1/NANOAODSIM +- /DYto2L-2Jets_Bin-MLL-50-PTLL-400_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /DYto2L-2Jets_Bin-MLL-50-PTLL-600_TuneCP5_13p6TeV_amcatnloFXFX-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM + +top: +- /TTto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v3/NANOAODSIM +- /TWminusto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /TbarWplusto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM + +vv: +- /WWto2L2Nu_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /WZto2L2Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM +- /ZZto2L2Q_TuneCP5_13p6TeV_powheg-pythia8/RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2/NANOAODSIM diff --git a/data/muon-smearing/stdflat.json.gz b/data/muon-smearing/stdflat.json.gz new file mode 100644 index 0000000..38126c1 Binary files /dev/null and b/data/muon-smearing/stdflat.json.gz differ diff --git a/data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.json.gz b/data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.json.gz new file mode 100644 index 0000000..a52e5b2 Binary files /dev/null and b/data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.json.gz differ diff --git a/data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.root b/data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.root new file mode 100644 index 0000000..f681f61 Binary files /dev/null and b/data/vjets-ewk/2023-08-11/WJetsCorr_collection_ewk.root differ diff --git a/data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.json.gz b/data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.json.gz new file mode 100644 index 0000000..e4fbd31 Binary files /dev/null and b/data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.json.gz differ diff --git a/data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.root b/data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.root new file mode 100644 index 0000000..1010d48 Binary files /dev/null and b/data/vjets-ewk/2023-08-11/ZJetsCorr_collection_ewk.root differ diff --git a/data/vjets-ewk/convert_root_to_json.py b/data/vjets-ewk/convert_root_to_json.py new file mode 100644 index 0000000..b97fb68 --- /dev/null +++ b/data/vjets-ewk/convert_root_to_json.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import gzip +import json +from pathlib import Path + +import ROOT + + +PAYLOADS = ( + ("WJetsCorr_collection_ewk.root", "WJetsCorr_collection_ewk.json.gz", "evj_pTV_EW", "evj"), + ("ZJetsCorr_collection_ewk.root", "ZJetsCorr_collection_ewk.json.gz", "eej_pTV_EW", "eej"), +) + + +def binning(histogram): + axis = histogram.GetXaxis() + return { + "nodetype": "binning", + "input": "pt", + "edges": [axis.GetBinLowEdge(index) for index in range(1, histogram.GetNbinsX() + 2)], + "content": [histogram.GetBinContent(index) for index in range(1, histogram.GetNbinsX() + 1)], + "flow": 0.0, + } + + +def convert(directory, root_name, json_name, correction_name, prefix): + source = ROOT.TFile.Open(str(directory / root_name), "READ") + if not source or source.IsZombie(): + raise RuntimeError(f"Cannot open {root_name}") + + names = { + "nominal": f"{prefix}_pTV_kappa_EW", + "d1": f"{prefix}_pTV_d1kappa_EW", + "d2": f"{prefix}_pTV_d2kappa_EW", + "d3": f"{prefix}_pTV_d3kappa_EW", + } + histograms = {key: source.Get(name) for key, name in names.items()} + if any(not histogram for histogram in histograms.values()): + raise RuntimeError(f"Missing EWK histogram in {root_name}") + + reference_edges = binning(histograms["nominal"])["edges"] + if any(binning(histogram)["edges"] != reference_edges for histogram in histograms.values()): + raise RuntimeError(f"Inconsistent binning in {root_name}") + + payload = { + "schema_version": 2, + "description": f"NLO electroweak correction converted from {root_name}", + "corrections": [ + { + "name": correction_name, + "description": "Boson pT correction and uncertainty components", + "version": 1, + "inputs": [ + {"name": "pt", "type": "real", "description": "Generator boson transverse momentum"}, + {"name": "variation", "type": "string", "description": "nominal, d1, d2, or d3"}, + ], + "output": {"name": "value", "type": "real"}, + "data": { + "nodetype": "category", + "input": "variation", + "content": [ + {"key": key, "value": binning(histogram)} for key, histogram in histograms.items() + ], + }, + } + ], + } + encoded = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() + with gzip.GzipFile(filename=str(directory / json_name), mode="wb", mtime=0) as output: + output.write(encoded) + + +def main(): + directory = Path(__file__).resolve().parent / "2023-08-11" + for arguments in PAYLOADS: + convert(directory, *arguments) + + +if __name__ == "__main__": + main() diff --git a/docs/framework-structure.md b/docs/framework-structure.md index 7361d27..101a157 100644 --- a/docs/framework-structure.md +++ b/docs/framework-structure.md @@ -35,6 +35,7 @@ - `src/producers/HeavyFlavMuonSampleProducer.cpp`: muon-channel event selection and channel-specific output branches. - `src/producers/HeavyFlavMinimalProducer.cpp`: minimal-channel leading-cleaned-AK8 selection and fatjet-only output. +- `src/producers/HeavyFlavQCDSampleProducer.cpp`: QCD dijet selection, secondary-vertex matching, two-fatjet output, and optional channel-owned SV detail filling through `channels.qcd.fill_sv`. - `src/producers/HeavyFlavBaseProducer.cpp`: shared lepton selection, jet/MET correction hookup, output branch filling, and fatjet-level shared content. ## Python reference diff --git a/external/CMSJMECalculators b/external/CMSJMECalculators index f31313d..a6075cb 160000 --- a/external/CMSJMECalculators +++ b/external/CMSJMECalculators @@ -1 +1 @@ -Subproject commit f31313d87bc917b0e6a2114b08a83a408bf8608c +Subproject commit a6075cbb8022120084e7ee03e63ff20a6e3c3fd1 diff --git a/include/nano/helpers/JmeVariation.h b/include/nano/helpers/JmeVariation.h index e8c00e9..dc881a8 100644 --- a/include/nano/helpers/JmeVariation.h +++ b/include/nano/helpers/JmeVariation.h @@ -14,10 +14,15 @@ enum class JmeVariation { JerDown, MetUp, MetDown, + MuonScaleUp, + MuonScaleDown, + MuonSmearUp, + MuonSmearDown, }; std::string_view variation_name(JmeVariation variation); JmeVariation parse_jme_variation(std::string_view name); std::vector parse_jme_variation_list(std::string_view text); +bool is_muon_variation(JmeVariation variation); } // namespace nano diff --git a/include/nano/helpers/MuonCorrection.h b/include/nano/helpers/MuonCorrection.h new file mode 100644 index 0000000..718e140 --- /dev/null +++ b/include/nano/helpers/MuonCorrection.h @@ -0,0 +1,43 @@ +#pragma once + +#include "nano/core/Collection.h" +#include "nano/helpers/JmeVariation.h" +#include "nano/producers/HeavyFlavBaseProducer.h" + +#include "MuonVariationsCalculator.h" + +#include +#include +#include + +namespace correction { +class CorrectionSet; +} + +namespace nano { + +struct MuonSFResult { + float nominal = 1.0f; + float stat_up = 1.0f; + float stat_down = 1.0f; + float syst_up = 1.0f; + float syst_down = 1.0f; +}; + +class MuonCorrection { +public: + explicit MuonCorrection(const ProducerConfig &config); + + MuonVariationsCalculator::result_t produce(Event &event, const std::vector &muons) const; + void apply(const MuonVariationsCalculator::result_t &result, JmeVariation variation, + std::vector &muons) const; + MuonSFResult scale_factor(const std::vector &muons, const std::string &correction_key, + float minimum_pt) const; + +private: + MuonVariationsCalculator mc_calculator_; + MuonVariationsCalculator data_calculator_; + std::shared_ptr scale_factors_; +}; + +} // namespace nano diff --git a/include/nano/helpers/NloEWWeightProducer.h b/include/nano/helpers/NloEWWeightProducer.h new file mode 100644 index 0000000..9344075 --- /dev/null +++ b/include/nano/helpers/NloEWWeightProducer.h @@ -0,0 +1,32 @@ +#pragma once + +#include "nano/core/Event.h" +#include "nano/core/OutputModel.h" +#include "nano/producers/HeavyFlavBaseProducer.h" + +#include +#include + +namespace correction { +class CorrectionSet; +} + +namespace nano { + +class NloEWWeightProducer { +public: + explicit NloEWWeightProducer(const ProducerConfig &config); + ~NloEWWeightProducer(); + + void begin_file(OutputModel &out) const; + void fill(Event &event, OutputModel &out) const; + +private: + enum class Boson { None, W, Z }; + + Boson boson_ = Boson::None; + std::shared_ptr corrections_; + std::string correction_key_; +}; + +} // namespace nano diff --git a/include/nano/io/RootOutputFile.h b/include/nano/io/RootOutputFile.h index 4a23f2f..163405a 100644 --- a/include/nano/io/RootOutputFile.h +++ b/include/nano/io/RootOutputFile.h @@ -24,6 +24,7 @@ class RootOutputFile { ~RootOutputFile(); void book_events(const OutputModel &model, std::string_view tree_name = "Events"); + void set_branch_title(std::string_view branch_name, std::string_view title); void fill_event(const OutputModel &model); void write(); diff --git a/include/nano/producers/HeavyFlavBaseProducer.h b/include/nano/producers/HeavyFlavBaseProducer.h index a9f57b7..9b0a41b 100644 --- a/include/nano/producers/HeavyFlavBaseProducer.h +++ b/include/nano/producers/HeavyFlavBaseProducer.h @@ -6,6 +6,7 @@ #include "nano/helpers/JmeEventResult.h" #include "nano/helpers/JmeVariation.h" +#include #include #include #include @@ -15,6 +16,7 @@ namespace nano { class JetMETCorrector; +class NloEWWeightProducer; class PuWeightProducer; class TopPtWeightProducer; class FatjetGenMatching; @@ -44,11 +46,26 @@ struct PuEraConfig { std::string correction_key; }; +struct MuonEraConfig { + std::string payload_subdir; + std::string version = "latest"; + std::string scale_smearing_file = "muon_scalesmearing.json.gz"; + std::string sf_file = "muon_Z.json.gz"; +}; + struct JetVetoMapEraConfig { std::string payload_subdir; std::string correction_key; }; +struct NloEWConfig { + std::string payload_dir; + std::string w_file; + std::string z_file; + std::string w_correction; + std::string z_correction; +}; + struct BTagConfig { std::string branch; float loose = 0.0f; @@ -86,6 +103,11 @@ struct ProducerConfig { std::unordered_map jme_eras; std::string pu_payload_dir; std::unordered_map pu_eras; + std::string muon_payload_dir; + std::string muon_smearing_file; + std::string muon_smearing_tool = "stdflat"; + std::unordered_map muon_eras; + NloEWConfig nlo_ew; bool jet_veto_map_enabled = false; std::string jet_veto_map_payload_dir; std::string jet_veto_map_type = "jetvetomap"; @@ -109,12 +131,15 @@ class HeavyFlavBaseProducer { static std::vector default_schema(const ProducerConfig &config); protected: + virtual std::size_t output_fatjet_count() const { return 1U; } void prepare_common_objects(Event &event) const; void select_leptons(Event &event) const; void apply_jme_and_select_jets(Event &event, const JmeEventResult &jme_result, JmeVariation variation) const; void load_gen_history(Event &event, std::vector &fatjets) const; + float get_lhe_v_pt(Event &event) const; + float get_gen_v_pt(Event &event) const; void fill_base_event_info(Event &event, JmeVariation variation); - void fill_fatjet_info(Event &event, const std::vector &fatjets); + void fill_fatjet_info(Event &event, const std::vector &fatjets, std::size_t fatjet_index); ProducerConfig config_; float jet_cone_size_ = 0.8f; @@ -122,6 +147,7 @@ class HeavyFlavBaseProducer { std::string subjet_name_ = "SubJet"; std::string genfatjet_name_ = "GenJetAK8"; std::unique_ptr jme_corrector_; + std::unique_ptr nlo_ew_weight_producer_; std::unique_ptr pu_weight_producer_; std::unique_ptr top_pt_weight_producer_; std::unique_ptr fatjet_gen_matching_; diff --git a/include/nano/producers/HeavyFlavQCDSampleProducer.h b/include/nano/producers/HeavyFlavQCDSampleProducer.h new file mode 100644 index 0000000..3d1ceb2 --- /dev/null +++ b/include/nano/producers/HeavyFlavQCDSampleProducer.h @@ -0,0 +1,33 @@ +#pragma once + +#include "nano/producers/HeavyFlavBaseProducer.h" + +namespace nano { + +class HeavyFlavQCDSampleProducer : public HeavyFlavBaseProducer { +public: + explicit HeavyFlavQCDSampleProducer(ProducerConfig config); + + void begin_file() override; + bool analyze(Event &event) override; + bool analyze_common(Event &event) override; + bool analyze_variation(Event &event, const JmeEventResult &jme_result, + JmeVariation variation) override; + +protected: + std::size_t output_fatjet_count() const override { return 2U; } + +private: + void select_secondary_vertices(Event &event) const; + void match_secondary_vertices( + std::vector &fatjets, + const std::vector &secondary_vertices) const; + void define_fatjet_sv_branches(std::size_t fatjet_index); + void fill_fatjet_sv_info(const std::vector &fatjets, + std::size_t fatjet_index); + + bool apply_sv_criteria_ = true; + bool fill_sv_ = true; +}; + +} // namespace nano diff --git a/include/nano/producers/HeavyFlavZbbSampleProducer.h b/include/nano/producers/HeavyFlavZbbSampleProducer.h new file mode 100644 index 0000000..c103994 --- /dev/null +++ b/include/nano/producers/HeavyFlavZbbSampleProducer.h @@ -0,0 +1,23 @@ +#pragma once + +#include "nano/producers/HeavyFlavBaseProducer.h" + +namespace nano { + +class HeavyFlavZbbSampleProducer : public HeavyFlavBaseProducer { +public: + explicit HeavyFlavZbbSampleProducer(ProducerConfig config); + + void begin_file() override; + bool analyze(Event &event) override; + bool analyze_common(Event &event) override; + bool analyze_variation(Event &event, const JmeEventResult &jme_result, JmeVariation variation) override; + +protected: + std::size_t output_fatjet_count() const override { return 2U; } + +private: + bool require_sv_cut_ = true; +}; + +} // namespace nano diff --git a/include/nano/producers/HeavyFlavZmmSampleProducer.h b/include/nano/producers/HeavyFlavZmmSampleProducer.h new file mode 100644 index 0000000..5dc8ddd --- /dev/null +++ b/include/nano/producers/HeavyFlavZmmSampleProducer.h @@ -0,0 +1,22 @@ +#pragma once + +#include "nano/helpers/MuonCorrection.h" +#include "nano/producers/HeavyFlavBaseProducer.h" + +namespace nano { + +class HeavyFlavZmmSampleProducer : public HeavyFlavBaseProducer { +public: + explicit HeavyFlavZmmSampleProducer(ProducerConfig config); + + void begin_file() override; + bool analyze(Event &event) override; + bool analyze_common(Event &event) override; + bool analyze_variation(Event &event, const JmeEventResult &jme_result, JmeVariation variation) override; + +private: + bool select_muons(Event &event, JmeVariation variation); + MuonCorrection muon_correction_; +}; + +} // namespace nano diff --git a/src/helpers/JetMETCorrector.cpp b/src/helpers/JetMETCorrector.cpp index dec1669..34cd06d 100644 --- a/src/helpers/JetMETCorrector.cpp +++ b/src/helpers/JetMETCorrector.cpp @@ -508,6 +508,10 @@ std::size_t JetMETCorrector::variation_index(const std::vector &ava allow_missing = true; break; case JmeVariation::Nominal: + case JmeVariation::MuonScaleUp: + case JmeVariation::MuonScaleDown: + case JmeVariation::MuonSmearUp: + case JmeVariation::MuonSmearDown: index = find_label("nominal"); break; } diff --git a/src/helpers/JmeVariation.cpp b/src/helpers/JmeVariation.cpp index 550393f..7d38b6a 100644 --- a/src/helpers/JmeVariation.cpp +++ b/src/helpers/JmeVariation.cpp @@ -21,6 +21,14 @@ std::string_view variation_name(JmeVariation variation) { return "met_up"; case JmeVariation::MetDown: return "met_down"; + case JmeVariation::MuonScaleUp: + return "muon_scale_up"; + case JmeVariation::MuonScaleDown: + return "muon_scale_down"; + case JmeVariation::MuonSmearUp: + return "muon_smear_up"; + case JmeVariation::MuonSmearDown: + return "muon_smear_down"; } return "nominal"; } @@ -47,7 +55,19 @@ JmeVariation parse_jme_variation(std::string_view name) { if (name == "met_down") { return JmeVariation::MetDown; } - throw std::runtime_error("Unsupported JME variation: " + std::string(name)); + if (name == "muon_scale_up") { + return JmeVariation::MuonScaleUp; + } + if (name == "muon_scale_down") { + return JmeVariation::MuonScaleDown; + } + if (name == "muon_smear_up") { + return JmeVariation::MuonSmearUp; + } + if (name == "muon_smear_down") { + return JmeVariation::MuonSmearDown; + } + throw std::runtime_error("Unsupported variation: " + std::string(name)); } std::vector parse_jme_variation_list(std::string_view text) { @@ -62,4 +82,9 @@ std::vector parse_jme_variation_list(std::string_view text) { return out; } +bool is_muon_variation(JmeVariation variation) { + return variation == JmeVariation::MuonScaleUp || variation == JmeVariation::MuonScaleDown || + variation == JmeVariation::MuonSmearUp || variation == JmeVariation::MuonSmearDown; +} + } // namespace nano diff --git a/src/helpers/MuonCorrection.cpp b/src/helpers/MuonCorrection.cpp new file mode 100644 index 0000000..ec15ec4 --- /dev/null +++ b/src/helpers/MuonCorrection.cpp @@ -0,0 +1,141 @@ +#include "nano/helpers/MuonCorrection.h" + +#include + +#include +#include +#include +#include + +namespace nano { + +namespace { + +std::shared_ptr +correction_set(const std::string &path) { + static std::unordered_map> + cache; + auto &entry = cache[path]; + if (!entry) { + entry = std::shared_ptr( + correction::CorrectionSet::from_file(path).release()); + } + return entry; +} + +float evaluate_sf(const correction::Correction::Ref &corr, + const ObjectView &muon, const std::string &variation, + float minimum_pt) { + if (muon.eta() < -2.4f || muon.eta() >= 2.4f || muon.pt() < minimum_pt) { + return 1.0f; + } + return static_cast( + corr->evaluate({static_cast(muon.eta()), + static_cast(muon.pt()), variation})); +} + +std::size_t muon_variation_index(JmeVariation variation) { + switch (variation) { + case JmeVariation::MuonScaleUp: + return 1U; + case JmeVariation::MuonScaleDown: + return 2U; + case JmeVariation::MuonSmearUp: + return 3U; + case JmeVariation::MuonSmearDown: + return 4U; + default: + return 0U; + } +} + +} // namespace + +MuonCorrection::MuonCorrection(const ProducerConfig &config) { + const auto key = config.era + "_NanoAOD" + config.nano_version; + const auto it = config.muon_eras.find(key); + if (it == config.muon_eras.end()) { + throw std::runtime_error("Missing muon correction campaign: " + key); + } + const auto root = config.muon_payload_dir + "/" + it->second.payload_subdir + + "/" + it->second.version; + const auto scale_smearing_file = root + "/" + it->second.scale_smearing_file; + mc_calculator_ = MuonVariationsCalculator::create( + scale_smearing_file, true, true, config.muon_smearing_file, + config.muon_smearing_tool); + data_calculator_ = MuonVariationsCalculator::create(scale_smearing_file, + false, false, "", ""); + scale_factors_ = correction_set(root + "/" + it->second.sf_file); +} + +MuonVariationsCalculator::result_t +MuonCorrection::produce(Event &event, + const std::vector &muons) const { + MuonVariationsCalculator::p4compv_t pt; + MuonVariationsCalculator::p4compv_t eta; + MuonVariationsCalculator::p4compv_t phi; + MuonVariationsCalculator::p4compv_int charge; + MuonVariationsCalculator::p4compv_int layers; + pt.reserve(muons.size()); + eta.reserve(muons.size()); + phi.reserve(muons.size()); + charge.reserve(muons.size()); + layers.reserve(muons.size()); + for (const auto &muon : muons) { + pt.push_back(muon.pt()); + eta.push_back(muon.eta()); + phi.push_back(muon.phi()); + charge.push_back(muon.get("charge")); + layers.push_back(muon.get("nTrackerLayers")); + } + + const auto seed = + static_cast(event.scalar("event") & 0x7fffffffULL); + return event.is_mc() + ? mc_calculator_.produce(pt, eta, phi, charge, layers, seed) + : data_calculator_.produce(pt, eta, phi, charge, layers, seed); +} + +void MuonCorrection::apply(const MuonVariationsCalculator::result_t &result, + JmeVariation variation, + std::vector &muons) const { + const auto index = muon_variation_index(variation); + if (index >= result.size()) { + throw std::runtime_error("Requested muon variation is unavailable"); + } + const auto &corrected_pt = result.pt(index); + if (corrected_pt.size() != muons.size()) { + throw std::runtime_error("Muon correction output size mismatch"); + } + for (std::size_t i = 0; i < muons.size(); ++i) { + muons[i].set("pt", corrected_pt[i]); + muons[i].set("p4", + ObjectView::LorentzVector(corrected_pt[i], muons[i].eta(), + muons[i].phi(), muons[i].mass())); + } +} + +MuonSFResult MuonCorrection::scale_factor(const std::vector &muons, + const std::string &correction_key, + float minimum_pt) const { + const auto corr = scale_factors_->at(correction_key); + float nominal = 1.0f; + float stat_up = 1.0f; + float stat_down = 1.0f; + float syst_up = 1.0f; + float syst_down = 1.0f; + for (const auto &muon : muons) { + const auto nominal_value = evaluate_sf(corr, muon, "nominal", minimum_pt); + const auto stat_value = evaluate_sf(corr, muon, "stat", minimum_pt); + const auto stat_delta = std::abs(stat_value); + nominal *= nominal_value; + stat_up *= nominal_value + stat_delta; + stat_down *= nominal_value - stat_delta; + syst_up *= evaluate_sf(corr, muon, "systup", minimum_pt); + syst_down *= evaluate_sf(corr, muon, "systdown", minimum_pt); + } + return {nominal, stat_up, stat_down, syst_up, syst_down}; +} + +} // namespace nano diff --git a/src/helpers/NloEWWeightProducer.cpp b/src/helpers/NloEWWeightProducer.cpp new file mode 100644 index 0000000..040051c --- /dev/null +++ b/src/helpers/NloEWWeightProducer.cpp @@ -0,0 +1,96 @@ +#include "nano/helpers/NloEWWeightProducer.h" + +#include "nano/core/Collection.h" + +#include + +#include +#include +#include +#include + +namespace nano { + +namespace { + +bool starts_with(const std::string &value, const std::string &prefix) { + return value.rfind(prefix, 0) == 0; +} + +std::shared_ptr +load_corrections(const std::string &path) { + return std::shared_ptr( + correction::CorrectionSet::from_file(path).release()); +} + +} // namespace + +NloEWWeightProducer::NloEWWeightProducer(const ProducerConfig &config) { + const auto sample_it = config.channel_options.strings.find("sample_name"); + const auto sample = sample_it == config.channel_options.strings.end() + ? std::string{} + : sample_it->second; + if (starts_with(sample, "Wto") || starts_with(sample, "WJetsTo")) { + boson_ = Boson::W; + corrections_ = load_corrections(config.nlo_ew.payload_dir + "/" + + config.nlo_ew.w_file); + correction_key_ = config.nlo_ew.w_correction; + } else if (starts_with(sample, "Zto") || starts_with(sample, "ZJetsTo") || + starts_with(sample, "DYto") || starts_with(sample, "DYJetsTo")) { + boson_ = Boson::Z; + corrections_ = load_corrections(config.nlo_ew.payload_dir + "/" + + config.nlo_ew.z_file); + correction_key_ = config.nlo_ew.z_correction; + } +} + +NloEWWeightProducer::~NloEWWeightProducer() = default; + +void NloEWWeightProducer::begin_file(OutputModel &out) const { + out.branch("nlo_ew_weight", 1.0f); + out.branch("nlo_ew_weight_up", 1.0f); + out.branch("nlo_ew_weight_down", 1.0f); +} + +void NloEWWeightProducer::fill(Event &event, OutputModel &out) const { + if (!event.is_mc() || boson_ == Boson::None) { + return; + } + + float fallback_pt = -1.0f; + float last_copy_pt = -1.0f; + for (const auto &particle : event.collection("GenPart").objects()) { + const auto pdg_id = particle.get("pdgId"); + if ((boson_ == Boson::W && std::abs(pdg_id) != 24) || + (boson_ == Boson::Z && pdg_id != 23)) { + continue; + } + fallback_pt = std::max(fallback_pt, particle.pt()); + if ((particle.get("statusFlags") & (1 << 13)) != 0) { + last_copy_pt = std::max(last_copy_pt, particle.pt()); + } + } + + const auto pt = last_copy_pt >= 0.0f ? last_copy_pt : fallback_pt; + if (pt < 100.0f) { + return; + } + + // Preserve the legacy ROOT lookup, where x == xmax belongs to the final bin. + constexpr float maximum_pt = 6500.0f; + const auto lookup_pt = pt == maximum_pt + ? std::nextafter(static_cast(pt), 0.0) + : static_cast(pt); + const auto correction = corrections_->at(correction_key_); + const auto evaluate = [&](const char *variation) { + return static_cast( + correction->evaluate({lookup_pt, std::string(variation)})); + }; + const auto nominal = 1.0f + evaluate("nominal"); + const auto uncertainty = 0.5f * std::abs(nominal - 1.0f); + out.fill("nlo_ew_weight", nominal); + out.fill("nlo_ew_weight_up", nominal + uncertainty); + out.fill("nlo_ew_weight_down", nominal - uncertainty); +} + +} // namespace nano diff --git a/src/io/RootOutputFile.cpp b/src/io/RootOutputFile.cpp index bda4e39..30f0f0d 100644 --- a/src/io/RootOutputFile.cpp +++ b/src/io/RootOutputFile.cpp @@ -49,6 +49,17 @@ void RootOutputFile::book_events(const OutputModel &model, std::string_view tree } } +void RootOutputFile::set_branch_title(std::string_view branch_name, std::string_view title) { + if (!events_tree_) { + throw std::runtime_error("Events tree has not been booked"); + } + auto *branch = events_tree_->GetBranch(std::string(branch_name).c_str()); + if (!branch) { + throw std::runtime_error("Cannot set title for missing output branch: " + std::string(branch_name)); + } + branch->SetTitle(std::string(title).c_str()); +} + void RootOutputFile::fill_event(const OutputModel &model) { if (!events_tree_) { throw std::runtime_error("Events tree has not been booked"); diff --git a/src/producers/HeavyFlavBaseProducer.cpp b/src/producers/HeavyFlavBaseProducer.cpp index 67d62a9..56f201d 100644 --- a/src/producers/HeavyFlavBaseProducer.cpp +++ b/src/producers/HeavyFlavBaseProducer.cpp @@ -4,11 +4,11 @@ #include "nano/core/Helpers.h" #include "nano/helpers/FatjetGenMatching.h" #include "nano/helpers/JetMETCorrector.h" +#include "nano/helpers/NloEWWeightProducer.h" #include "nano/helpers/PuWeightProducer.h" #include "nano/helpers/TopPtWeightProducer.h" #include -#include #include #include #include @@ -60,6 +60,10 @@ LorentzVector p4_from_raw_values(const ObjectView &obj) { return ObjectView::LorentzVector(raw_pt, obj.eta(), obj.phi(), raw_mass); } +std::string fatjet_branch_name(std::size_t fatjet_index, std::string_view variable) { + return "fj_" + std::to_string(fatjet_index + 1U) + "_" + std::string(variable); +} + void prepare_raw_kinematics(Event &event, std::string_view object_name) { auto objects = event.collection(object_name).objects(); for (auto &obj : objects) { @@ -75,6 +79,7 @@ void prepare_raw_kinematics(Event &event, std::string_view object_name) { HeavyFlavBaseProducer::HeavyFlavBaseProducer(ProducerConfig config) : config_(std::move(config)) { jme_corrector_ = std::make_unique(config_); + nlo_ew_weight_producer_ = std::make_unique(config_); pu_weight_producer_ = std::make_unique(config_); top_pt_weight_producer_ = std::make_unique(config_.era); fatjet_gen_matching_ = std::make_unique(); @@ -108,37 +113,43 @@ void HeavyFlavBaseProducer::begin_file() { out_.branch("genWeight", 1.0f); if (config_.include_lhe_weights) { out_.branch("LHEScaleWeight", std::vector{}); + out_.branch("LHEPdfWeight", std::vector{}); } pu_weight_producer_->begin_file(out_); + nlo_ew_weight_producer_->begin_file(out_); top_pt_weight_producer_->begin_file(out_); - out_.branch("fj_1_is_qualified", false); - for (const auto *name : {"fj_1_pt", "fj_1_eta", "fj_1_phi", "fj_1_mass", "fj_1_rawpt", "fj_1_sdmass", - "fj_1_sdmass_uncorrected", - "fj_1_tau1", "fj_1_tau2", "fj_1_tau3", - "fj_1_tau4", "fj_1_deltaR_sj12", "fj_1_sj1_pt", "fj_1_sj1_eta", "fj_1_sj1_phi", - "fj_1_sj1_mass", "fj_1_sj1_rawpt", "fj_1_sj1_btagdeepcsv", "fj_1_sj2_pt", "fj_1_sj2_eta", - "fj_1_sj2_phi", "fj_1_sj2_mass", "fj_1_sj2_rawpt", "fj_1_sj2_btagdeepcsv"}) { - out_.branch(name, 0.0f); - } - for (const auto &tagger : config_.tagger_names) { - out_.branch("fj_1_" + tagger, -99.0f); - } + constexpr std::string_view fatjet_float_variables[]{ + "pt", "eta", "phi", "mass", "rawpt", "gptmass_x2p", "gptmass_generic", "sdmass", + "sdmass_uncorrected", + "tau1", "tau2", "tau3", "tau4", "deltaR_sj12", "sj1_pt", "sj1_eta", "sj1_phi", + "sj1_mass", "sj1_rawpt", "sj1_btagdeepcsv", "sj2_pt", "sj2_eta", "sj2_phi", "sj2_mass", + "sj2_rawpt", "sj2_btagdeepcsv", "dr_H", "dr_H_daus", "H_pt", "dr_Z", "dr_Z_daus", "Z_pt", + "dr_W", "dr_W_daus", "W_pt", "dr_T", "dr_T_b", "dr_T_Wq_max", "dr_T_Wq_min", "T_pt", + }; + constexpr std::string_view fatjet_int_variables[]{ + "genfj_nbhadrons", "genfj_nchadrons", "genfj_partonflavour", "nbhadrons", "nchadrons", "partonflavour", + "sj1_nbhadrons", "sj1_nchadrons", "sj1_partonflavour", "sj2_nbhadrons", "sj2_nchadrons", + "sj2_partonflavour", "H_decay", "Z_decay", "W_decay", "T_Wq_max_pdgId", "T_Wq_min_pdgId", + }; - for (const auto *name : {"fj_1_genfj_nbhadrons", "fj_1_genfj_nchadrons", "fj_1_genfj_partonflavour", - "fj_1_nbhadrons", "fj_1_nchadrons", "fj_1_partonflavour", - "fj_1_sj1_nbhadrons", "fj_1_sj1_nchadrons", "fj_1_sj1_partonflavour", - "fj_1_sj2_nbhadrons", "fj_1_sj2_nchadrons", "fj_1_sj2_partonflavour", - "fj_1_H_decay", "fj_1_Z_decay", "fj_1_W_decay", - "fj_1_T_Wq_max_pdgId", "fj_1_T_Wq_min_pdgId"}) { - out_.branch(name, std::int32_t{0}); + const auto num_output_fatjets = output_fatjet_count(); + if (num_output_fatjets == 0U) { + throw std::logic_error("HeavyFlavBaseProducer requires at least one output fatjet slot"); } - - for (const auto *name : {"fj_1_dr_H", "fj_1_dr_H_daus", "fj_1_H_pt", "fj_1_dr_Z", "fj_1_dr_Z_daus", - "fj_1_Z_pt", "fj_1_dr_W", "fj_1_dr_W_daus", "fj_1_W_pt", "fj_1_dr_T", - "fj_1_dr_T_b", "fj_1_dr_T_Wq_max","fj_1_dr_T_Wq_min", "fj_1_T_pt"}) { - out_.branch(name, 0.0f); + for (std::size_t fatjet_index = 0; fatjet_index < num_output_fatjets; ++fatjet_index) { + out_.branch(fatjet_branch_name(fatjet_index, "is_qualified"), false); + for (const auto variable : fatjet_float_variables) { + out_.branch(fatjet_branch_name(fatjet_index, variable), 0.0f); + } + for (const auto &tagger : config_.tagger_names) { + out_.branch(fatjet_branch_name(fatjet_index, tagger), -99.0f); + } + for (const auto variable : fatjet_int_variables) { + out_.branch(fatjet_branch_name(fatjet_index, variable), std::int32_t{0}); + } } + } // Build the explicit input schema from the runtime card. nano_run has already @@ -271,6 +282,57 @@ void HeavyFlavBaseProducer::load_gen_history(Event &event, std::vectorprocess(event, fatjets); } +float HeavyFlavBaseProducer::get_lhe_v_pt(Event &event) const { + if (!event.is_mc()) { + return -1.0f; + } + + const auto sample_it = config_.channel_options.strings.find("sample_name"); + const auto sample = sample_it == config_.channel_options.strings.end() ? std::string{} : sample_it->second; + const auto starts_with = [&](const std::string &prefix) { return sample.rfind(prefix, 0) == 0; }; + const auto is_dy = starts_with("DYto") || starts_with("DYJetsTo"); + const auto is_w = starts_with("Wto") || starts_with("WJetsTo"); + const auto is_z = starts_with("Zto") || starts_with("ZJetsTo"); + if (!is_dy && !is_w && !is_z) { + return -1.0f; + } + if (is_dy && event.has_physical_branch("LHE_Vpt")) { + return event.scalar("LHE_Vpt"); + } + + const auto target_pdg_id = is_w ? 24 : 23; + float lhe_v_pt = -1.0f; + for (const auto &particle : event.collection("LHEPart").objects()) { + const auto pdg_id = std::abs(particle.get("pdgId")); + if (pdg_id == target_pdg_id) { + lhe_v_pt = std::max(lhe_v_pt, particle.pt()); + } + } + if (lhe_v_pt < 0.0f && event.has_physical_branch("LHE_Vpt")) { + lhe_v_pt = event.scalar("LHE_Vpt"); + } + return lhe_v_pt; +} + +float HeavyFlavBaseProducer::get_gen_v_pt(Event &event) const { + if (!event.is_mc()) { + return -1.0f; + } + float fallback = -1.0f; + float hard_process = -1.0f; + for (const auto &particle : event.collection("GenPart").objects()) { + const auto pdg_id = std::abs(particle.get("pdgId")); + if (pdg_id != 23 && pdg_id != 24) { + continue; + } + fallback = std::max(fallback, particle.pt()); + if ((particle.get("statusFlags") & (1 << 13)) != 0) { + hard_process = std::max(hard_process, particle.pt()); + } + } + return hard_process >= 0.0f ? hard_process : fallback; +} + // Reset and fill event-level output shared by all heavy-flavour channels: // identifiers, era/lumi labels, MET filters, L1 prefiring, lepton count, HT, // corrected MET, generator weight, optional LHE weights, PU weights, and top-pt @@ -310,101 +372,117 @@ void HeavyFlavBaseProducer::fill_base_event_info(Event &event, JmeVariation vari out_.fill("jetVetoFlag", event.has("jetVetoFlag") ? event.get("jetVetoFlag") : std::int32_t{-99}); out_.fill("genWeight", event.is_mc() ? event.scalar("genWeight") : 1.0f); if (config_.include_lhe_weights) { - out_.fill("LHEScaleWeight", variation == JmeVariation::Nominal && event.has_physical_branch("LHEScaleWeight") + const auto nominal = variation == JmeVariation::Nominal; + out_.fill("LHEScaleWeight", nominal && event.has_physical_branch("LHEScaleWeight") ? event.vector("LHEScaleWeight") : std::vector{}); + out_.fill("LHEPdfWeight", nominal && event.has_physical_branch("LHEPdfWeight") + ? event.vector("LHEPdfWeight") + : std::vector{}); } pu_weight_producer_->fill(event, out_); + nlo_ew_weight_producer_->fill(event, out_); top_pt_weight_producer_->fill(event, out_); } -// Fill the leading selected fatjet block. Values come from the JME-corrected -// fatjet and its linked corrected subjets; tagger branches are driven by +// Fill one selected fatjet slot. Values come from the JME-corrected fatjet and +// its linked corrected subjets; tagger branches are driven by // stored_tagger_names, and gen-matching branches use attributes attached by // load_gen_history(). -void HeavyFlavBaseProducer::fill_fatjet_info(Event &event, const std::vector &fatjets) { - if (fatjets.empty()) { +void HeavyFlavBaseProducer::fill_fatjet_info(Event &event, const std::vector &fatjets, + std::size_t fatjet_index) { + if (fatjet_index >= output_fatjet_count()) { + throw std::out_of_range("Fatjet output index exceeds the configured number of slots"); + } + if (fatjet_index >= fatjets.size() || !fatjets[fatjet_index].get("is_qualified")) { return; } - const auto &fj = fatjets.front(); + const auto &fj = fatjets[fatjet_index]; + const auto prefix = fatjet_branch_name(fatjet_index, ""); + const auto fill = [this, &prefix](std::string_view variable, const auto &value) { + out_.fill(prefix + std::string(variable), value); + }; const auto subjets = fj.extra>("subjets"); - out_.fill("fj_1_is_qualified", fj.get("is_qualified")); - out_.fill("fj_1_pt", fj.pt()); - out_.fill("fj_1_eta", fj.eta()); - out_.fill("fj_1_phi", fj.phi()); - out_.fill("fj_1_mass", fj.mass()); - out_.fill("fj_1_rawpt", safe_object_float(fj, "rawPt", -1.0f)); - out_.fill("fj_1_sdmass", fj.get("msoftdrop")); - out_.fill("fj_1_sdmass_uncorrected", safe_object_float(fj, "msoftdrop_uncorrected", 0.0f)); - out_.fill("fj_1_tau1", safe_object_float(fj, "tau1", 0.0f)); - out_.fill("fj_1_tau2", safe_object_float(fj, "tau2", 0.0f)); - out_.fill("fj_1_tau3", safe_object_float(fj, "tau3", 0.0f)); - out_.fill("fj_1_tau4", safe_object_float(fj, "tau4", 0.0f)); + fill("is_qualified", fj.get("is_qualified")); + fill("pt", fj.pt()); + fill("eta", fj.eta()); + fill("phi", fj.phi()); + fill("mass", fj.mass()); + fill("rawpt", safe_object_float(fj, "rawPt", -1.0f)); + const auto raw_mass = safe_object_float(fj, "rawMass", 0.0f); + fill("gptmass_x2p", raw_mass * safe_object_float(fj, "globalParT3_massCorrX2p", 0.0f)); + fill("gptmass_generic", raw_mass * safe_object_float(fj, "globalParT3_massCorrGeneric", 0.0f)); + fill("sdmass", fj.get("msoftdrop")); + fill("sdmass_uncorrected", safe_object_float(fj, "msoftdrop_uncorrected", 0.0f)); + fill("tau1", safe_object_float(fj, "tau1", 0.0f)); + fill("tau2", safe_object_float(fj, "tau2", 0.0f)); + fill("tau3", safe_object_float(fj, "tau3", 0.0f)); + fill("tau4", safe_object_float(fj, "tau4", 0.0f)); for (const auto &tagger : config_.tagger_names) { - out_.fill("fj_1_" + tagger, safe_object_float(fj, tagger, -99.0f)); + fill(tagger, safe_object_float(fj, tagger, -99.0f)); } if (!subjets.empty()) { - out_.fill("fj_1_sj1_pt", subjets[0].pt()); - out_.fill("fj_1_sj1_eta", subjets[0].eta()); - out_.fill("fj_1_sj1_phi", subjets[0].phi()); - out_.fill("fj_1_sj1_mass", subjets[0].mass()); - out_.fill("fj_1_sj1_rawpt", safe_object_float(subjets[0], "rawPt", -1.0f)); - out_.fill("fj_1_sj1_btagdeepcsv", safe_object_float(subjets[0], "btagDeepB", -1.0f)); - out_.fill("fj_1_sj1_nbhadrons", safe_object_int(subjets[0], "nBHadrons", -1)); - out_.fill("fj_1_sj1_nchadrons", safe_object_int(subjets[0], "nCHadrons", -1)); - out_.fill("fj_1_sj1_partonflavour", safe_object_int(subjets[0], "partonFlavour", -1)); + fill("sj1_pt", subjets[0].pt()); + fill("sj1_eta", subjets[0].eta()); + fill("sj1_phi", subjets[0].phi()); + fill("sj1_mass", subjets[0].mass()); + fill("sj1_rawpt", safe_object_float(subjets[0], "rawPt", -1.0f)); + fill("sj1_btagdeepcsv", safe_object_float(subjets[0], "btagDeepB", -1.0f)); + fill("sj1_nbhadrons", safe_object_int(subjets[0], "nBHadrons", -1)); + fill("sj1_nchadrons", safe_object_int(subjets[0], "nCHadrons", -1)); + fill("sj1_partonflavour", safe_object_int(subjets[0], "partonFlavour", -1)); } if (subjets.size() > 1U) { - out_.fill("fj_1_deltaR_sj12", delta_r(subjets[0], subjets[1])); - out_.fill("fj_1_sj2_pt", subjets[1].pt()); - out_.fill("fj_1_sj2_eta", subjets[1].eta()); - out_.fill("fj_1_sj2_phi", subjets[1].phi()); - out_.fill("fj_1_sj2_mass", subjets[1].mass()); - out_.fill("fj_1_sj2_rawpt", safe_object_float(subjets[1], "rawPt", -1.0f)); - out_.fill("fj_1_sj2_btagdeepcsv", safe_object_float(subjets[1], "btagDeepB", -1.0f)); - out_.fill("fj_1_sj2_nbhadrons", safe_object_int(subjets[1], "nBHadrons", -1)); - out_.fill("fj_1_sj2_nchadrons", safe_object_int(subjets[1], "nCHadrons", -1)); - out_.fill("fj_1_sj2_partonflavour", safe_object_int(subjets[1], "partonFlavour", -1)); + fill("deltaR_sj12", delta_r(subjets[0], subjets[1])); + fill("sj2_pt", subjets[1].pt()); + fill("sj2_eta", subjets[1].eta()); + fill("sj2_phi", subjets[1].phi()); + fill("sj2_mass", subjets[1].mass()); + fill("sj2_rawpt", safe_object_float(subjets[1], "rawPt", -1.0f)); + fill("sj2_btagdeepcsv", safe_object_float(subjets[1], "btagDeepB", -1.0f)); + fill("sj2_nbhadrons", safe_object_int(subjets[1], "nBHadrons", -1)); + fill("sj2_nchadrons", safe_object_int(subjets[1], "nCHadrons", -1)); + fill("sj2_partonflavour", safe_object_int(subjets[1], "partonFlavour", -1)); } else { - out_.fill("fj_1_deltaR_sj12", 99.0f); + fill("deltaR_sj12", 99.0f); } const auto gen_fatjets = event.collection(genfatjet_name_).objects(); const auto gen_idx = safe_object_int(fj, "genJetAK8Idx", -1); if (gen_idx >= 0 && static_cast(gen_idx) < gen_fatjets.size()) { const auto &gen_fj = gen_fatjets[static_cast(gen_idx)]; - out_.fill("fj_1_genfj_nbhadrons", safe_object_int(gen_fj, "nBHadrons", -1)); - out_.fill("fj_1_genfj_nchadrons", safe_object_int(gen_fj, "nCHadrons", -1)); - out_.fill("fj_1_genfj_partonflavour", safe_object_int(gen_fj, "partonFlavour", -1)); + fill("genfj_nbhadrons", safe_object_int(gen_fj, "nBHadrons", -1)); + fill("genfj_nchadrons", safe_object_int(gen_fj, "nCHadrons", -1)); + fill("genfj_partonflavour", safe_object_int(gen_fj, "partonFlavour", -1)); } else { - out_.fill("fj_1_genfj_nbhadrons", std::int32_t{-1}); - out_.fill("fj_1_genfj_nchadrons", std::int32_t{-1}); - out_.fill("fj_1_genfj_partonflavour", std::int32_t{-1}); + fill("genfj_nbhadrons", std::int32_t{-1}); + fill("genfj_nchadrons", std::int32_t{-1}); + fill("genfj_partonflavour", std::int32_t{-1}); } - out_.fill("fj_1_nbhadrons", safe_object_int(fj, "nBHadrons", -1)); - out_.fill("fj_1_nchadrons", safe_object_int(fj, "nCHadrons", -1)); - out_.fill("fj_1_partonflavour", safe_object_int(fj, "partonFlavour", -1)); - out_.fill("fj_1_dr_H", safe_object_float(fj, "dr_H", 99.0f)); - out_.fill("fj_1_dr_H_daus", safe_object_float(fj, "dr_H_daus", 99.0f)); - out_.fill("fj_1_H_pt", safe_object_float(fj, "H_pt", -1.0f)); - out_.fill("fj_1_H_decay", safe_object_int(fj, "H_decay", 0)); - out_.fill("fj_1_dr_Z", safe_object_float(fj, "dr_Z", 99.0f)); - out_.fill("fj_1_dr_Z_daus", safe_object_float(fj, "dr_Z_daus", 99.0f)); - out_.fill("fj_1_Z_pt", safe_object_float(fj, "Z_pt", -1.0f)); - out_.fill("fj_1_Z_decay", safe_object_int(fj, "Z_decay", 0)); - out_.fill("fj_1_dr_W", safe_object_float(fj, "dr_W", 99.0f)); - out_.fill("fj_1_dr_W_daus", safe_object_float(fj, "dr_W_daus", 99.0f)); - out_.fill("fj_1_W_pt", safe_object_float(fj, "W_pt", -1.0f)); - out_.fill("fj_1_W_decay", safe_object_int(fj, "W_decay", 0)); - out_.fill("fj_1_dr_T", safe_object_float(fj, "dr_T", 99.0f)); - out_.fill("fj_1_dr_T_b", safe_object_float(fj, "dr_T_b", 99.0f)); - out_.fill("fj_1_dr_T_Wq_max", safe_object_float(fj, "dr_T_Wq_max", 99.0f)); - out_.fill("fj_1_dr_T_Wq_min", safe_object_float(fj, "dr_T_Wq_min", 99.0f)); - out_.fill("fj_1_T_Wq_max_pdgId", safe_object_int(fj, "T_Wq_max_pdgId", 0)); - out_.fill("fj_1_T_Wq_min_pdgId", safe_object_int(fj, "T_Wq_min_pdgId", 0)); - out_.fill("fj_1_T_pt", safe_object_float(fj, "T_pt", -1.0f)); + fill("nbhadrons", safe_object_int(fj, "nBHadrons", -1)); + fill("nchadrons", safe_object_int(fj, "nCHadrons", -1)); + fill("partonflavour", safe_object_int(fj, "partonFlavour", -1)); + fill("dr_H", safe_object_float(fj, "dr_H", 99.0f)); + fill("dr_H_daus", safe_object_float(fj, "dr_H_daus", 99.0f)); + fill("H_pt", safe_object_float(fj, "H_pt", -1.0f)); + fill("H_decay", safe_object_int(fj, "H_decay", 0)); + fill("dr_Z", safe_object_float(fj, "dr_Z", 99.0f)); + fill("dr_Z_daus", safe_object_float(fj, "dr_Z_daus", 99.0f)); + fill("Z_pt", safe_object_float(fj, "Z_pt", -1.0f)); + fill("Z_decay", safe_object_int(fj, "Z_decay", 0)); + fill("dr_W", safe_object_float(fj, "dr_W", 99.0f)); + fill("dr_W_daus", safe_object_float(fj, "dr_W_daus", 99.0f)); + fill("W_pt", safe_object_float(fj, "W_pt", -1.0f)); + fill("W_decay", safe_object_int(fj, "W_decay", 0)); + fill("dr_T", safe_object_float(fj, "dr_T", 99.0f)); + fill("dr_T_b", safe_object_float(fj, "dr_T_b", 99.0f)); + fill("dr_T_Wq_max", safe_object_float(fj, "dr_T_Wq_max", 99.0f)); + fill("dr_T_Wq_min", safe_object_float(fj, "dr_T_Wq_min", 99.0f)); + fill("T_Wq_max_pdgId", safe_object_int(fj, "T_Wq_max_pdgId", 0)); + fill("T_Wq_min_pdgId", safe_object_int(fj, "T_Wq_min_pdgId", 0)); + fill("T_pt", safe_object_float(fj, "T_pt", -1.0f)); } } // namespace nano diff --git a/src/producers/HeavyFlavMinimalProducer.cpp b/src/producers/HeavyFlavMinimalProducer.cpp index fc537e5..9f8f9e0 100644 --- a/src/producers/HeavyFlavMinimalProducer.cpp +++ b/src/producers/HeavyFlavMinimalProducer.cpp @@ -64,7 +64,7 @@ bool HeavyFlavMinimalProducer::analyze_variation(Event &event, const JmeEventRes } fill_base_event_info(event, variation); - fill_fatjet_info(event, std::vector{leading_fatjet}); + fill_fatjet_info(event, std::vector{leading_fatjet}, 0U); return true; } diff --git a/src/producers/HeavyFlavMuonSampleProducer.cpp b/src/producers/HeavyFlavMuonSampleProducer.cpp index 8f0c071..59f21f2 100644 --- a/src/producers/HeavyFlavMuonSampleProducer.cpp +++ b/src/producers/HeavyFlavMuonSampleProducer.cpp @@ -116,7 +116,7 @@ bool HeavyFlavMuonSampleProducer::analyze_variation(Event &event, const JmeEvent probe_jets.erase(probe_jets.begin() + 1, probe_jets.end()); fill_base_event_info(event, variation); - fill_fatjet_info(event, probe_jets); + fill_fatjet_info(event, probe_jets, 0U); out_.fill("passMuTrig", pass_trigger(event, config_.required_triggers)); out_.fill("muon_pt", mu.pt()); diff --git a/src/producers/HeavyFlavQCDSampleProducer.cpp b/src/producers/HeavyFlavQCDSampleProducer.cpp new file mode 100644 index 0000000..bc56031 --- /dev/null +++ b/src/producers/HeavyFlavQCDSampleProducer.cpp @@ -0,0 +1,346 @@ +#include "nano/producers/HeavyFlavQCDSampleProducer.h" + +#include "nano/core/Collection.h" +#include "nano/core/Helpers.h" + +#include +#include +#include +#include + +namespace nano { + +namespace { + +bool get_channel_bool(const ProducerConfig &config, const std::string &key) { + // Resolve a required boolean from the active channel configuration and fail + // early when the runtime card does not provide it. + const auto it = config.channel_options.bools.find(key); + if (it == config.channel_options.bools.end()) { + throw std::runtime_error("Missing boolean channel option channels." + + config.channel + "." + key); + } + return it->second; +} + +std::string fatjet_sv_branch_name(std::size_t fatjet_index, + std::string_view variable) { + // Convert the zero-based output slot and a logical variable name into the + // flat-tree convention, for example slot 0 + "nsv" becomes "fj_1_nsv". + return "fj_" + std::to_string(fatjet_index + 1U) + "_" + + std::string(variable); +} + +float corrected_sv_mass(const ObjectView &sv) { + // Compute the pAngle-based corrected SV mass used by the NanoHRTTools + // reference and store it later as an SV-level derived attribute. + const auto projected_momentum = + static_cast(polar_p4(sv).P()) * std::sin(sv.get("pAngle")); + const auto mass = sv.mass(); + return std::sqrt(mass * mass + projected_momentum * projected_momentum) + + projected_momentum; +} + +} // namespace + +/* + * Channel summary: qcd + * + * Purpose + * - Select a dijet phase space dominated by multijet production. + * - Retain both leading boosted AK8 jets for heavy-flavour tagger studies. + * - Require displaced-vertex content in the probe jets by default. + * + * Event selection implemented in this producer + * - Build loose leptons and use the shared AK4/AK8 lepton cleaning. + * - Apply the shared AK4/AK8/SubJet JME corrections and MET propagation. + * - Require at least two selected AK8 fatjets and keep the leading two in + * corrected-pt order. + * - Mark a fatjet qualified when 50 < msoftdrop < 200 GeV. + * - When channels.qcd.apply_sv_criteria is enabled, reject events with fewer + * than two secondary vertices. Fatjet qualification then additionally requires + * every linked subjet to have at least one matched SV; in the usual two- + * subjet case, both sj1 and sj2 must satisfy this matching requirement. + * - Require at least one of the two leading fatjets to be qualified. + * - When channels.qcd.fill_sv is enabled, store matched-SV counts, track sums, + * leading-SV properties, and corrected SV masses for both fatjet slots. + */ + +HeavyFlavQCDSampleProducer::HeavyFlavQCDSampleProducer(ProducerConfig config) + : HeavyFlavBaseProducer([&config] { + config.channel = "qcd"; + return config; + }()), + apply_sv_criteria_(get_channel_bool(config_, "apply_sv_criteria")), + fill_sv_(get_channel_bool(config_, "fill_sv")) {} + +void HeavyFlavQCDSampleProducer::begin_file() { + HeavyFlavBaseProducer::begin_file(); + out_.branch("passHTTrig", false); + if (fill_sv_) { + define_fatjet_sv_branches(0U); + define_fatjet_sv_branches(1U); + } +} + +void HeavyFlavQCDSampleProducer::select_secondary_vertices(Event &event) const { + // Build the pt-ordered SV collection used throughout the event and attach + // the corrected mass to each SV when detailed SV output is enabled. + auto secondary_vertices = event.collection("SV").objects(); + if (fill_sv_) { + for (auto &sv : secondary_vertices) { + sv.set("masscor", corrected_sv_mass(sv)); + } + } + std::sort( + secondary_vertices.begin(), secondary_vertices.end(), + [](const auto &lhs, const auto &rhs) { return lhs.pt() > rhs.pt(); }); + event.set("secondary_vertices", std::move(secondary_vertices)); +} + +void HeavyFlavQCDSampleProducer::match_secondary_vertices( + std::vector &fatjets, + const std::vector &secondary_vertices) const { + // Match the ordered SVs to each fatjet and its linked subjets, then attach + // the match lists and derived counts directly to the corresponding objects. + for (auto &fatjet : fatjets) { + std::vector matched_vertices; + std::int32_t ntracks = 0; + std::int32_t ntracks_sv12 = 0; + std::int32_t nsv_ptgt25 = 0; + std::int32_t nsv_ptgt50 = 0; + for (const auto &sv : secondary_vertices) { + if (delta_r(sv, fatjet) < jet_cone_size_) { + matched_vertices.push_back(sv); + if (fill_sv_) { + const auto sv_ntracks = sv.get("ntracks"); + ntracks += sv_ntracks; + if (matched_vertices.size() <= 2U) { + ntracks_sv12 += sv_ntracks; + } + nsv_ptgt25 += sv.pt() > 25.0f ? 1 : 0; + nsv_ptgt50 += sv.pt() > 50.0f ? 1 : 0; + } + } + } + // Define the matched SV list (sv_list) for the fatjet. + fatjet.set("sv_list", std::move(matched_vertices)); + if (fill_sv_) { + const auto &fatjet_vertices = + fatjet.extra>("sv_list"); + fatjet.set("nsv", static_cast(fatjet_vertices.size())); + fatjet.set("nsv_ptgt25", nsv_ptgt25); + fatjet.set("nsv_ptgt50", nsv_ptgt50); + fatjet.set("ntracks", ntracks); + fatjet.set("ntracks_sv12", ntracks_sv12); + } + + auto subjets = fatjet.extra>("subjets"); + const auto match_radius = + subjets.size() == 2U + ? std::min(0.4f, 0.5f * delta_r(subjets[0], subjets[1])) + : 0.4f; + for (auto &subjet : subjets) { + std::vector subjet_vertices; + std::int32_t subjet_ntracks = 0; + for (const auto &sv : secondary_vertices) { + if (delta_r(sv, subjet) < match_radius) { + subjet_vertices.push_back(sv); + if (fill_sv_) { + subjet_ntracks += sv.get("ntracks"); + } + } + } + // For each subjet, define the matched SV list (sv_list) as well. + subjet.set("sv_list", std::move(subjet_vertices)); + if (fill_sv_) { + const auto &matched_subjet_vertices = + subjet.extra>("sv_list"); + subjet.set("nsv", + static_cast(matched_subjet_vertices.size())); + subjet.set("ntracks", subjet_ntracks); + } + } + fatjet.set("subjets", subjets); + + float sj12_masscor_dxysig = 0.0f; + if (fill_sv_ && subjets.size() >= 2U) { + const auto &sj1_vertices = + subjets[0].extra>("sv_list"); + const auto &sj2_vertices = + subjets[1].extra>("sv_list"); + if (!sj1_vertices.empty() && !sj2_vertices.empty()) { + const auto &sj1_sv = sj1_vertices.front(); + const auto &sj2_sv = sj2_vertices.front(); + const auto &selected_sv = + sj1_sv.get("dxySig") > sj2_sv.get("dxySig") ? sj1_sv + : sj2_sv; + sj12_masscor_dxysig = selected_sv.get("masscor"); + } + } + if (fill_sv_) { + fatjet.set("sj12_masscor_dxysig", sj12_masscor_dxysig); + } + } +} + +void HeavyFlavQCDSampleProducer::define_fatjet_sv_branches( + std::size_t fatjet_index) { + // Keep logical variable names separate from the output slot so the same + // schema can be generated consistently for fj_1 and fj_2. + constexpr std::string_view fatjet_int_variables[]{ + "nsv", "nsv_ptgt25", "nsv_ptgt50", "ntracks", "ntracks_sv12", + }; + constexpr std::string_view subjet_int_variables[]{ + "ntracks", + "nsv", + "sv1_ntracks", + }; + constexpr std::string_view subjet_float_variables[]{ + "sv1_pt", "sv1_mass", "sv1_masscor", "sv1_dxy", "sv1_dxysig", + "sv1_dlen", "sv1_dlensig", "sv1_chi2ndof", "sv1_pangle", + }; + + // Define fatjet-level branches such as fj_1_nsv, fj_1_ntracks_sv12, and the + // corresponding fj_2 branches. + for (const auto variable : fatjet_int_variables) { + out_.branch(fatjet_sv_branch_name(fatjet_index, variable), std::int32_t{0}); + } + + // Define both subjet blocks, for example fj_1_sj1_ntracks, + // fj_1_sj2_sv1_masscor, fj_2_sj1_nsv, and fj_2_sj2_sv1_pangle. + for (std::size_t subjet_index = 0; subjet_index < 2U; ++subjet_index) { + const auto subjet_prefix = "sj" + std::to_string(subjet_index + 1U) + "_"; + for (const auto variable : subjet_int_variables) { + out_.branch(fatjet_sv_branch_name(fatjet_index, + subjet_prefix + std::string(variable)), + std::int32_t{0}); + } + for (const auto variable : subjet_float_variables) { + out_.branch(fatjet_sv_branch_name(fatjet_index, + subjet_prefix + std::string(variable)), + 0.0f); + } + } + // Define the combined leading-SV branch, for example + // fj_1_sj12_masscor_dxysig or fj_2_sj12_masscor_dxysig. + out_.branch(fatjet_sv_branch_name(fatjet_index, "sj12_masscor_dxysig"), 0.0f); +} + +void HeavyFlavQCDSampleProducer::fill_fatjet_sv_info( + const std::vector &fatjets, std::size_t fatjet_index) { + // Fill the QCD-only SV block for one qualified fatjet slot directly from + // derived attributes attached during secondary-vertex matching. + if (fatjet_index >= fatjets.size() || + !fatjets[fatjet_index].get("is_qualified")) { + return; + } + + const auto &fatjet = fatjets[fatjet_index]; + const auto fill = [this, fatjet_index](std::string_view variable, + const auto &value) { + out_.fill(fatjet_sv_branch_name(fatjet_index, variable), value); + }; + fill("nsv", fatjet.get("nsv")); + fill("nsv_ptgt25", fatjet.get("nsv_ptgt25")); + fill("nsv_ptgt50", fatjet.get("nsv_ptgt50")); + fill("ntracks", fatjet.get("ntracks")); + fill("ntracks_sv12", fatjet.get("ntracks_sv12")); + + const auto &subjets = fatjet.extra>("subjets"); + const auto num_output_subjets = std::min(2U, subjets.size()); + for (std::size_t subjet_index = 0; subjet_index < num_output_subjets; + ++subjet_index) { + const auto prefix = "sj" + std::to_string(subjet_index + 1U) + "_"; + const auto &subjet = subjets[subjet_index]; + fill(prefix + "ntracks", subjet.get("ntracks")); + fill(prefix + "nsv", subjet.get("nsv")); + const auto &matched_vertices = + subjet.extra>("sv_list"); + if (matched_vertices.empty()) { + continue; + } + + const auto &sv = matched_vertices.front(); + fill(prefix + "sv1_pt", sv.pt()); + fill(prefix + "sv1_mass", sv.mass()); + fill(prefix + "sv1_masscor", sv.get("masscor")); + fill(prefix + "sv1_ntracks", sv.get("ntracks")); + fill(prefix + "sv1_dxy", sv.get("dxy")); + fill(prefix + "sv1_dxysig", sv.get("dxySig")); + fill(prefix + "sv1_dlen", sv.get("dlen")); + fill(prefix + "sv1_dlensig", sv.get("dlenSig")); + fill(prefix + "sv1_chi2ndof", sv.get("chi2")); + fill(prefix + "sv1_pangle", sv.get("pAngle")); + } + fill("sj12_masscor_dxysig", fatjet.get("sj12_masscor_dxysig")); +} + +bool HeavyFlavQCDSampleProducer::analyze_common(Event &event) { + prepare_common_objects(event); + select_secondary_vertices(event); + return true; +} + +bool HeavyFlavQCDSampleProducer::analyze_variation( + Event &event, const JmeEventResult &jme_result, JmeVariation variation) { + apply_jme_and_select_jets(event, jme_result, variation); + + auto probe_jets = event.get>("fatjets"); + if (probe_jets.size() < 2U) { + return false; + } + probe_jets.resize(2U); + + const auto &secondary_vertices = + event.get>("secondary_vertices"); + if (apply_sv_criteria_ && secondary_vertices.size() < 2U) { + return false; + } + match_secondary_vertices(probe_jets, secondary_vertices); + + // Require at least one linked subjet and an SV match for every linked subjet. + const auto pass_sv_requirement = [](const ObjectView &fatjet) { + const auto &subjets = fatjet.extra>("subjets"); + return !subjets.empty() && + std::all_of( + subjets.begin(), subjets.end(), [](const ObjectView &subjet) { + return !subjet.extra>("sv_list") + .empty(); + }); + }; + bool has_qualified_fatjet = false; + for (auto &fatjet : probe_jets) { + const auto softdrop_mass = fatjet.get("msoftdrop"); + const auto pass_sv = !apply_sv_criteria_ || pass_sv_requirement(fatjet); + const auto is_qualified = + softdrop_mass > 50.0f && softdrop_mass < 200.0f && pass_sv; + fatjet.set("is_qualified", is_qualified); + has_qualified_fatjet = has_qualified_fatjet || is_qualified; + } + if (!has_qualified_fatjet) { + return false; + } + + // Reset and fill the shared event output, the two common fatjet blocks, the + // optional QCD SV blocks, and the channel trigger decision. + fill_base_event_info(event, variation); + fill_fatjet_info(event, probe_jets, 0U); + fill_fatjet_info(event, probe_jets, 1U); + if (fill_sv_) { + fill_fatjet_sv_info(probe_jets, 0U); + fill_fatjet_sv_info(probe_jets, 1U); + } + out_.fill("passHTTrig", pass_trigger(event, config_.required_triggers)); + return true; +} + +bool HeavyFlavQCDSampleProducer::analyze(Event &event) { + if (!analyze_common(event)) { + return false; + } + const auto jme_result = compute_jme_result(event); + return analyze_variation(event, jme_result, JmeVariation::Nominal); +} + +} // namespace nano diff --git a/src/producers/HeavyFlavZbbSampleProducer.cpp b/src/producers/HeavyFlavZbbSampleProducer.cpp new file mode 100644 index 0000000..2add4a0 --- /dev/null +++ b/src/producers/HeavyFlavZbbSampleProducer.cpp @@ -0,0 +1,83 @@ +#include "nano/producers/HeavyFlavZbbSampleProducer.h" + +#include "nano/core/Collection.h" +#include "nano/core/Helpers.h" + +#include +#include + +namespace nano { + +namespace { + +bool bool_option(const ProducerConfig &config, const std::string &key, bool fallback) { + const auto it = config.channel_options.bools.find(key); + return it == config.channel_options.bools.end() ? fallback : it->second; +} + +} // namespace + +/* + * Channel summary: zbb + * + * Purpose + * - Select a boosted dijet phase space used for hadronic Z and heavy-flavour + * tagging studies. + * + * Event selection implemented in this producer + * - Require two corrected AK8 jets with pt >= 400/200 GeV. + * - Require the two leading AK8 jets to satisfy |DeltaPhi| >= pi/2. + * - Require at least two secondary vertices when require_sv_cut is enabled. + */ + +HeavyFlavZbbSampleProducer::HeavyFlavZbbSampleProducer(ProducerConfig config) + : HeavyFlavBaseProducer([&config] { + config.channel = "zbb"; + return config; + }()), + require_sv_cut_(bool_option(config_, "require_sv_cut", true)) {} + +void HeavyFlavZbbSampleProducer::begin_file() { + HeavyFlavBaseProducer::begin_file(); + out_.branch("passHTTrig", false); + out_.branch("lheVpt", -1.0f); + out_.branch("genVpt", -1.0f); +} + +bool HeavyFlavZbbSampleProducer::analyze_common(Event &event) { + prepare_common_objects(event); + return true; +} + +bool HeavyFlavZbbSampleProducer::analyze_variation(Event &event, const JmeEventResult &jme_result, JmeVariation variation) { + apply_jme_and_select_jets(event, jme_result, variation); + auto fatjets = event.get>("fatjets"); + constexpr float pi = 3.14159265358979323846f; + if (fatjets.size() < 2U || fatjets[0].pt() < 400.0f || fatjets[1].pt() < 200.0f || + std::abs(delta_phi(fatjets[0], fatjets[1])) < 0.5f * pi) { + return false; + } + fatjets.resize(2); + + if (require_sv_cut_ && event.collection("SV").size() < 2U) { + return false; + } + + fill_base_event_info(event, variation); + fill_fatjet_info(event, fatjets, 0U); + fill_fatjet_info(event, fatjets, 1U); + out_.fill("passHTTrig", pass_trigger(event, config_.required_triggers)); + out_.fill("lheVpt", get_lhe_v_pt(event)); + out_.fill("genVpt", get_gen_v_pt(event)); + return true; +} + +bool HeavyFlavZbbSampleProducer::analyze(Event &event) { + if (!analyze_common(event)) { + return false; + } + const auto jme_result = compute_jme_result(event); + return analyze_variation(event, jme_result, JmeVariation::Nominal); +} + +} // namespace nano diff --git a/src/producers/HeavyFlavZmmSampleProducer.cpp b/src/producers/HeavyFlavZmmSampleProducer.cpp new file mode 100644 index 0000000..7c2ec9b --- /dev/null +++ b/src/producers/HeavyFlavZmmSampleProducer.cpp @@ -0,0 +1,179 @@ +#include "nano/producers/HeavyFlavZmmSampleProducer.h" + +#include "nano/core/Collection.h" +#include "nano/core/Helpers.h" + +#include +#include + +namespace nano { + +namespace { + +void book_sf(OutputModel &out, const std::string &name) { + out.branch(name, 1.0f); + out.branch(name + "_stat_up", 1.0f); + out.branch(name + "_stat_down", 1.0f); + out.branch(name + "_syst_up", 1.0f); + out.branch(name + "_syst_down", 1.0f); +} + +void fill_sf(OutputModel &out, const std::string &name, const MuonSFResult &sf) { + out.fill(name, sf.nominal); + out.fill(name + "_stat_up", sf.stat_up); + out.fill(name + "_stat_down", sf.stat_down); + out.fill(name + "_syst_up", sf.syst_up); + out.fill(name + "_syst_down", sf.syst_down); +} + +} // namespace + +/* + * Channel summary: zmm + * + * Purpose + * - Select boosted Z->mumu events for a clean recoil-jet control sample. + * + * Event selection implemented in this producer + * - Apply the configured muon scale correction and MC resolution smearing. + * - Require exactly two isolated opposite-sign muons with pt >= 60/30 GeV. + * - Require dimuon pt >= 400 GeV and 70 <= mass <= 110 GeV. + * - Require a corrected AK8 jet separated from both muons by DeltaR > 0.8. + * - Keep only the leading separated AK8 jet for output. + */ + +HeavyFlavZmmSampleProducer::HeavyFlavZmmSampleProducer(ProducerConfig config) + : HeavyFlavBaseProducer([&config] { + config.channel = "zmm"; + return config; + }()), + muon_correction_(config_) {} + +void HeavyFlavZmmSampleProducer::begin_file() { + HeavyFlavBaseProducer::begin_file(); + out_.branch("passMuTrig", false); + out_.branch("leptonicZ_pt", 0.0f); + out_.branch("leptonicZ_mass", 0.0f); + out_.branch("lheVpt", -1.0f); + out_.branch("genVpt", -1.0f); + for (int index = 0; index < 2; ++index) { + const auto prefix = "muon" + std::to_string(index) + "_"; + out_.branch(prefix + "pt", 0.0f); + out_.branch(prefix + "eta", 0.0f); + out_.branch(prefix + "phi", 0.0f); + out_.branch(prefix + "mass", 0.0f); + out_.branch(prefix + "miniIso", 0.0f); + } + for (const auto *name : {"muonHLTSF", "muonIDSF", "muonISOSF", "muonIDISOSF"}) { + book_sf(out_, name); + } +} + +bool HeavyFlavZmmSampleProducer::analyze_common(Event &event) { + auto muons = event.collection("Muon").objects(); + auto variations = muon_correction_.produce(event, muons); + event.set("muon_variations", std::move(variations)); + muon_correction_.apply(event.get("muon_variations"), JmeVariation::Nominal, muons); + prepare_common_objects(event); + + const auto candidates = {JmeVariation::Nominal, JmeVariation::MuonScaleUp, JmeVariation::MuonScaleDown, + JmeVariation::MuonSmearUp, JmeVariation::MuonSmearDown}; + bool passes_any_variation = false; + for (const auto variation : candidates) { + if (!event.is_mc() && variation != JmeVariation::Nominal) { + continue; + } + passes_any_variation = select_muons(event, variation) || passes_any_variation; + } + muons = event.collection("Muon").objects(); + muon_correction_.apply(event.get("muon_variations"), JmeVariation::Nominal, muons); + return passes_any_variation; +} + +bool HeavyFlavZmmSampleProducer::select_muons(Event &event, JmeVariation variation) { + auto muons = event.collection("Muon").objects(); + muon_correction_.apply(event.get("muon_variations"), variation, muons); + std::vector selected; + for (auto &muon : muons) { + const auto passes_id = (muon.pt() > 15.0f && muon.get("looseId")) || + (muon.pt() > 30.0f && muon.get("highPtId") != 0); + if (passes_id && std::abs(muon.eta()) < 2.4f && muon.get("pfIsoId") > 1) { + selected.push_back(muon); + } + } + if (selected.size() != 2U) { + return false; + } + std::sort(selected.begin(), selected.end(), [](const auto &a, const auto &b) { return a.pt() > b.pt(); }); + if (selected[0].pt() < 60.0f || selected[1].pt() < 30.0f || + selected[0].get("charge") * selected[1].get("charge") > 0) { + return false; + } + + const auto z = selected[0].p4() + selected[1].p4(); + if (z.Pt() < 400.0 || z.M() < 70.0 || z.M() > 110.0) { + return false; + } + event.set("muons", selected); + event.set("leptonicZ", z); + return true; +} + +bool HeavyFlavZmmSampleProducer::analyze_variation(Event &event, const JmeEventResult &jme_result, JmeVariation variation) { + if (!select_muons(event, variation)) { + return false; + } + apply_jme_and_select_jets(event, jme_result, variation); + const auto &muons = event.get>("muons"); + std::vector probe_jets; + for (const auto &jet : event.get>("fatjets")) { + if (delta_r(jet, muons[0]) > 0.8f && delta_r(jet, muons[1]) > 0.8f) { + probe_jets.push_back(jet); + } + } + if (probe_jets.empty()) { + return false; + } + probe_jets.resize(1); + + fill_base_event_info(event, variation); + fill_fatjet_info(event, probe_jets, 0U); + out_.fill("passMuTrig", pass_trigger(event, config_.required_triggers)); + const auto &z = event.get("leptonicZ"); + out_.fill("leptonicZ_pt", static_cast(z.Pt())); + out_.fill("leptonicZ_mass", static_cast(z.M())); + out_.fill("lheVpt", get_lhe_v_pt(event)); + out_.fill("genVpt", get_gen_v_pt(event)); + for (std::size_t index = 0; index < muons.size(); ++index) { + const auto prefix = "muon" + std::to_string(index) + "_"; + out_.fill(prefix + "pt", muons[index].pt()); + out_.fill(prefix + "eta", muons[index].eta()); + out_.fill(prefix + "phi", muons[index].phi()); + out_.fill(prefix + "mass", muons[index].mass()); + out_.fill(prefix + "miniIso", muons[index].get("miniPFRelIso_all")); + } + + if (event.is_mc()) { + const auto hlt = muon_correction_.scale_factor( + muons, "NUM_Mu50_or_CascadeMu100_or_HighPtTkMu100_DEN_CutBasedIdTrkHighPt_and_TkIsoLoose", 52.0f); + const auto id = muon_correction_.scale_factor(muons, "NUM_HighPtID_DEN_TrackerMuons", 10.0f); + const auto iso = muon_correction_.scale_factor(muons, "NUM_LooseRelTkIso_DEN_HighPtID", 10.0f); + fill_sf(out_, "muonHLTSF", hlt); + fill_sf(out_, "muonIDSF", id); + fill_sf(out_, "muonISOSF", iso); + fill_sf(out_, "muonIDISOSF", + {id.nominal * iso.nominal, id.stat_up * iso.stat_up, id.stat_down * iso.stat_down, + id.syst_up * iso.syst_up, id.syst_down * iso.syst_down}); + } + return true; +} + +bool HeavyFlavZmmSampleProducer::analyze(Event &event) { + if (!analyze_common(event)) { + return false; + } + const auto jme_result = compute_jme_result(event); + return analyze_variation(event, jme_result, JmeVariation::Nominal); +} + +} // namespace nano diff --git a/templates/condor/process.sh.in b/templates/condor/process.sh.in index 7b257ab..f013268 100644 --- a/templates/condor/process.sh.in +++ b/templates/condor/process.sh.in @@ -45,8 +45,12 @@ with open("job_manifest.json") as handle: jobs = {int(job["index"]): job for job in json.load(handle)["jobs"]} job = jobs[job_index] +sample_name = job.get("nickname") +if not isinstance(sample_name, str) or not sample_name.strip(): + raise RuntimeError("Missing sample nickname for job %d" % job_index) print("FINAL_OUTPUT=" + shlex.quote(job["output_file"])) print("INPUTS=" + shlex.quote(",".join(job["inputs"]))) +print("SAMPLE_NAME=" + shlex.quote(sample_name)) PY )" @@ -87,6 +91,7 @@ run_one_input() { --num-events "${NUM_EVENTS}" --channel "${CHANNEL}" --config "${CONFIG}" + --set "channels.${CHANNEL}.sample_name=${SAMPLE_NAME}" ) cmd+=(--variations "${VARIATIONS}") diff --git a/tests/README.md b/tests/README.md index 38597d9..bd88160 100644 --- a/tests/README.md +++ b/tests/README.md @@ -41,10 +41,32 @@ What it is meant to check: - Required output branches are present. - Event selection and key scalar/vector branch values remain compatible with the stored references. - JME variation output naming and propagation remain stable for the monitored branches. -- LHE weight copying is checked for the 2016APV MC case with `output.include_lhe_weights=true`. +- LHE scale/PDF weight copying and branch-title preservation are checked for the 2016APV MC case with `output.include_lhe_weights=true`. Some correction-dependent branches are intentionally excluded from strict value comparisons when the C++ implementation uses newer correctionlib/CMSJMECalculators behavior than the old reference production. They are still monitored for presence and reported where relevant. +## Correction equivalence tests + +Two fast CTest targets cover the correction migrations: + +```bash +ctest --test-dir build -R 'muon_external_equivalence|vjets_ewk_json' --output-on-failure +``` + +- `muon_external_equivalence` compares the old scale/smear formulas with + `MuonVariationsCalculator` for data nominal and all five MC outputs while + holding the four-input random correction and seed fixed. +- `vjets_ewk_json` compares every W/Z EWK bin and the nominal/up/down weights + between the retained ROOT references and the production correctionlib JSON + payloads, including the 100 and 6500 GeV boundaries and overflow. + +Regenerate the V+jets payloads after intentionally updating the ROOT +references with: + +```bash +python3 data/vjets-ewk/convert_root_to_json.py +``` + ## JME bundle I/O debug diagnostic This is a manual diagnostic for the JME correction inputs and outputs. It is controlled by the environment variable: diff --git a/tests/condor_sample_name_test.py b/tests/condor_sample_name_test.py new file mode 100644 index 0000000..0dccbe5 --- /dev/null +++ b/tests/condor_sample_name_test.py @@ -0,0 +1,20 @@ +import argparse +import subprocess +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--source-dir", required=True) + args = parser.parse_args() + + template = Path(args.source_dir) / "templates" / "condor" / "process.sh.in" + text = template.read_text() + assert 'sample_name = job.get("nickname")' in text + assert 'print("SAMPLE_NAME=" + shlex.quote(sample_name))' in text + assert '--set "channels.${CHANNEL}.sample_name=${SAMPLE_NAME}"' in text + subprocess.run(["bash", "-n", str(template)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/muon_external_equivalence.cpp b/tests/muon_external_equivalence.cpp new file mode 100644 index 0000000..6038f83 --- /dev/null +++ b/tests/muon_external_equivalence.cpp @@ -0,0 +1,205 @@ +#include "MuonVariationsCalculator.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct CrystalBall { + double mean; + double sigma; + double alpha; + double n; + + double inverse(double u) const { + constexpr double pi = 3.14159265358979323846; + const auto abs_alpha = std::abs(alpha); + const auto sqrt_pi_over_two = std::sqrt(pi / 2.0); + const auto sqrt_two = std::sqrt(2.0); + const auto exponential = std::exp(-0.5 * abs_alpha * abs_alpha); + const auto c1 = n / abs_alpha / (n - 1.0) * exponential; + const auto d1 = 2.0 * sqrt_pi_over_two * std::erf(abs_alpha / sqrt_two); + const auto norm = 1.0 / sigma / (d1 + 2.0 * c1); + const auto norm_sigma = norm * sigma; + const auto norm_c1 = norm_sigma * c1; + const auto f = 1.0 - abs_alpha * abs_alpha / n; + const auto g = sigma * n / abs_alpha; + const auto c = (d1 + 2.0 * c1) / c1; + const auto d = (d1 + 2.0 * c1) / 2.0; + const auto k = 1.0 / (n - 1.0); + const auto cdf = [&](double x) { + const auto delta = (x - mean) / sigma; + if (delta < -abs_alpha) { + return norm_c1 / std::pow(f - sigma * delta / g, n - 1.0); + } + if (delta > abs_alpha) { + return norm_c1 * (c - std::pow(f + sigma * delta / g, 1.0 - n)); + } + return norm_sigma * (d - sqrt_pi_over_two * std::erf(-delta / sqrt_two)); + }; + if (u < cdf(mean - abs_alpha * sigma)) { + return mean + g * (f - std::pow(norm_c1 / u, k)); + } + if (u > cdf(mean + abs_alpha * sigma)) { + return mean - g * (f - std::pow(c - u / norm_c1, -k)); + } + return mean - + sqrt_two * sigma * + boost::math::erf_inv((d - u / norm_sigma) / sqrt_pi_over_two); + } +}; + +double extra_k(double data, double mc) { + return data * data > mc * mc ? std::sqrt(data * data - mc * mc) : 0.0; +} + +std::vector> +legacy(const std::string &muon_file, const std::string &random_file, + const MuonVariationsCalculator::p4compv_t &pts, + const MuonVariationsCalculator::p4compv_t &etas, + const MuonVariationsCalculator::p4compv_t &phis, + const MuonVariationsCalculator::p4compv_int &charges, + const MuonVariationsCalculator::p4compv_int &layers, int seed, + bool is_mc) { + auto corrections = correction::CorrectionSet::from_file(muon_file); + auto randoms = correction::CorrectionSet::from_file(random_file); + const auto suffix = is_mc ? "mc" : "data"; + const auto a = corrections->at("a_" + std::string(suffix)); + const auto m = corrections->at("m_" + std::string(suffix)); + const auto cb = corrections->at("cb_params"); + const auto poly = corrections->at("poly_params"); + const auto kd = corrections->at("k_data"); + const auto km = corrections->at("k_mc"); + const auto random = randoms->at("stdflat"); + std::vector> out( + is_mc ? 5U : 1U, std::vector(pts.begin(), pts.end())); + + for (std::size_t i = 0; i < pts.size(); ++i) { + const auto scale = + 1.0 / + (m->evaluate({etas[i], phis[i], std::string("nom")}) / pts[i] + + charges[i] * a->evaluate({etas[i], phis[i], std::string("nom")})) / + pts[i]; + const auto scaled_pt = pts[i] * scale; + if (!is_mc) { + out[0][i] = scaled_pt; + continue; + } + + const auto abs_eta = std::abs(etas[i]); + CrystalBall crystal_ball{ + cb->evaluate({abs_eta, static_cast(layers[i]), 0}), + cb->evaluate({abs_eta, static_cast(layers[i]), 1}), + cb->evaluate({abs_eta, static_cast(layers[i]), 3}), + cb->evaluate({abs_eta, static_cast(layers[i]), 2}), + }; + const auto uniform = random->evaluate({pts[i], etas[i], phis[i], seed}); + const auto draw = crystal_ball.inverse(uniform); + const auto standard_deviation = [&](double pt) { + return std::max( + 0.0, + poly->evaluate({abs_eta, static_cast(layers[i]), 0}) + + poly->evaluate({abs_eta, static_cast(layers[i]), 1}) * + pt + + poly->evaluate({abs_eta, static_cast(layers[i]), 2}) * + pt * pt); + }; + const auto nominal_k = extra_k(kd->evaluate({abs_eta, std::string("nom")}), + km->evaluate({abs_eta, std::string("nom")})); + const auto mc_stat = km->evaluate({abs_eta, std::string("stat")}); + const auto smear_up_k = + extra_k(kd->evaluate({abs_eta, std::string("nom")}), + km->evaluate({abs_eta, std::string("nom")}) + mc_stat); + const auto smear_down_k = + extra_k(kd->evaluate({abs_eta, std::string("nom")}), + km->evaluate({abs_eta, std::string("nom")}) - mc_stat); + const auto a_stat = a->evaluate({etas[i], phis[i], std::string("stat")}); + const auto m_stat = m->evaluate({etas[i], phis[i], std::string("stat")}); + const auto rho_stat = + m->evaluate({etas[i], phis[i], std::string("rho_stat")}); + const auto scale_uncertainty = + scaled_pt * scaled_pt * + std::sqrt(m_stat * m_stat / (scaled_pt * scaled_pt) + a_stat * a_stat + + 2.0 * charges[i] * rho_stat * m_stat * a_stat / scaled_pt); + out[0][i] = + scaled_pt * (1.0 + nominal_k * standard_deviation(scaled_pt) * draw); + out[1][i] = + (scaled_pt + scale_uncertainty) * + (1.0 + + nominal_k * standard_deviation(scaled_pt + scale_uncertainty) * draw); + out[2][i] = + (scaled_pt - scale_uncertainty) * + (1.0 + + nominal_k * standard_deviation(scaled_pt - scale_uncertainty) * draw); + out[3][i] = + scaled_pt * (1.0 + smear_up_k * standard_deviation(scaled_pt) * draw); + out[4][i] = + scaled_pt * (1.0 + smear_down_k * standard_deviation(scaled_pt) * draw); + } + return out; +} + +void compare(const MuonVariationsCalculator::result_t &actual, + const std::vector> &expected) { + if (actual.size() != expected.size()) { + throw std::runtime_error("Variation count mismatch"); + } + for (std::size_t variation = 0; variation < expected.size(); ++variation) { + for (std::size_t muon = 0; muon < expected[variation].size(); ++muon) { + const auto observed = static_cast(actual.pt(variation)[muon]); + const auto reference = expected[variation][muon]; + const auto tolerance = std::max(1e-5, 1e-6 * std::abs(reference)); + if (std::abs(observed - reference) > tolerance) { + throw std::runtime_error( + "Muon output mismatch at variation " + std::to_string(variation) + + ", muon " + std::to_string(muon) + ": " + std::to_string(observed) + + " != " + std::to_string(reference)); + } + } + } +} + +} // namespace + +int main(int argc, char **argv) { + if (argc != 3) { + std::cerr << "Usage: muon_external_equivalence " + "\n"; + return 2; + } + try { + const MuonVariationsCalculator::p4compv_t pt{32.0f, 75.0f, 240.0f, 900.0f}; + const MuonVariationsCalculator::p4compv_t eta{-2.1f, -0.4f, 0.8f, 2.2f}; + const MuonVariationsCalculator::p4compv_t phi{-2.7f, -0.2f, 1.3f, 2.8f}; + const MuonVariationsCalculator::p4compv_int charge{-1, 1, -1, 1}; + const MuonVariationsCalculator::p4compv_int layers{9, 12, 15, 18}; + constexpr int seed = 123456789; + + auto mc = MuonVariationsCalculator::create(argv[1], true, true, argv[2], + "stdflat"); + const std::vector expected_labels{ + "nominal", "mesScaleup", "mesScaledown", "mesSmearup", "mesSmeardown"}; + if (mc.available() != expected_labels) { + throw std::runtime_error("Unexpected external variation labels"); + } + compare(mc.produce(pt, eta, phi, charge, layers, seed), + legacy(argv[1], argv[2], pt, eta, phi, charge, layers, seed, true)); + + auto data = MuonVariationsCalculator::create(argv[1], false, false, "", ""); + compare( + data.produce(pt, eta, phi, charge, layers, seed), + legacy(argv[1], argv[2], pt, eta, phi, charge, layers, seed, false)); + } catch (const std::exception &error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/tests/muon_validation_test.py b/tests/muon_validation_test.py index dd1e991..a64d626 100755 --- a/tests/muon_validation_test.py +++ b/tests/muon_validation_test.py @@ -331,10 +331,16 @@ def compare_lhe_weights(expected, output, max_expected_entries=None, report=None rf, rt = tree(expected) of, ot = tree(output) try: - if "LHEScaleWeight" not in branch_names(ot): - raise AssertionError("output is missing LHEScaleWeight") - if "LHEScaleWeight" not in branch_names(rt): - raise AssertionError(f"expected source is missing LHEScaleWeight: {expected}") + branches = ("LHEScaleWeight", "LHEPdfWeight") + for branch in branches: + if branch not in branch_names(ot): + raise AssertionError(f"output is missing {branch}") + if branch not in branch_names(rt): + raise AssertionError(f"expected source is missing {branch}: {expected}") + input_title = rt.GetBranch(branch).GetTitle() + output_title = ot.GetBranch(branch).GetTitle() + if input_title != output_title: + raise AssertionError(f"{branch} title differs: expected={input_title!r} output={output_title!r}") out_entries = keyed_entries(ot) expected_keys = set(out_entries) ref_entries = {} @@ -348,21 +354,25 @@ def compare_lhe_weights(expected, output, max_expected_entries=None, report=None break missing_keys = sorted(expected_keys - set(ref_entries)) if missing_keys: - raise AssertionError(f"LHEScaleWeight expected source is missing {len(missing_keys)} output event keys") + raise AssertionError(f"LHE weight source is missing {len(missing_keys)} output event keys") common_keys = sorted(expected_keys) - compared_values = 0 + compared_values = {branch: 0 for branch in branches} for key in common_keys: rt.GetEntry(ref_entries[key]) ot.GetEntry(out_entries[key]) - ref_vals = list(getattr(rt, "LHEScaleWeight")) - out_vals = list(getattr(ot, "LHEScaleWeight")) - if len(ref_vals) != len(out_vals): - raise AssertionError(f"LHEScaleWeight length differs for event={key}: expected={len(ref_vals)} output={len(out_vals)}") - for idx, (rv, ov) in enumerate(zip(ref_vals, out_vals)): - compared_values += 1 - if abs(float(ov) - float(rv)) > 1.0e-6 + 1.0e-6 * abs(float(rv)): - raise AssertionError(f"LHEScaleWeight differs for event={key} index={idx}: expected={rv} output={ov}") - line = f"LHEScaleWeight copied from input: common_events={len(common_keys)} values={compared_values}" + for branch in branches: + ref_vals = list(getattr(rt, branch)) + out_vals = list(getattr(ot, branch)) + if len(ref_vals) != len(out_vals): + raise AssertionError(f"{branch} length differs for event={key}: expected={len(ref_vals)} output={len(out_vals)}") + for idx, (rv, ov) in enumerate(zip(ref_vals, out_vals)): + compared_values[branch] += 1 + if abs(float(ov) - float(rv)) > 1.0e-6 + 1.0e-6 * abs(float(rv)): + raise AssertionError(f"{branch} differs for event={key} index={idx}: expected={rv} output={ov}") + line = "LHE weights copied from input: common_events=%d %s" % ( + len(common_keys), + " ".join(f"{branch}={compared_values[branch]}" for branch in branches), + ) if report: report.line(line) else: diff --git a/tests/vjets_ewk_json_test.py b/tests/vjets_ewk_json_test.py new file mode 100644 index 0000000..e15d7e1 --- /dev/null +++ b/tests/vjets_ewk_json_test.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +import argparse +import math +from pathlib import Path + +import ROOT +from correctionlib import CorrectionSet + + +CASES = ( + ("WJetsCorr_collection_ewk", "evj_pTV_EW", "evj"), + ("ZJetsCorr_collection_ewk", "eej_pTV_EW", "eej"), +) + + +def close(left, right): + return math.isclose(left, right, rel_tol=0.0, abs_tol=1e-12) + + +def check_case(directory, stem, correction_name, prefix): + root_file = ROOT.TFile.Open(str(directory / f"{stem}.root"), "READ") + correction = CorrectionSet.from_file(str(directory / f"{stem}.json.gz"))[correction_name] + names = { + "nominal": f"{prefix}_pTV_kappa_EW", + "d1": f"{prefix}_pTV_d1kappa_EW", + "d2": f"{prefix}_pTV_d2kappa_EW", + "d3": f"{prefix}_pTV_d3kappa_EW", + } + histograms = {variation: root_file.Get(name) for variation, name in names.items()} + + for variation, histogram in histograms.items(): + for index in range(1, histogram.GetNbinsX() + 1): + pt = histogram.GetXaxis().GetBinCenter(index) + expected = histogram.GetBinContent(index) + observed = correction.evaluate(pt, variation) + if not close(expected, observed): + raise AssertionError(f"{stem} {variation} bin {index}: {expected} != {observed}") + + def root_value(histogram, pt): + if pt < 100.0: + return 0.0 + lookup = math.nextafter(pt, 0.0) if pt == 6500.0 else pt + return histogram.GetBinContent(histogram.GetXaxis().FindFixBin(lookup)) + + def json_value(variation, pt): + if pt < 100.0: + return 0.0 + lookup = math.nextafter(pt, 0.0) if pt == 6500.0 else pt + return correction.evaluate(lookup, variation) + + for pt in (99.0, 100.0, 6500.0, math.nextafter(6500.0, math.inf), 7000.0): + root_values = [root_value(histograms[variation], pt) for variation in names] + json_values = [json_value(variation, pt) for variation in names] + if any(not close(left, right) for left, right in zip(root_values, json_values)): + raise AssertionError(f"{stem} boundary pt={pt}: {root_values} != {json_values}") + root_weight = 1.0 + root_values[0] + root_uncertainty = 0.5 * abs(root_values[0]) + json_weight = 1.0 + json_values[0] + json_uncertainty = 0.5 * abs(json_values[0]) + for left, right in ( + (root_weight, json_weight), + (root_weight + root_uncertainty, json_weight + json_uncertainty), + (root_weight - root_uncertainty, json_weight - json_uncertainty), + ): + if not close(left, right): + raise AssertionError(f"{stem} weight pt={pt}: {left} != {right}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--source-dir", type=Path, required=True) + args = parser.parse_args() + directory = args.source_dir / "data/vjets-ewk/2023-08-11" + for case in CASES: + check_case(directory, *case) + + +if __name__ == "__main__": + main()