Skip to content

NanoV15 2022-2025 analysis: 2025 from split 2024 MC, V+jets stitch, fit updates, ABCDnn k-fold - #363

Open
zichunhao wants to merge 51 commits into
mainfrom
nanov15-bdt-2022-2025
Open

zichunhao wants to merge 51 commits into
mainfrom
nanov15-bdt-2022-2025

Conversation

@zichunhao

@zichunhao zichunhao commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This branch extends the boosted HH→4b analysis on the NanoV15 skims from 2022-2024 to 2022-2025, and merges current main (faf1dc2). It builds on #344, #345 and #346, which brought in earlier versions of the transformer, ABCDnn and fom_fast code; the overlapping changes from main's later PRs (#349, #350, #352, #353, #356, #358) were reconciled as listed below.

What it adds

  • 2025 from the 2024 MC (--split-shared-mc): 2024 and 2025 each take one event-parity half of the 2024 MC. Each half is renormalized by its actual generator-weight fraction (1/f) and scaled to its era's luminosity, and the two halves enter the fit as independent eras (no lumi scale-up). TrainBDT and the ABCDnn inference pickles use the same split.
  • V+jets and Higgs backgrounds: the 2024/2025 Bin-PTQQ samples are open-ended, so by default only Bin-PTQQ-100 is loaded; --vjets-stitch --vjets-stitch-mode range --override-tag <reskim> stitches all four in LHE V pT ranges. The 2022-2023 W→qq cross sections are halved (each W charge was generated twice), with a load-time rescale for older skims. 2022/2022EE load one ttH(bb) and one VBF H(bb) production instead of two (= exact selector).
  • Corrections: measured 2024 and 2025 trigger efficiency maps. Placeholder GloParT-v3 JMS (1 ± 0.05) and one-sided JMR (1.15) on the MC mass at load time (--no-glopartv3-jmsr to turn off), with the shifted-mass columns named after the tagger's mass. New TXbb SF options (--an184-txbb-sfs, --borrow-txbb-sfs), and --no-systematics for nominal-only templates.
  • Datacards and fits: 2022-2024 and 2022-2025 cards. 2025 JER, JMS and JMR nuisances (JMS/JMR only for templates made with the placeholder), and 2024/2025 JMS/JMR shifts from the full 2024 MC (--jmsr-2425-fullmc). Shape shifts into empty nominal bins are kept (--shape-empty-bin-floor). Guards for the qcd_eff seed and empty-process lnN. The four v15 theory-acceptance shape systematics are dropped (broken templates; the inclusive lnN theory uncertainties stay). F-test helpers (ftest_limits.py, env-driven run_ftest_hh4b.sh).
  • FOM scan: opt-in options (--fom-b-floor, --fom-scan-bin3, --fom-vbf-floor-at-band, --fail-txbb-floor, --txbb-wp-bin2/3, --fom-fail-match, --save-ntuples) whose defaults keep the old behavior. The slim-cache key gets one token per change to the loaded frames, and the cache re-evaluates the ttbar BDT-shape SF with the scanned model.
  • ABCDnn and BDTs: k-fold ensemble training and combination (train.py --n-folds/--fold, apply.py --ensemble-dir) and a faster loader. Three trained 2022-2025 models: the MC QCD BDT 26Sep01_v13_glopartv3_v15_2225, and the data-driven QCD BDTs ABCD_26Sep11_v15_2225_2025ew_jet1txbb_abcd_smvbf_bal_xbb (single network) and ABCD_26Oct01_v15_2225_kf20median_abcd_smvbf_bal_xbb (k=20 ensemble median).
  • Skimmer and inputs: LHE V pT and per-V-pT-bin weight sums for V+jets; GloParT-v2 branches and an OR-tagger preselection; empty/low-yield chunks handled; guards for a missing LHEScaleWeight and malformed diboson gen records; AK4 jets are no longer assigned to both AK8 jets. 2024 file-index fixes (global xrootd for bad FNAL copies) and the 2024 ggHH kl=1 TrimmedSyst production.

Reconciliation with main

  • D1 split weights: kept the exact 1/f renormalization in place of main's fixed ×2 (Fix 2025 template normalization in the 2024 MC 50/50 split #358), so weights are scaled once. Main's split helpers and tests/test_mc_split_scaling.py are unchanged, and --split-shared-mc is defined once.
  • D2 diboson: main's exact ZZ?/WW?/WZ? selectors (Support 2024/2025 eras (NanoV15) in postprocessing and datacards #349), which drop the decay-specific ZZ/WW/WZ sub-samples that 2022-2023 used to add on top of the inclusive ones. New slim-cache token __dibosonexact for 2022-2023BPix only.
  • D3 2025 data: kept JetMET_Run2025D (the v15 skim has its trigger columns, and LUMI["2025"] includes it).
  • D4 trigger SFs: the real 2024/2025 maps, not main's 2023BPix redirect. Main's both-files fallback check is kept.
  • D5 file reading: kept the footer check and per-sample filters/columns (needed by the V+jets stitch and the empty column-less parquet files).
  • D6 read failures: one read per file. A failure raises an error that names the file and sample; samples are never skipped silently.
  • D7 templates: main's per-sample loop, fixed so that a sample is freed only after the last year (otherwise later years got empty templates). --no-systematics is honored.
  • D8 JMS/JMR columns: renamed for any tagger's mass. Main's glopart-v3-only rename is dropped, so no columns are duplicated.
  • D9 2025 JER/JMS/JMR: kept the conditional 2025 entries. The branch's 2024/2025 TXbb/ttbar-Xbb placeholder loop is dropped because main's static entries give identical cards.
  • D10 QCD MC: HT≥400 bins only, now for every tagger (both QCD-4Jets_HT- and QCD_HT- spellings).
  • D11 QCD selector: main's "QCD_HT-" literal.
  • D12 PlotFits: --year argument (default 2022-2025) sets the luminosity label.

Validation

  • Tests: main's suite gives 286 passed and 5 skipped (the integration tests, which need a local fixture). With a fixture from scripts/make_test_data.py on the v12 skim, all 291 pass; the integration test's argument set gained this branch's five new options at their parser defaults.
  • pre-commit (main's config) passes with --all-files.
  • Templates: 2022, 2024 and 2025 templates from this merge and from the branch tip were made with identical production arguments. 2024 and 2025 are bit-identical, and so are all data histograms. In 2022 only zz and other-diboson change (D2): their fail-region MC yield drops by 3.3% and 53%, and their pass categories are unchanged.
  • Datacards: a 2022-2025 card set from the current templates is identical with and without the dropped placeholder loop, and identical to the branch tip's: text cards, model.pkl, and the workspace object by object.

Follow-ups

  • A separate commit on this branch changes the skimmer's good_ak8jets default regressed mass to ParT3massX2p, with a fallback to the PNet legacy mass when the NanoAOD has no GloParT-v3.
  • D2 changes the 2022-2023 diboson backgrounds, so the next template/card/limit rerun will differ from the current results, and the 2022-2023BPix FOM slim caches are rebuilt.
  • Main's chunked BDT inference: with --bdt-inference-chunk-size > 0, _build_and_score_bdt_events returns a reset index, so the later .loc[mask_negative_vbf] misaligns on frames whose index is not 0..n-1 (for example the 2024/2025 split halves). The default (0) is unaffected.
  • Since the move to main's template loop, the per-region cutflow CSVs and per-year template plots are no longer written, and the template pickles are larger (empty rows from the shared sample axis). Template values are unchanged.

…tion criteria, and enhance control plot variables
gen_selection_VV hard-indexed daughters.pdgId[:, 1, 1], assuming every event
has 2 gen V bosons each with 2 daughters. Some amcatnloFXFX diboson samples
(e.g. WWtoLNu2Q-1Jets-4FS) have events with <2 selected Vs -> "index out of
range" -> skim crash. Pad daughters to [., 2, 2] with PAD_VAL before indexing:
a no-op for well-formed events (verified byte-identical output on 4934/5000
events of the failing file) and PAD_VAL (no PDGID match) for malformed ones.
The FNAL dcache copy (cmsdcadisk.fnal.gov) of the 2024 TTtoLNu2Q NanoAODv15
files is unreadable: jobs failed 100% with coffea "cannot unpack NoneType"
(all input files skipped at pre-processing -> no chunks). The dataset itself
is fine at other sites (verified via cms-xrd-global: 624239 events, GloParT3
branches present), so route the 790 2024 TTtoLNu2Q file reads through the
global redirector. Only those paths changed.
Same bad-FNAL-dcache-staging issue as TTtoLNu2Q (coffea NoneType/no-chunks):
2024 QCD-4Jets HT-40to70 and HT-70to100, and GluGluHHto4B kl-0p00 also had
unreadable cmsdcadisk copies. Route those 3 datasets' file reads through
cms-xrd-global (other working 2024 MC left on cmsdcadisk).
FNAL dcache copy of d2138e34-...root is corrupt (_lzma.LZMAError: Corrupt
input data), holding the skim job. Same bad-copy pattern as TTtoLNu2Q /
QCD_HT-40to70 / QCD_HT-70to100 / GluGlutoHHto4B_kl-0p00; route this dataset
through the global redirector to pick a good replica.
…global

All 3 skim jobs failed with empty-chunks after preprocessing (coffea
'cannot unpack non-iterable NoneType' - the no-chunks bad-copy variant);
cmsdcadisk RunIII2024Summer24NanoAODv15 copies unreadable. Route through
the global redirector, same pattern as the other 2024 bad-copy datasets.
Two failure modes on chunks with ~0 selected events, both of which held the
job instead of finishing:

1. bbbbSkimmer.getBDT KeyError (e.g. 'bbFatJetPt_JES_up'): a chunk with no fat
   jets never has its JEC/JMSR-shifted fat-jet branches built, but the jshift
   loop still iterates the shifts. Wrap getBDT in try/except KeyError and skip
   the shifted BDT for such a chunk with a warning (re-raise for the nominal
   shift, which must always be buildable). The chunk fails the >=2-fat-jet
   selection anyway, so nothing is written for it.

2. run.py pd.read_parquet(dir) 'Unsupported cast from bool to null': a low-yield
   chunk can write an all-null boolean column that pyarrow cannot unify with
   another chunk's typed column when reading the whole dir at once. Read the
   chunk files individually and concat in pandas (which upcasts), recovering the
   events that did pass; if no chunk wrote output, warn and skip parquet/root
   (the pickle with the cutflow is always written, so the job exits cleanly).

Fixes the held ZZto4L-1Jets (all eras) and TTto4Q skim jobs.
…sent

Some PFNano files don't store the LHEScaleWeight branch, so events[0].LHEScaleWeight
raised AttributeError in get_scale_weights (reached for TTto* and HH signal). Return
None (already the empty-branch behaviour, handled by add_weights) when the field is
missing, so the skim job doesn't crash. Surfaced on a 10-event TTto4Q chunk after the
empty-chunk getBDT/parquet fixes (7c667fc) let it get further.
Byte-identical to v13_glopartv3.py except the four tagger-column
references, swapped to ParT-v2 (ParT3massX2p->ParTmassVis,
ParT3TXbb->ParTTXbb). This makes the GloParT-v2 and v3 BDTs differ only
in the tagger (padding-mask feature handling otherwise identical to v3),
enabling the controlled same-skim v2-vs-v3 comparison.
v15 NanoAOD carries FatJet_globalParT2_* branches: add the ParT2
tagger/mass fields to extra_vars and jmsr_vars (ParTmassVis/Res) so the
v13_glopartv2 / AN-23-151 selection can run on the v15 skim (same-skim
v2-vs-v3 control). Keep glopart-v3 as the common loose (TXbb>0.3)
preselection tagger; the OR presel now includes ParT2 and ParT3.
- LUMI["2022-2024"] = 171388.6 pb-1 (2022+2022EE+2023+2023BPix+2024)
- jmsr_vars: add bbFatJetParT3massX2p (glopart-v3 regressed mass) beside
  the v2 ParTmassVis (was duplicated)
- sig_keys_vbf: kvm2p12 (CV=-2.12, wrong sign) -> kv2p12 (CV=+2.12),
  matching samples_run3 (VBFHHto4B_CV-2p12)
- load_run3_samples: for non-v2 taggers, swap the hardcoded ParTmassVis
  JMSR-shifted mass columns for the study tagger's regressed-mass name
  (glopart-v3 -> ParT3massX2p) so a --templates load doesn't error
- get_templates: drop sig_keys absent from events_dict (older skims lack
  some benchmarks / use CV-m2p12 vs CV-2p12) to keep downstream consistent
- --an184-txbb-sfs: AN-23-184 measured GloParT-v3 Zbb SFs (WP<->purity);
  --borrow-txbb-sfs: v2 Zbb SFs as a v3 proxy (stopgap)
- gate all systematic template-building on args.systematics so
  --no-systematics builds nominal-only templates (no missing-column errors)
- select JEC/JMSR-shifted BDT features by their jshift-suffixed names via
  get_var_mapping (XGBoost applies by column position)
- CreateDatacard: --year 2022-2024 + glopart-v3 choice; explicit era lists
  (hh_years carries a 2025 placeholder); 2024 uncorr_years handling; drop
  the 4 theory-ACCEPTANCE shape systs (ps_fsr/isr/QCDScaleacc/PDFacc) that
  are broken on the v15 skim (nan/degeneracy) -- inclusive lnN theory kept;
  VBF kv2p12 label
- PlotFits: VBF kv2p12 label
- corrections: 2024/2025 trigger eff borrows 2023BPix (latest era), not 2023
- gen_selection_V: add GenVLHEPt, the pT of the status-2 LHE W/Z. This is
  the variable the 2024 {W,Z}to2Q-2Jets_Bin-PTQQ-X generator filter cuts on
  (open-ended pT > X), needed to stitch those samples exactly. NanoAOD's
  LHE_Vpt is built from leptons and is 0 for V->qq, and the last-copy GenVPt
  is shifted by the parton shower.
- bbbbSkimmer: only use the JMSR / FatJet skim variables that exist in the
  input. The central 2024 Summer24 NanoAODv15 samples carry only
  globalParT3 branches, so the GloParT-v2 fields added for v15 made the
  skimmer crash on them.
- make_filelists_v15: a 2024 MC sample can now list several datasets of
  the same process; their files are merged into one sample, so the skimmer
  normalizes them together (sum of genWeights over both), which is the
  effective-luminosity combination when |genWeight| is the same.
- GluGlutoHHto4B kl-1p00: add the TrimmedSyst production (78.4M events) to
  the PowhegBugFix one (2.0M). Same gridpack apart from the trimmed PDF
  reweighting list; same genWeight (0.03309), 103 PDF / 9 scale / 4 PS
  weights and LHE m(HH) as PowhegBugFix and the 2022 sample. Indexed via
  the global redirector (not fully replicated at FNAL disk).
- kl-0p00/2p45/5p00 TrimmedSyst are NOT used: their gridpacks are copies of
  the kl-1p00 one (powheg.input chhh = 1.0), and the samples have the SM
  genWeight and m(HH) instead of the labelled kl (chi2/ndf vs 2022 of
  1.8e4-1.5e5 / 10, PowhegBugFix 5-16 / 10).
- Vto2Q 2024 comment: the Bin-PTQQ samples are open-ended, not exclusive.
For Wto2Q-/Zto2Q- datasets (those with GenVLHEPt), the pickle totals now also
hold the sums per bin of the status-2 LHE W/Z pT, over all generated events
before any selection (10 GeV bins on [0, 2000) plus under/overflow, so the
Bin-PTQQ thresholds and the 2022-2023 PTQQ edges are bin edges):
- np_nominal_lhevpt: the per-bin np_nominal (sum of the norm-preserving
  weights, genWeight x pileup x PS), plus np_nominal_lhevpt_sumw2;
- genweight_lhevpt (sum of genWeight) and nevents_lhevpt (raw counts);
- np_<syst>_lhevpt for every norm-preserving variation (pileup, ISR, FSR
  up/down), so each variation can be normalised per bin too.

They allow the open-ended 2024 Bin-PTQQ samples to be stitched and normalised
per LHE V pT bin (without relying on the samples' cross-section ratios) and
each sample's threshold to be checked on all generated events. The skim can
only keep preselected events, so these sums cannot be rebuilt afterwards.
Parquet output and all existing totals are unchanged (checked byte for byte
on V+jets and HH chunks); other datasets get no new keys. The weighted sums
are float64 even for an empty chunk, so coffea's accumulate always works.
The 2024 TrimmedSyst ggHH productions labelled kl-0p00, kl-2p45 and kl-5p00
were generated with kl = 1 (their gridpacks are copies of the kl-1p00 one,
powheg.input chhh = 1.0), so they are SM HH. They are indexed only to study
the bug (e.g. m(HH)), kept separate from the analysis, and never to be used
as kappa_lambda points:
- make_filelists_v15 / nanoindex_v15: new 2024 category "HHTrimmedSyst"
  (714 + 658 + 675 files, global redirector) with keys
  "TrimmedSyst-GluGlutoHHto4B_kl-<X>_kt-1p00_c2-0p00_TuneCP5_13p6TeV". The
  "TrimmedSyst-" prefix keeps every analysis selector (prefix match on
  "GluGlutoHHto4B_kl-...") from loading them; no existing entry changes.
- xsecs: the three keys get the SM ggHH cross section, which is what they
  contain (the skimmer scales weights by xsec x lumi, and the loader needs
  an entry).
- submit_configs/skimmer_reskim_2024_hhtrimmedsyst.yaml: 2024 HHTrimmedSyst,
  10 files per job (206 jobs), to be submitted with its own skim tag.
…3 W->qq xsecs

The 2024/2025 {W,Z}to2Q-2Jets_Bin-PTQQ-X samples are open-ended (PTQQ > X), so summing them
over-counted V+jets 2-4x above 200 GeV. By default only Bin-PTQQ-100 is loaded; --vjets-stitch
--vjets-stitch-mode range (with --override-tag for the re-skim) stitches all four in LHE V pT
ranges. The 2022-2023 W->qq cross sections are halved because their MadGraph cards generate each
W charge twice; older skims are rescaled at load time with a warning.
…-v3 JMS/JMR

2022 and 2022EE hold two productions each of ttH(bb) and VBF H(bb), and the prefix selectors
loaded both; a case-sensitive exact selector ("=" prefix) now picks one. PostProcess applies
placeholder JMS (1 +- 0.05) and one-sided JMR (1.15, resolution scaling) to the GloParT-v3 mass of
all MC except QCD at load time (--no-glopartv3-jmsr to turn off), and CreateDatacard adds the 2025
JMS/JMR nuisance. Both change the FOM slim-cache key.
… loader

Adds opt-in FOM and region options (--fom-b-floor, --fom-scan-bin3, --fom-vbf-floor-at-band,
--fail-txbb-floor, --txbb-wp-bin2/3, --fom-fail-match, --save-ntuples) whose defaults keep the
previous behaviour. The slim cache now re-evaluates the ttbar BDT-shape SF with the scanned model
(new __ttbdtsf key). ABCDnn training gets --fast-loader, and prepare_bdt_data.py more weight
columns.
…, robustness fixes

Adds placeholder nuisances for the 2025 JER and the 2024/2025 TXbb and ttbar Xbb SFs. Shape shifts
into bins with zero nominal yield are now kept (--shape-empty-bin-floor), and the 2024/2025 JMS/JMR
shifts come from the full 2024 MC (--jmsr-2425-fullmc). Also guards the qcd_eff seed and the
empty-process lnN conversion, freezes the blinded fail-QCD bins consistently in the F-test, and
lets run_ftest_hh4b.sh take its inputs from environment variables.
The Orchestration section listed wrapper scripts, nrp/ and plotting scripts that are not committed,
and linked a missing notes/ABCDnn.md. It is replaced by a short section on the committed
versions/abcd_smvbf.yaml composition config; the example is labelled as a single-era (2022) run.
train.py --n-folds K --fold i keeps the held-out test set, splits the remaining events into K
fixed random folds and trains member i on the other K-1 folds, validating on fold i (seed base+i).
apply.py --ensemble-dir combines the members per event (median, mean, std, q16, q84 of the
weights, TF and purity), saves every member's weights and the member templates with the per-bin
(q84-q16)/2 band, and refuses incomplete or inconsistent members. prepare_bdt_data.py accepts the
ensemble weight columns. Without these options the behaviour is unchanged.
ABCD_26Oct01_v15_2225_kf20median_abcd_smvbf_bal_xbb: same recipe, events and split as
ABCD_26Sep11_v15_2225_2025ew_jet1txbb_abcd_smvbf_bal_xbb, with the data-driven QCD weighted by the
per-event median of the 20 ensemble members (w_QCD_A_median).
…legacy fallback

good_ak8jets now uses ParT3massX2p (the analysis mass) for the mSD > 50 OR mReg > 0 requirement, falling back to particleNet_mass_legacy when the NanoAOD has no globalParT3 branches. On a 2024D JetMET sync file this changes ak8_numjets by one event and leaves the skim unchanged.

@cmantill cmantill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, left two minor comments.

Also: can you look into the automated review comment on --bdt-inference-chunk-size > 0. " _build_and_score_bdt_events returns a reset index, so the later .loc[mask_negative_vbf] misaligns on frames whose index is not 0..n-1 (for example the 2024/2025 split halves). The default (0) is unaffected."


# GloParT-v3 regressed mass; fall back to the PNet legacy mass if the NanoAOD has no globalParT3
if mreg_str not in fatjets.fields:
mreg_str = "particleNet_mass_legacy"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that we fall back to particleNet for 2022 and 2023? Does our nanoAOD for 2022 and 2023 have globalParT3?

# find closest to second fatjet among the remaining jets
dr1 = jets_away_wo0.delta_r(second_fatjet)
closest_idx1 = ak.argmin(dr1, axis=1, keepdims=False)
jet_near_fatjet1 = jets_away_wo0[ak.singletons(closest_idx1)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we tested the impact on signal?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants