diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aada7b4d2..99e8a17d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -315,7 +315,7 @@ jobs: - name: bsm rabbit setup run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run_python.sh scripts/rabbit/setupRabbit.py - -i $HIST_FILE --lumiScale $LUMI_SCALE --excludeProcGroups QCD WtoNMu_10 WtoNMu_50 --breitwignerWMassWeights + -i $HIST_FILE --lumiScale $LUMI_SCALE --addBSM WtoNMu_5 --breitwignerWMassWeights --postfix bsm -o $WREMNANTS_OUTDIR - name: bsm rabbit fit @@ -772,13 +772,13 @@ jobs: run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_plot_hists.py --config utilities/styles/styles.py $WREMNANTS_OUTDIR/ZMassWLike_eta_pt_charge/fitresults_uncorr.hdf5 --result uncorr --title CMS --titlePos 1 --subtitle Preliminary - -o $WEB_DIR/$PLOT_DIR --legCols 6 --yscale 1.2 --prefit --legCol 6 --legSize large -m Basemodel + -o $WEB_DIR/$PLOT_DIR --legCols 6 --yscale 1.2 --prefit --legCol 6 --legSize large -m BaseMapping - name: wlike postfit plot run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_plot_hists.py --config utilities/styles/styles.py $WREMNANTS_OUTDIR/ZMassWLike_eta_pt_charge/fitresults_uncorr.hdf5 --result uncorr --title CMS --titlePos 1 --subtitle Preliminary - -o $WEB_DIR/$PLOT_DIR --legCols 6 --yscale 1.2 --rrange 1.03 0.97 --binSeparationLines -2.2 -2.1 1.2 1.3 -m Basemodel + -o $WEB_DIR/$PLOT_DIR --legCols 6 --yscale 1.2 --rrange 1.03 0.97 --binSeparationLines -2.2 -2.1 1.2 1.3 -m BaseMapping - name: wlike rabbit impacts run: >- @@ -873,13 +873,13 @@ jobs: run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_fit.py '$WREMNANTS_OUTDIR/ZMassDilepton_etaAbsEta_mll/ZMassDilepton.hdf5' - -t -1 --computeHistErrors --saveHists --saveHistsPerProcess --computeVariations --doImpacts -m Basemodel -m Project ch0 mll + -t -1 --computeHistErrors --saveHists --saveHistsPerProcess --computeVariations --doImpacts -m BaseMapping -m Project ch0 mll -o '$WREMNANTS_OUTDIR/ZMassDilepton_etaAbsEta_mll/' - name: dilepton postfit mll-etaAbsEta plot run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_plot_hists.py --config utilities/styles/styles.py - $WREMNANTS_OUTDIR/ZMassDilepton_etaAbsEta_mll/fitresults.hdf5 -o $WEB_DIR/$PLOT_DIR -m Basemodel + $WREMNANTS_OUTDIR/ZMassDilepton_etaAbsEta_mll/fitresults.hdf5 -o $WEB_DIR/$PLOT_DIR -m BaseMapping --yscale 1.4 --binSeparationLines -0.6 0.0 --titlePos 1 --title CMS --subtitle Preliminary - name: dilepton postfit mll plot @@ -1145,12 +1145,12 @@ jobs: - name: dilepton prefit plot run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_plot_hists.py $WREMNANTS_OUTDIR/ZMassDilepton_ptll_yll/fitresults.hdf5 - --config utilities/styles/styles.py -o $WEB_DIR/$PLOT_DIR --yscale 1.2 --prefit --title CMS --subtitle Preliminary -m Basemodel + --config utilities/styles/styles.py -o $WEB_DIR/$PLOT_DIR --yscale 1.2 --prefit --title CMS --subtitle Preliminary -m BaseMapping - name: dilepton postfit plot run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_plot_hists.py $WREMNANTS_OUTDIR/ZMassDilepton_ptll_yll/fitresults.hdf5 - --config utilities/styles/styles.py -o $WEB_DIR/$PLOT_DIR --yscale 1.2 --title CMS --subtitle Preliminary -m Basemodel + --config utilities/styles/styles.py -o $WEB_DIR/$PLOT_DIR --yscale 1.2 --title CMS --subtitle Preliminary -m BaseMapping - name: dilepton plotting ptll run: >- @@ -1190,7 +1190,7 @@ jobs: run: >- scripts/ci/run_with_singularity.sh scripts/ci/setup_and_run.sh rabbit_plot_hists.py --config utilities/styles/styles.py $WREMNANTS_OUTDIR/ZMassDilepton_ptll/fitresults_from_ZMassWLike_eta_pt_charge.hdf5 --result asimov_uncorr --postfix uncorr_fromWLike - -o $WEB_DIR/$PLOT_DIR --legCols 1 --yscale 1.2 --rrange 1.03 0.97 --title CMS --subtitle Preliminary -m Basemodel + -o $WEB_DIR/$PLOT_DIR --legCols 1 --yscale 1.2 --rrange 1.03 0.97 --title CMS --subtitle Preliminary -m BaseMapping gen: # The type of runner that the job will run on diff --git a/rabbit b/rabbit index 79953d0c7..99e39bffa 160000 --- a/rabbit +++ b/rabbit @@ -1 +1 @@ -Subproject commit 79953d0c7581383db2d614109f2ddc4dfcddfa46 +Subproject commit 99e39bffa108c5aa25e770f452823727a6a95ba2 diff --git a/scripts/corrections/make_theory_corr.py b/scripts/corrections/make_theory_corr.py index d7bcf0a8a..9b3d8248f 100644 --- a/scripts/corrections/make_theory_corr.py +++ b/scripts/corrections/make_theory_corr.py @@ -250,6 +250,7 @@ def main(): eventgen_procs = [ "WtoNMu_MN-5-V-0p001", "WtoNMu_MN-10-V-0p001", + "WtoNMu_MN-30-V-0p001", "WtoNMu_MN-50-V-0p001", ] diff --git a/scripts/histmakers/mw_with_mu_eta_pt.py b/scripts/histmakers/mw_with_mu_eta_pt.py index f2fc8bb11..fac876061 100644 --- a/scripts/histmakers/mw_with_mu_eta_pt.py +++ b/scripts/histmakers/mw_with_mu_eta_pt.py @@ -76,6 +76,11 @@ default=common.get_default_mtcut(analysis_label), help="Value for the transverse mass cut in the event selection", ) +parser.add_argument( + "--mtGenCut", + action="store_true", + help="Apply mt cut at generator level", +) parser.add_argument( "--vetoGenPartPt", type=float, @@ -675,11 +680,7 @@ def build_graph(df, dataset): logger.info(f"build graph for dataset: {dataset.name}") results = [] isW = dataset.name in common.wprocs - isBSM = dataset.name in [ - "WtoNMu_MN-5-V-0p001", - "WtoNMu_MN-10-V-0p001", - "WtoNMu_MN-50-V-0p001", - ] + isBSM = dataset.name.startswith("WtoNMu") isWmunu = isBSM or dataset.name in [ "WplusmunuPostVFP", "WminusmunuPostVFP", @@ -785,7 +786,7 @@ def build_graph(df, dataset): ) cols = [*cols, "run4axis"] - if isBSM: + if isWmunu: # to compute inclusive cross section unfolding_tools.add_xnorm_histograms( results, @@ -809,7 +810,7 @@ def build_graph(df, dataset): "pt_min": template_minpt, "pt_max": template_maxpt, "abseta_max": template_maxeta, - "mtw_min": None, + "mtw_min": args.mtCut if args.mtGenCut else None, } if hasattr(dataset, "out_of_acceptance"): df = unfolding_tools.select_fiducial_space( diff --git a/scripts/histmakers/mz_dilepton.py b/scripts/histmakers/mz_dilepton.py index d1f83c551..36884d92e 100644 --- a/scripts/histmakers/mz_dilepton.py +++ b/scripts/histmakers/mz_dilepton.py @@ -273,6 +273,7 @@ "nonTrigMuons_charge0": hist.axis.Regular( 2, -2.0, 2.0, underflow=False, overflow=False, name="nonTrigMuons_charge0" ), + "ptll_resolution": hist.axis.Regular(1000, -1, 1, name="ptll_resolution"), } auxiliary_gen_axes = [ @@ -1068,6 +1069,25 @@ def build_graph(df, dataset): ) # test plots + if args.validationHists: + # resolution plot + df = df.Define("ptll_relResolution", "(ptll - postfsrPTV)/postfsrPTV") + df = df.Define("ptll_resolution", "(ptll - postfsrPTV)") + results.append( + df.HistoBoost( + f"nominal_relResolution", + [all_axes["ptll_resolution"], all_axes["ptll"], axis_absYll], + ["ptll_resolution", "postfsrPTV", "absYll", "nominal_weight"], + ) + ) + results.append( + df.HistoBoost( + f"nominal_resolution", + [all_axes["ptll_resolution"], all_axes["ptll"], axis_absYll], + ["ptll_resolution", "postfsrPTV", "absYll", "nominal_weight"], + ) + ) + if args.validationHists and args.useDileptonTriggerSelection: df_plusTrig = df.Filter("trigMuons_passTrigger0") df_minusTrig = df.Filter("nonTrigMuons_passTrigger0") diff --git a/scripts/plotting/inclusive_xsec_summary.py b/scripts/plotting/inclusive_xsec_summary.py index 717a46b4d..2659656a7 100644 --- a/scripts/plotting/inclusive_xsec_summary.py +++ b/scripts/plotting/inclusive_xsec_summary.py @@ -47,14 +47,12 @@ pdf_results = {} comp_result = {} -pdf_lumis = {} +# pdf_lumis = {} for pdf_file in args.pdfFiles: pdf_name = pdf_file.split("/")[-2].split("_")[-1] pdf_result, pdf_meta = rabbit.io_tools.get_fitresult(pdf_file, meta=True) - pdf_lumis[pdf_name] = pdf_meta["meta_info_input"]["channel_info"]["ch0"]["lumi"] - pdf_model = pdf_result["physics_models"] if "CompositeModel" in pdf_model.keys(): @@ -79,9 +77,15 @@ (r"$\mathrm{W}^{+}$", "Project ch1_masked qGen", "ch1_masked", {"qGen": 1}), (r"$\mathrm{W}$", "Project ch1_masked", "ch1_masked", None), (r"$\mathrm{Z}$", "Project ch0_masked", "ch0_masked", None), + # ( + # r"$\mathrm{W}^{-}/\mathrm{W}^{+}$", + # "Ratio ch1_masked ch1_masked qGen:0,ptGen:sum,absEtaGen:sum qGen:1,ptGen:sum,absEtaGen:sum", + # "ch1_masked", + # None, + # ), ( r"$\mathrm{W}^{+}/\mathrm{W}^{-}$", - "Ratio ch1_masked ch1_masked qGen:0,ptGen:sum,absEtaGen:sum qGen:1,ptGen:sum,absEtaGen:sum", + "Ratio ch1_masked ch1_masked qGen:1,ptGen:sum,absEtaGen:sum qGen:0,ptGen:sum,absEtaGen:sum", "ch1_masked", None, ), @@ -130,18 +134,13 @@ h1 = h1[{"yield": hist.sum}] hi = hi[{"yield": hist.sum}] - if model.startswith("Ratio"): - scale = 1 - else: - scale = 1 / (lumi * 1000) - - prefit = hp.value * scale - prefit_error = hp.variance**0.5 * scale + prefit = hp.value + prefit_error = hp.variance**0.5 - value = h1.value * scale - error = h1.variance**0.5 * scale + value = h1.value + error = h1.variance**0.5 - impacts = hi.values() * scale + impacts = hi.values() labels = np.array(hi.axes["impacts"]) mask = np.isin(labels, grouping) @@ -190,22 +189,26 @@ df["prefit_error"] = prefit_error for pdf_name, pdf_res in pdf_results.items(): - hr = pdf_res[model.replace("_masked", "")]["channels"][ + channel_models = pdf_res[model.replace("_masked", "")]["channels"][ channel.replace("_masked", "") - ]["hist_prefit_inclusive"].get() + ] + hr = channel_models["hist_prefit_inclusive"].get() + hr_impacts = channel_models[ + "hist_prefit_inclusive_global_impacts_grouped" + ].get() if selection is not None: hr = hr[selection] + hr_impacts = hr_impacts[selection] if getattr(hr, "axes", False) and "yield" in hr.axes.name: hr = hr[{"yield": hist.sum}] + hr_impacts = hr_impacts[{"yield": hist.sum}] - if model.startswith("Ratio"): - scale = 1 - else: - scale = 1 / (pdf_lumis[pdf_name] * 1000) - - df[pdf_name] = hr.value * scale - df[f"{pdf_name}_error"] = hr.variance**0.5 * scale + df[pdf_name] = hr.value + df[f"{pdf_name}_error"] = hr.variance**0.5 + df[f"{pdf_name}_pdf"] = hr_impacts[ + {"impacts": f"pdf{pdf_name.replace('aN3LO','an3lo')}"} + ] # Convert 'labels' column to categorical with the custom order df["label"] = pd.Categorical(df["label"], categories=custom_order, ordered=True) @@ -305,6 +308,17 @@ marker="o", label=pdf_name if i == 0 else None, ) + # # only plot PDF uncertainties + # pdf_error_pdf = df_g[f"{pdf_name}_pdf"].values[0] / norm + # ax.errorbar( + # [pdf_value], + # [i + 1 - (j + 1) / (nPDFs + 1)], + # xerr=pdf_error_pdf, + # color=pdf_colors[pdf_name], + # capsize=5, + # capthick=2, + # marker="o", + # ) # round to two significant digits in total uncertainty sig_digi = 2 - int(math.floor(math.log10(abs(total)))) - 1 @@ -387,7 +401,7 @@ ax.set_xlim([lo, hi]) ax.set_ylim([0, len(norms) + 2]) -ax.set_xlabel("1./Measurement", fontsize=20) +ax.set_xlabel("Prediction / Measurement") # Disable ticks on the top and right axes ax.tick_params(top=False) @@ -442,10 +456,10 @@ def plot_cov_ellipse(cov, pos, nstd=2, **kwargs): ("WZ", xsec_keys[2], xsec_keys[3], "pb"), ("R", xsec_keys[4], xsec_keys[5], None), ): - ckey1 = ( + ckey0 = ( channel0[1].replace("_masked", "") + " " + channel0[2].replace("_masked", "") ) - ckey2 = ( + ckey1 = ( channel1[1].replace("_masked", "") + " " + channel1[2].replace("_masked", "") ) @@ -456,10 +470,6 @@ def plot_cov_ellipse(cov, pos, nstd=2, **kwargs): for pdf_name, result in comp_result.items(): ibin = 0 - if name == "R": - scale = 1 - else: - scale = 1 / (1000 * pdf_lumis[pdf_name]) for k, r in result["channels"].items(): fittype = "postfit" if f"hist_postfit_inclusive" in r.keys() else "prefit" @@ -467,41 +477,42 @@ def plot_cov_ellipse(cov, pos, nstd=2, **kwargs): if getattr(hi, "axes", False) and "yield" in hi.axes.name: hi = hi[{"yield": hist.sum}] - if k == ckey1: + if k == ckey0: sel = channel0[-1] if sel is not None: - x = hi[sel].value * scale + x = hi[sel].value ix = ibin + [i for i in sel.values()][0] else: - x = hi.value * scale + x = hi.value ix = ibin - if k == ckey2: + if k == ckey1: sel = channel1[-1] if sel is not None: - y = hi[channel0[-1]].value * scale + y = hi[sel].value iy = ibin + [i for i in sel.values()][0] else: - y = hi.value * scale + y = hi.value iy = ibin ibin += hi.size if hasattr(hi, "size") else 1 cov = result[f"hist_{fittype}_inclusive_cov"].get().values() - cov = cov[np.ix_([ix, iy], [ix, iy])] * scale**2 + cov = cov[np.ix_([ix, iy], [ix, iy])] # for pos, cov in zip(points, covs): if fittype == "postfit": - icol = "grey" ell = plot_cov_ellipse( cov, np.array([x, y]), nstd=2, edgecolor="none", - facecolor=icol, + facecolor="grey", label="Measurement", ) + ax.add_patch(ell) + ax.plot(x, y, color="black", marker="P") else: icol = pdf_colors[pdf_name] ell = plot_cov_ellipse( @@ -513,8 +524,8 @@ def plot_cov_ellipse(cov, pos, nstd=2, **kwargs): linewidth=2, label=pdf_name, ) - ax.add_patch(ell) - ax.plot(x, y, color=icol, marker="o", alpha=0) # measurement center + ax.add_patch(ell) + ax.plot(x, y, color=icol, marker="o", alpha=0) xlim = ax.get_xlim() ylim = ax.get_ylim() @@ -550,7 +561,7 @@ def plot_cov_ellipse(cov, pos, nstd=2, **kwargs): padding_loc="auto", ) - plot_tools.add_cms_decor(ax, args.cmsDecor, data=True, lumi=lumi, loc=args.logoPos) + plot_tools.add_cms_decor(ax, args.cmsDecor, data=True, loc=args.logoPos) outname = f"summary_2D_{name}" if args.postfix: diff --git a/scripts/plotting/makeDataMCStackPlot.py b/scripts/plotting/makeDataMCStackPlot.py index d4b976c56..81cab0124 100644 --- a/scripts/plotting/makeDataMCStackPlot.py +++ b/scripts/plotting/makeDataMCStackPlot.py @@ -82,6 +82,7 @@ "QCD", "WtoNMu_5", "WtoNMu_10", + "WtoNMu_30", "WtoNMu_50", ], ) diff --git a/scripts/plotting/response_matrix.py b/scripts/plotting/response_matrix.py index 4c147c893..83dd3e2e3 100644 --- a/scripts/plotting/response_matrix.py +++ b/scripts/plotting/response_matrix.py @@ -51,6 +51,8 @@ args = parser.parse_args() +cmap = plt.get_cmap("tab10") + logger = logging.setup_logger(__file__, args.verbose, args.noColorLogger) outdir = output_tools.make_plot_dir(args.outpath, args.outfolder, eoscp=args.eoscp) @@ -66,14 +68,18 @@ ) # set gen axes empty to not integrate over when loading if "Wmunu" in groups.groups: - groups.copyGroup( - "Wmunu", "Wmunu_qGen0", member_filter=lambda x: x.name.startswith("Wminus") - ) - groups.copyGroup( - "Wmunu", "Wmunu_qGen1", member_filter=lambda x: x.name.startswith("Wplus") - ) + if "minus" in args.channels: + groups.copyGroup( + "Wmunu", "Wmunu_qGen0", member_filter=lambda x: x.name.startswith("Wminus") + ) + + if "plus" in args.channels: + groups.copyGroup( + "Wmunu", "Wmunu_qGen1", member_filter=lambda x: x.name.startswith("Wplus") + ) - groups.deleteGroup("Wmunu") + if "all" not in args.channels: + groups.deleteGroup("Wmunu") if "Zmumu" in groups.groups: groups.groups["Zmumu"].deleteMembers( @@ -94,12 +100,12 @@ "eta": r"$\mathrm{Reco}\ \eta$", "abs(eta)": r"$\mathrm{Reco}\ |\eta|$", "absEtaGen": r"$\mathrm{Gen}\ |\eta|$", - "ptll": r"$\mathrm{Reco}\ p_\mathrm{T}(\ell\ell)\ [\mathrm{GeV}]$", - "ptW": r"$\mathrm{Reco}\ p_\mathrm{T}(\ell\nu)\ [\mathrm{GeV}]$", - "ptVGen": r"$\mathrm{Gen}\ p_\mathrm{T}(V)\ [\mathrm{GeV}]$", - "yll": r"$\mathrm{Reco}\ Y(\mathrm{V})$", - "abs(yll)": r"$\mathrm{Reco}\ |Y(\mathrm{V})|$", - "absYVGen": r"$\mathrm{Gen}\ |Y(\mathrm{V})|$", + "ptll": r"$\mathrm{Reco}\ p_\mathrm{T}^{\ell\ell}\ [\mathrm{GeV}]$", + "ptW": r"$\mathrm{Reco}\ p_\mathrm{T}^{\ell\nu}\ [\mathrm{GeV}]$", + "ptVGen": r"$\mathrm{Gen}\ p_\mathrm{T}^Z\ [\mathrm{GeV}]$", + "yll": r"$\mathrm{Reco}\ y^{\ell\ell}$", + "abs(yll)": r"$\mathrm{Reco}\ |y^{\ell\ell}|$", + "absYVGen": r"$\mathrm{Gen}\ |y^\mathrm{Z}|$", "cosThetaStarll": r"$\mathrm{Reco}\ \cos{\theta^{\star}_{\ell\ell}}$", "phiStarll": r"$\mathrm{Reco}\ \phi^{\star}_{\ell\ell}$", "helicitySig": r"Helicity", @@ -187,7 +193,7 @@ def plot_resolution( ax.set_ylabel(ylabel) xedges = None - for sel2, idx2 in selections_slices: + for j, (sel2, idx2) in enumerate(selections_slices): if ( ( idx2 not in [hist.underflow, hist.overflow] @@ -208,11 +214,19 @@ def plot_resolution( else: edges = h2d.axes[sel2].edges var2 = translate_label[sel2].replace(r"[\mathrm{GeV}]", "") + unit = "GeV" if r"[\mathrm{GeV}]" in translate_label[sel2] else "" if idx2 == hist.overflow: - label = f"{var2} > {edges[-1]}" + lo = edges[-1] + if int(lo) == lo: + lo = int(lo) + label = f"{var2} > {lo} {unit}" elif idx2 + 1 < len(edges): lo, hi = edges[idx2], edges[idx2 + 1] - label = f"{lo} < {var2} < {hi}" + if int(lo) == lo: + lo = int(lo) + if int(hi) == hi: + hi = int(hi) + label = f"{lo} < {var2} < {hi} {unit}" h1d = h2d[{sel2: idx2}] if len(axes_reco) > 1: h1d = hh.unrolledHist(h1d, binwnorm=None, obs=axes_reco) @@ -224,7 +238,9 @@ def plot_resolution( if normalize: values /= abs(values).sum() - ax.stairs(values, edges=h1d.axes[0].edges, label=label) + ax.stairs( + values, edges=h1d.axes[0].edges, label=label, color=cmap(j % cmap.N) + ) if xedges is None: xedges = h1d.axes[0].edges @@ -243,14 +259,19 @@ def plot_resolution( if sel is not None: lo, hi = histo.axes[sel].edges[idx], histo.axes[sel].edges[idx + 1] - var = translate_label[sel].replace(r"[\mathrm{GeV}]", "") + var = translate_label[sel].replace(r"\ [\mathrm{GeV}]", "") + unit = "GeV" if r"[\mathrm{GeV}]" in translate_label[sel] else "" + if int(hi) == hi: + hi = int(hi) + if int(lo) == lo: + lo = int(lo) if sel.startswith("abs") and lo == 0: - title = f"{var} < {hi}" + title = f"{var} < {hi} {unit}" else: - title = f"{lo} < {var} < {hi}" + title = f"{lo} < {var} < {hi} {unit}" plt.text( - 0.06, + 0.04, 0.94, title, horizontalalignment="left", @@ -292,6 +313,93 @@ def plot_resolution( else {"charge": -1.0j if channel == "minus" else 1.0j} ) + if "resolution" in args.histName or "relResolution" in args.histName: + + if "relResolution" in args.histname: + relative = True + else: + relative = False + + fig = plt.figure(figsize=(10, 6)) + ax = fig.add_subplot() + + vreco = translate_label["ptll"] + vgen = translate_label["ptVGen"] + binlabel = translate_label["abs(yll)"] + + ax.set_xlabel(vgen) + vreco = ( + vreco.replace(r"[\mathrm{GeV}]", "") + .replace("$", "") + .replace("(V)", "(Z)") + .replace("Reco", "") + .replace(r"\ ", "") + ) + vgen = ( + vgen.replace(r"[\mathrm{GeV}]", "") + .replace("$", "") + .replace("(V)", "(Z)") + .replace("Gen", "") + .replace(r"\ ", "") + ) + if relative: + ylabel = rf"$\sigma(({vreco} - {vgen}) / {vgen})$" + else: + ylabel = rf"$\sigma({vreco} - {vgen})$" + ax.set_ylabel(ylabel) + + x = histo.axes["ptll"].edges + x_centers = histo.axes["ptll"].centers + r_centers = histo.axes["ptll_resolution"].centers + + for idx, ibin in enumerate(histo.axes["absYll"]): + values = histo[{"absYll": idx}].values() + + mean_res = np.zeros(len(x_centers)) + std_res = np.zeros(len(x_centers)) + + for i in range(len(x_centers)): + weights = values[:, i] + + if weights.sum() == 0: + mean_res[i] = np.nan + std_res[i] = np.nan + continue + + # weighted mean + mean = np.average(r_centers, weights=weights) + + # weighted variance + var = np.average((r_centers - mean) ** 2, weights=weights) + + mean_res[i] = mean + std_res[i] = np.sqrt(var) + + ax.stairs(std_res, x, label=f"{binlabel}[{idx}]") + + plot_tools.add_cms_decor( + ax, args.cmsDecor, data=True, lumi=None, loc=args.logoPos + ) + plot_tools.addLegend( + ax, ncols=args.legCols, loc=args.legPos, text_size=args.legSize + ) + + ax.set_ylim(0, 0.9) + ax.set_xlim(0, 100) + + outfile = f"resolution_1d_{g_name}_pt" + + plot_tools.save_pdf_and_png(outdir, outfile) + + output_tools.write_index_and_log( + outdir, + outfile, + analysis_meta_info={args.infile: groups.getMetaInfo()}, + args=args, + ) + + continue + # plot slices of resolution if all(x in histo.axes.name for x in ["pt", "ptGen", "absEtaGen"]): plot_resolution( @@ -299,7 +407,13 @@ def plot_resolution( axes_reco="pt", axis_gen="ptGen", selections_global=(("absEtaGen", 0), ("absEtaGen", 17)), - selections_slices=(("ptGen", 0), ("ptGen", 6), ("ptGen", 13)), + selections_slices=( + ("ptGen", 0), + ("ptGen", 3), + ("ptGen", 6), + ("ptGen", 10), + ("ptGen", 13), + ), ) if all(x in histo.axes.name for x in ["ptGen", "eta", "absEtaGen"]): plot_resolution( @@ -309,7 +423,9 @@ def plot_resolution( selections_global=(("ptGen", 0), ("ptGen", 13)), selections_slices=( ("absEtaGen", 0), - ("absEtaGen", 8), + ("absEtaGen", 3), + ("absEtaGen", 7), + ("absEtaGen", 10), ("absEtaGen", 17), ), ) @@ -321,11 +437,16 @@ def plot_resolution( axis_gen="ptVGen", selections_global=( ("absYVGen", 0), - ("absYVGen", 3), + ("absYVGen", 4), ), selections_slices=( ("ptVGen", 0), + ("ptVGen", 4), ("ptVGen", 8), + ("ptVGen", 12), + ("ptVGen", 14), + ("ptVGen", 16), + ("ptVGen", 18), ("ptVGen", hist.overflow), ), ) @@ -341,6 +462,7 @@ def plot_resolution( selections_slices=( ("absYVGen", 0), ("absYVGen", 2), + ("absYVGen", 4), ("absYVGen", hist.overflow), ), ) diff --git a/scripts/rabbit/setupRabbit.py b/scripts/rabbit/setupRabbit.py index 28f8e6257..0e24dd3a7 100644 --- a/scripts/rabbit/setupRabbit.py +++ b/scripts/rabbit/setupRabbit.py @@ -166,7 +166,7 @@ def make_parser(parser=None): type=str, nargs="*", help="Don't run over processes belonging to these groups (only accepts exact group names)", - default=["QCD", "WtoNMu_5", "WtoNMu_10", "WtoNMu_50"], + default=["QCD"], ) parser.add_argument( "--filterProcGroups", @@ -175,6 +175,13 @@ def make_parser(parser=None): help="Only run over processes belonging to these groups", default=[], ) + parser.add_argument( + "--addBSM", + type=str, + default=None, + help="Add BSM model", + choices=["WtoNMu_0", "WtoNMu_5", "WtoNMu_10", "WtoNMu_30", "WtoNMu_50"], + ) parser.add_argument( "-x", "--excludeNuisances", @@ -224,10 +231,14 @@ def make_parser(parser=None): ) parser.add_argument( "--rebin", - type=int, + type=parsing.str_to_list_or_int, nargs="*", default=[], - help="Rebin axis by this value (default, 1, does nothing)", + help=""" + Rebin axis by this value (default, 1, does nothing); + use integer 'n' for merging 'n' bins; + use comma separated list with new edges, use a leading space in case the bin edge is negative (e.g. " -2.4") + """, ) parser.add_argument( "--absval", @@ -927,6 +938,7 @@ def setup( xnorm=any( inputBaseName.startswith(x) for x in ["gen", "xnorm", "prefsr", "postfsr"] ), + bsm_model=args.addBSM, ) datagroups.fit_axes = fitvar @@ -994,6 +1006,15 @@ def setup( else: base_group = "Zee" if datagroups.flavor == "ee" else "Zmumu" + if args.addBSM == "WtoNMu_0": + # an empty group should have been created, add SM W members as proxy for BSM + datagroups.groups["WtoNMu_0"].addMembers(datagroups.groups["Wmunu"].members) + + # normalize total cross section to BSM default + total_xsec = sum([m.xsec for m in datagroups.groups["WtoNMu_0"].members]) + for member in datagroups.groups["WtoNMu_0"].members: + member.xsec = common.xsec_WtoNMu * member.xsec / total_xsec + if args.addTauToSignal: # add tau signal processes to signal group datagroups.groups[base_group].addMembers( diff --git a/utilities/common.py b/utilities/common.py index ed8bb6180..6aa4e42ba 100644 --- a/utilities/common.py +++ b/utilities/common.py @@ -58,6 +58,7 @@ "WminusCharmToMuNu", "WtoNMu_MN-5-V-0p001", "WtoNMu_MN-10-V-0p001", + "WtoNMu_MN-30-V-0p001", "WtoNMu_MN-50-V-0p001", ] zprocs = [ @@ -353,6 +354,7 @@ def get_dilepton_ptV_binning(fine=False): [ 0, 1, + 1.5, 2, 2.5, 3, diff --git a/utilities/differential.py b/utilities/differential.py index 075b23aae..a79398051 100644 --- a/utilities/differential.py +++ b/utilities/differential.py @@ -104,7 +104,7 @@ def get_dilepton_axes( gen_level, add_out_of_acceptance_axis=False, flow_y=False, - rebin_pt=False, + rebin_pt=None, ): """ construct axes, columns, and selections for differential Z dilepton measurement from correponding reco edges. Currently supported: pT(Z), |yZ| @@ -129,13 +129,8 @@ def get_dilepton_axes( if v == "ptVGen": edges = reco_edges["ptll"] - if rebin_pt: - # use 2 ptll bin for each ptVGen bin, last bin is overflow - if len(edges) % 2: - # in case it's an odd number of edges, last two bins are overflow - edges = edges[:-1] - # 1 gen bin for 2 reco bins - edges = edges[::2] + if rebin_pt is not None: + edges = rebin_pt(edges) axes.append( hist.axis.Variable( diff --git a/utilities/parsing.py b/utilities/parsing.py index aba18ad6c..1a7e1eef0 100644 --- a/utilities/parsing.py +++ b/utilities/parsing.py @@ -29,6 +29,17 @@ def str_to_complex_or_int(value): raise argparse.ArgumentTypeError(f"Invalid integer: '{value}'") +def str_to_list_or_int(value): + # .strip() handles the "leading space" trick if you use it + value = value.strip() + if "," in value: + return [float(x) for x in value.split(",")] + try: + return int(value) + except ValueError: + raise argparse.ArgumentTypeError(f"Invalid integer: '{value}'") + + def set_parser_attribute(parser, argument, attribute, newValue): # change an argument of the parser, must be called before parse_arguments logger = logging.child_logger(__name__) diff --git a/utilities/styles/styles.py b/utilities/styles/styles.py index 87a9ef761..e2a3c70e0 100644 --- a/utilities/styles/styles.py +++ b/utilities/styles/styles.py @@ -12,12 +12,14 @@ def translate_html_to_latex(n): # transform html style formatting into latex style if "", r"\mathit{") + n.replace("", r"\mathit{") + .replace("", "}") .replace("", "_{") .replace("", "^{") - .replace("", "}") .replace("", "}") .replace("", "}") + .replace(r"μ", r"\mu") + .replace(r"ε", r"\epsilon") .replace(" ", r"\ ") ) return n @@ -52,9 +54,10 @@ def translate_html_to_latex(n): "Fake_e": "#964A8B", "Fake_mu": "#964A8B", "Prompt": "#E42536", - "WtoNMu_5": "#409C3D", - "WtoNMu_10": "#2BC74D", - "WtoNMu_50": "#00FF80", + "WtoNMu_5": "#006400", + "WtoNMu_10": "#009933", + "WtoNMu_30": "#00CC99", + "WtoNMu_50": "#00FFFF", "BSM": "#409C3D", } @@ -116,9 +119,10 @@ def translate_html_to_latex(n): "Fake_e": "Nonprompt (e)", "Fake_mu": r"Nonprompt ($\mu$)", "Prompt": "Prompt", - "WtoNMu_5": r"W$^{\pm}\to\mathrm{N}\mu (5GeV)$", - "WtoNMu_10": r"W$^{\pm}\to\mathrm{N}\mu (10GeV)$", - "WtoNMu_50": r"W$^{\pm}\to\mathrm{N}\mu (50GeV)$", + "WtoNMu_5": r"W$^{\pm}\to\mu\mathrm{N} (5GeV)$", + "WtoNMu_10": r"W$^{\pm}\to\mu\mathrm{N} (10GeV)$", + "WtoNMu_30": r"W$^{\pm}\to\mu\mathrm{N} (30GeV)$", + "WtoNMu_50": r"W$^{\pm}\to\mu\mathrm{N} (50GeV)$", } axis_labels = { @@ -132,7 +136,7 @@ def translate_html_to_latex(n): "muonJetPt": {"label": r"$\mathit{p}_{T}^\mathrm{jet[\mu]}$", "unit": "GeV"}, "recoil_perp": {"label": r"$\it{u}_{\mathrm{T}}^{\perp}$", "unit": "GeV"}, "recoil_para": {"label": r"$\it{u}_{\mathrm{T}}^{\parallel}$", "unit": "GeV"}, - "qGen": r"$|\mathit{q}^{\mu}|$", + "qGen": r"$\mathit{q}^{\mu}$", "eta": r"$\mathit{\eta}^{\mu}$", "etaGen": r"$\mathit{\eta}^{\mu}$", "abseta": r"$|\mathit{\eta}^{\mu}|$", @@ -376,6 +380,7 @@ def translate_html_to_latex(n): "binByBinStatZtautau", "binByBinStatWtoNMu_5", "binByBinStatWtoNMu_10", + "binByBinStatWtoNMu_30", "binByBinStatWtoNMu_50", ], "unfolding": [ @@ -406,6 +411,10 @@ def translate_html_to_latex(n): "pdfCT18Z", "pTModeling", "theory_ew", + "massShift", + "widthW", + "widthZ", + "sin2thetaZ", ], "unfolding_min": [ "Total", @@ -433,6 +442,30 @@ def translate_html_to_latex(n): "bcQuarkMass", "theory_ew", ], + "xsecs": [ + "Total", + "stat", + "binByBinStat", + "binByBinStatW", + "binByBinStatZ", + # "expNoLumi", + "luminosity", + "angularCoeffs", + "pdfCT18Z", + "pTModeling", + "theory_ew", + "massShift", + "widthW", + "widthZ", + "sin2thetaZ", + "muon_eff_syst", + "muon_eff_stat", + "prefire", + "muonCalibration", + "Fake", + "recoil", + "CMS_background", + ], } text_dict = { @@ -465,9 +498,10 @@ def translate_html_to_latex(n): } impact_labels = { - "WtoNMu_5": "μW→μN(5GeV)", - "WtoNMu_10": "μW→μN(10GeV)", - "WtoNMu_50": "μW→μN(50GeV)", + "WtoNMu_5": "μW→Nμ(5GeV)", + "WtoNMu_10": "μW→Nμ(10GeV)", + "WtoNMu_30": "μW→Nμ(30GeV)", + "WtoNMu_50": "μW→Nμ(50GeV)", "massShiftZ100MeV": "mZ", "massShiftW100MeV": "mW", "widthZ": "ΓmZ", @@ -483,7 +517,7 @@ def translate_html_to_latex(n): "QCDscaleWPtHelicityMiNNLO": "μR μF scale (W)", "QCDscaleZPtChargeHelicityMiNNLO": "μR μF scale (Z)", "QCDscaleWPtChargeHelicityMiNNLO": "μR μF scale (W)", - "binByBinStat": "Bin-by-bin stat.", + "binByBinStat": "Simulation stat.", "binByBinStatW": "Bin-by-bin stat. (W)", "binByBinStatZ": "Bin-by-bin stat. (Z)", "binByBinStatWmunu": "Bin-by-bin stat. (W→μν)", @@ -496,9 +530,10 @@ def translate_html_to_latex(n): "binByBinStatTop": "Bin-by-bin stat. (top)", "binByBinStatWtoNMu_5": "Bin-by-bin stat. (BSM)", "binByBinStatWtoNMu_10": "Bin-by-bin stat. (BSM)", + "binByBinStatWtoNMu_30": "Bin-by-bin stat. (BSM)", "binByBinStatWtoNMu_50": "Bin-by-bin stat. (BSM)", "recoil": "recoil", - "CMS_background": "Bkg.", + "CMS_background": "Other bkg.", "FakeHighMT": "FakeHighMT", "FakeLowMT": "FakeLowMT", "rFake": "fakerate", diff --git a/wremnants/datasets/datagroup.py b/wremnants/datasets/datagroup.py index aed47966c..c21725857 100644 --- a/wremnants/datasets/datagroup.py +++ b/wremnants/datasets/datagroup.py @@ -69,7 +69,7 @@ def addMember(self, member, member_operation=None): self.memberOp = [None] * len(self.members) self.memberOp.append(deepcopy(member_operation)) - self.members.append(member) + self.members.append(deepcopy(member)) def deleteMembers(self, members=None): if members is None: diff --git a/wremnants/datasets/datagroups.py b/wremnants/datasets/datagroups.py index 59a7eb3f5..2b9875900 100644 --- a/wremnants/datasets/datagroups.py +++ b/wremnants/datasets/datagroups.py @@ -895,9 +895,10 @@ def getHistForUnfolding( if self.groups[group_name].memberOp is not None: base_member_op = self.groups[group_name].memberOp[base_member_idx] - return base_member_op(nominal_hist) - else: - return nominal_hist + if base_member_op is not None: + return base_member_op(nominal_hist) + + return nominal_hist def getPOINames(self, gen_bin_indices, axes_names, base_name, flow=True): poi_names = [] diff --git a/wremnants/datasets/datagroups2016.py b/wremnants/datasets/datagroups2016.py index 8104eeeab..e4c8ed9b9 100644 --- a/wremnants/datasets/datagroups2016.py +++ b/wremnants/datasets/datagroups2016.py @@ -4,7 +4,12 @@ def make_datagroups_2016( - dg, combine=False, pseudodata_pdfset=None, excludeGroups=None, filterGroups=None + dg, + combine=False, + pseudodata_pdfset=None, + excludeGroups=None, + filterGroups=None, + bsm_model=None, ): # reset datagroups dg.groups = {} @@ -65,18 +70,12 @@ def make_datagroups_2016( "QCD", members=dg.get_members_from_results(startswith=["QCD"]), ) - dg.addGroup( - "WtoNMu_5", - members=dg.get_members_from_results(startswith=["WtoNMu_MN-5-"]), - ) - dg.addGroup( - "WtoNMu_10", - members=dg.get_members_from_results(startswith=["WtoNMu_MN-10-"]), - ) - dg.addGroup( - "WtoNMu_50", - members=dg.get_members_from_results(startswith=["WtoNMu_MN-50-"]), - ) + if bsm_model is not None: + model, mass = bsm_model.split("_") + dg.addGroup( + bsm_model, + members=dg.get_members_from_results(startswith=[f"{model}_MN-{mass}-"]), + ) else: dg.addGroup( "Other", diff --git a/wremnants/datasets/datagroupsLowPU.py b/wremnants/datasets/datagroupsLowPU.py index e06cb67b5..440af8072 100644 --- a/wremnants/datasets/datagroupsLowPU.py +++ b/wremnants/datasets/datagroupsLowPU.py @@ -3,7 +3,9 @@ logger = logging.child_logger(__name__) -def make_datagroups_lowPU(dg, combine=False, excludeGroups=None, filterGroups=None): +def make_datagroups_lowPU( + dg, combine=False, excludeGroups=None, filterGroups=None, bsm_model=None +): # reset datagroups dg.groups = {} diff --git a/wremnants/datasets/datasetDict_v9.py b/wremnants/datasets/datasetDict_v9.py index 7b295ec29..c4d45cee7 100644 --- a/wremnants/datasets/datasetDict_v9.py +++ b/wremnants/datasets/datasetDict_v9.py @@ -231,6 +231,13 @@ "xsec": common.xsec_WtoNMu, "group": "WtoNMu_10", }, + "WtoNMu_MN-30-V-0p001": { + "filepaths": [ + "{BASE_PATH}/WtoNMu_MN-30-V-0p001_TuneCP5_13TeV_madgraph-pythia8/" + ], + "xsec": common.xsec_WtoNMu, + "group": "WtoNMu_30", + }, "WtoNMu_MN-50-V-0p001": { "filepaths": [ "{BASE_PATH}/WtoNMu_MN-50-V-0p001_TuneCP5_13TeV_madgraph-pythia8/" diff --git a/wremnants/helicity_utils.py b/wremnants/helicity_utils.py index 7b823ffd0..7384f3bdc 100644 --- a/wremnants/helicity_utils.py +++ b/wremnants/helicity_utils.py @@ -11,6 +11,7 @@ from utilities.io_tools import input_tools from wremnants.correctionsTensor_helper import makeCorrectionsTensor from wremnants.theory_tools import helicity_xsec_to_angular_coeffs +from wums import boostHistHelpers as hh from wums import logging logger = logging.child_logger(__name__) @@ -32,7 +33,7 @@ def make_helicity_weight_helper( is_z=False, filename=f"{common.data_dir}/angularCoefficients/w_z_helicity_xsecs_theoryAgnosticBinning_scetlib_dyturboCorr_maxFiles_m1.hdf5", - rebi_ptVgen=False, + rebin_ptVgen_edges=None, ): with h5py.File(filename, "r") as ff: @@ -41,8 +42,10 @@ def make_helicity_weight_helper( hist_helicity_xsec_scales = out["Z"] if is_z else out["W"] # rebinning must happen *before* calculating Ai's - if rebi_ptVgen: - hist_helicity_xsec_scales = hist_helicity_xsec_scales[{"ptVgen": hist.rebin(2)}] + if rebin_ptVgen_edges is not None: + hist_helicity_xsec_scales = hh.rebinHist( + hist_helicity_xsec_scales, "ptVgen", rebin_ptVgen_edges + ) corrh = helicity_xsec_to_angular_coeffs(hist_helicity_xsec_scales) diff --git a/wremnants/theory_tools.py b/wremnants/theory_tools.py index 143064723..a4ceb573a 100644 --- a/wremnants/theory_tools.py +++ b/wremnants/theory_tools.py @@ -657,7 +657,7 @@ def define_postfsr_vars(df, mode=None): ) df = df.Define( "postfsrOtherLep_mass", - "isEvenEvent ? wrem::get_pdgid_mass(GenPart_pdgId[postfsrLep][postfsrAntiLep_idx]) : wrem::get_pdgid_mass(GenPart_pdgId[postfsrAntiLep][postfsrLep_idx])", + "isEvenEvent ? wrem::get_pdgid_mass(GenPart_pdgId[postfsrAntiLep][postfsrAntiLep_idx]) : wrem::get_pdgid_mass(GenPart_pdgId[postfsrLep][postfsrLep_idx])", ) df = df.Define( diff --git a/wremnants/unfolding_tools.py b/wremnants/unfolding_tools.py index f8df87bf2..2923acc1f 100644 --- a/wremnants/unfolding_tools.py +++ b/wremnants/unfolding_tools.py @@ -346,16 +346,16 @@ def __init__( self.poi_as_noi = poi_as_noi self.unfolding_levels = unfolding_levels - # self.qcdScaleByHelicity_helper = + def rebin_pt(edges): + # use 2 ptll bin for each ptVGen bin, except first and last + # first gen bin same size as reco bin, then 1 gen bin for 2 reco bins + new_edges = np.array([*edges[:2], *edges[3::2]]) + if len(new_edges) % 2: + # in case it's an odd number of edges, last two bins are overflow + edges = edges[:-1] + return new_edges - if self.add_helicity_axis: - # helper to derive helicity xsec shape from event by event reweighting - self.weightsByHelicity_helper_unfolding = helicity_utils.make_helicity_weight_helper( - is_z=True, - filename=f"{common.data_dir}/angularCoefficients/w_z_helicity_xsecs_maxFiles_m1_alphaSunfoldingBinning_helicity.hdf5", - # filename=f"{common.data_dir}/angularCoefficients/w_z_helicity_xsecs_maxFiles_m1_nnpdf31_alphaSunfoldingBinning_helicity.hdf5", - rebi_ptVgen=True, - ) + self.weightsByHelicity_helper_unfolding = None self.unfolding_axes = {} self.unfolding_cols = {} @@ -368,13 +368,22 @@ def __init__( level, flow_y=self.poi_as_noi, add_out_of_acceptance_axis=self.poi_as_noi, - rebin_pt=True, + rebin_pt=rebin_pt, ) self.unfolding_axes[level] = a self.unfolding_cols[level] = c self.unfolding_selections[level] = s if self.add_helicity_axis: + if self.weightsByHelicity_helper_unfolding is None: + edges = [ax for ax in a if ax.name == "ptVGen"][0].edges + # helper to derive helicity xsec shape from event by event reweighting + self.weightsByHelicity_helper_unfolding = helicity_utils.make_helicity_weight_helper( + is_z=True, + filename=f"{common.data_dir}/angularCoefficients/w_z_helicity_xsecs_maxFiles_m1_alphaSunfoldingBinning_helicity.hdf5", + rebin_ptVgen_edges=edges, + ) + for ax in a: if ax.name == "acceptance": continue @@ -383,6 +392,7 @@ def __init__( wbh_axis = self.weightsByHelicity_helper_unfolding.hist.axes[ ax.name.replace("Gen", "gen") ] + if any(ax.edges != wbh_axis.edges): raise RuntimeError( f"""