Repository navigation
Conversation
The HH->bbWW single-lepton shapes hold, in the file for mass MX, a signal region for
every mass (SR_SL_M300 ... SR_SL_M1000); only SR_SL_M${MX} belongs to the MX limit.
Listing those directories as categories puts all ten into every card as separate bins,
and nothing reports it, since every file has all of them.
A configuration can now map a category to its input directory, with the model
parameters substituted at read time:
input_categories:
SR_SL/res2b: SR_SL_M${MX}/res2b
The category keeps one name for every mass -- bin names, per-category card
directories, Process/Uncertainty scoping -- and only the read is redirected, in the
maker's four histogram-path builders. A map entry for a category that is not
configured is an error; configurations without a map read exactly as before.
This covers cards that read the merged shapes directly. rebin_2d.py does not apply the
map, so a card with a `preprocess:` step still needs its input under the category name.
Checked on the real SL shapes (v2605a_TwoStageDNN_Shapes_v3, MX=500, Run3_Early, no
preprocessing): the card has the six bins {e,mu} x SR_SL/{res2b,boosted,recovery}, and
TT and signal in every bin equal the sum of the four sub-eras' SR_SL_M500 inputs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ositivity Two changes to what a candidate binning has to satisfy, both about backgrounds that are present but not measured. min_bin_bkg_each_neff requires every non-negligible background to have at least this many effective MC entries in every bin. min_bin_bkg_neff already existed but constrains only the *summed* background, so it is satisfied by any one well-measured process -- TT alone was covering for DY at N_eff 0.1. The sliced axis has carried the per-process arm for a while (min_slice_bkg_each_neff); the mass axis, where essentially all fit bins live, had no per-process test at all. Failing bins are merged into their lower-signal neighbour, so the resolution given up is where the signal is thinnest. Defaults to 0.0, which is the previous behaviour exactly. Positivity is no longer exemptable, in either _bin_passes or _slice_passes. `exempt` exists so that a negligible process cannot veto every candidate split -- it is about the *magnitude* floor, not about sign. Letting an exempt process go negative is not merely an ugly bin: resolveNegativeBins in dc_make/maker.py rejects a negative bin holding >= signalFractionForRelevantBins of the signal unconditionally, ignoring allow_negative_bins_within_error, so the datacard fails to build at all. Observed at m1000 muMu/SR/recovery_dnn1, where ST was 4.24% of the slice -- just under min_bkg_frac 0.05, hence exempt -- and came out at -0.567 +- 0.789, killing the production. Measured on HH->bbWW Run3_Early: with n_slices 4 and bkg_per_bin 1.0, turning the new gate on at 2.0 gives a geometric-mean expected limit 0.988x the production binning -- better on the limit *and* on every validity metric at once. Per-process bins below N_eff 1 fall 1.5% -> 0.2%, below N_eff 2 fall 3.2% -> 1.1%, the median DY N_eff nearly doubles 16.3 -> 31.8, and DY bins below N_eff 4 fall 29.2% -> 18.2%, for 3.7% of the fit bins (6220 -> 5989). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We5E9ZkM9tanReeRE2WSSR
Everything that decides *where* a boundary goes -- slice finding and the gates it applies, which processes are minor enough to exempt, the figures of merit, and the histogram plumbing -- moves out of bin_opt_2d/rebin_2d.py into common/binning_core.py, unchanged. The cell tables the HME-box search is built on need the same answers to the same questions, and importing them from one place is what keeps a second copy from drifting. What stays behind in rebin_2d.py is the part that knows what a "bin" is, plus BINNING_DEFAULTS and load_binning_config(), whose "unknown key raises" contract only works if the file that reads the knobs states its own valid keys. No behaviour change: the moved code is byte-identical to the lines it came from, and the only other change to rebin_2d.py is its import header. Carved out of e205797 on Binning_2D_DP, without the 1D binner that commit also added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…olved ones Once a category list is expanded against a rebinning record (the base "SR/res2b" becomes "SR/res2b_hmebox0"), the map's keys -- the names the configuration writes -- no longer appear in self.categories, and every key read as unknown. Main does not expand categories yet, so nothing is affected here; the HME-box binning PR adds the expansion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
strategy: hme_box cuts one contiguous HME window per category and bins the DNN inside it, instead of cutting the DNN into slices and binning HME inside each. Everything outside the box is dropped, so the category becomes a single datacard bin (SR/res2b -> SR/res2b_hmebox0). Box and DNN binning are chosen together: a window is scored by the best DNN binning it admits (_box_value), not by its own integrated significance, which would only widen it to swallow signal and ignore the shape the fit uses. The DNN binning inside a box is an exact partition search (partition_dp) against the same Asimov Z^2 the greedy slice boundaries use, with the bin gates as a hard mask; _best_count() takes the best-scoring count up to max_bins_per_slice, and dp_min_bin_gain gives back any bin whose split raised Z^2 by less than that fraction of the category's total. The windows are scanned on a box_stride grid inside the box_signal_quantile range of the signal, then refined within +-box_refine. min_bkg_frac is judged once over the whole box, and the gates are those of _bin_passes, including min_bin_bkg_each_neff and the never-exempt positivity. common/binning_dp.py holds the prefix-sum cells that make this affordable (every window and candidate bin is an O(1) lookup instead of a ROOT integral), the vectorised box tables, and check_box_mask(), which re-derives a sample of the tables with the canonical _bin_passes on every run so the two spellings of the gate cannot drift. binning.json records the box layout as "y_range"/"x_ranges" beside the DNN-slice layout's "x_range"/"y_ranges", and replays either; both strategies now also record the objective (Z, Z^2 per slice, bin count) recomputed from the written ranges. CreateDatacardsTask labels the shape plots by the surviving axis read from that record, so box panels are labelled by the DNN rather than by HME. Default stays "greedy", which is unchanged by this commit. Carved out of Binning_2D_DP (92e0010 and the parts of 39f9511, 88f612d, 14287b5 it uses), without the "dp" strategy, the budget allocator, axis alternation, binning_exclude_processes, fill_negative_bins, fitted tails, per-process N_eff floors, exempt_scope, the dc_make base-category expansion and the plot book. Verified identical to Binning_2D_DP on v2605a_DNNOutputs_v3 Run3_Early, MX 500 and 1000: binning.json equal apart from the dropped knobs (all at their defaults there), and every one of 188928 hme_box and 755712 greedy rebinned histograms equal in edges, contents and errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The helpers moved to common/binning_core.py were moved to share them with a 1D binner that is not part of this change, so they go back where they were on main, unchanged except _slice_passes, which keeps the positivity rule. binning_dp.py no longer imports from the binner: the one figure of merit it needs is passed to build_cells, as the gate already is passed to check_window_mask. The development-time checks (parity_check, the partition self-test) and the Cells methods only they used are dropped; check_window_mask still re-derives a sample of every window's tables against _bin_passes and significance() on each run. The method is not specific to HH->bbWW's HME: it cuts one window on y and bins x inside it, whatever the two axes hold. So the strategy is y_window (was hme_box), its knobs are window_stride, window_refine and window_signal_quantile, and the prose describes x and y with HH->bbWW as the example. greedy stays the default. Checked against Binning_2D_DP on Run3_Early, v2605a_DNNOutputs_v3, MX 500 and 1000: the rebinned shapes are identical for y_window (188928 histograms) and greedy (755712), and binning.json is identical outside the knobs block, the reference's old names mapped to the new. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A window on y with x binned inside it is one orientation of a general method, so the strategy is now "window", and window_axis (y, the default, or x) says which axis the window is cut on; the other is the one binned inside it. --window-axis sets it from the command line, and any other value is refused. The search is written with the window on y. For window_axis x, build_cells builds the cells transposed, the same search runs, and the result is returned in the input's axes: a selection on x with bins along y, the layout the greedy slices already use, so the writer, binning.json, replay and plot labels need no change. Checked on Run3_Early, v2605a_DNNOutputs_v3, against Binning_2D_DP (the reference the method was measured on): window with window_axis y reproduces its rebinned shapes exactly at MX 500 and 1000 (188928 histograms), and greedy does too (755712). For window_axis x, every 2D input at MX 500 was transposed and rebinned with window_axis x: the binning is the same as window_axis y on the original input, and the 94464 shapes agree, 66464 bit-identical and the rest within 1e-9 relative (the transposed sums round differently). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comments and docstrings only: the code is unchanged (the syntax tree without docstrings is identical to the previous commit's). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rebin_2d.py reads each base category from where input_categories places it for the
file's mass (e.g. SR/res2b from SR_SL_M${MX}/res2b) and writes it under the category's
own name, so binning.json and the rebinned shapes are keyed the same way for every mass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A card that reads rebinned shapes lists the written names (SR/res2b_hmebox0) while its input_categories map is keyed by the base category (SR/res2b) that rebin_2d.py reads. The maker now accepts a key that is a configured category or the base of one, and still refuses any other, which catches typos. Sliced names are read under their own names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The merged input for one mass carries every signal sample at every mass, but a rebinned file is only read for its own mass, since input_file_pattern names it. Other points' signal histograms, nominal and systematic, are now skipped, matched from the configuration's signal name patterns; backgrounds and data are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… limit
Configurations built from different inputs (the single- and double-lepton channels)
cannot be summed histogram by histogram like a group era's sub-eras; they are combined
card by card, in one law invocation:
- datacard_config / datacard_tag on StatInferenceTask: a task can run a named
configuration, with its products under <version>/<tag>/. An empty tag keeps every
path as before.
- A combination configuration (`members: {name: {config, hists_version, user_custom}}`).
member_req() runs each member's chain under its own tag and histograms, and
CombinedResonantLimitsTask combines their headline cards per mass, labelled by member,
after checking they share the mass points and no bin.
- A limit_plots curve may be a list of globs; in a combination each names its member,
"SL:${ERA}/e/*.txt", and each member's own limit_plots are drawn as well.
ResonantLimitsTask's per-mass combineCards and publish steps are factored out to be
shared. Impacts on a combination are refused for now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@cms-flaf-bot please test |
|
pipeline#15865813 started |
|
Heads-up: #32 (multi-era change set C) changes the law tasks this PR edits (
For the SL+DL combination this suggests one card workflow per member configuration. Each member's identity would go into
This PR already conflicts with |
This PR is based on #24 and #25
The combination of SL and DL requires the previous PRs to finish first, and then this adds support of a two-configuration mixing in the plotting wrappers and limit setting