From f1056493cd8e5d97052a1412c016a5d2ed1013f6 Mon Sep 17 00:00:00 2001 From: rodwyer100 Date: Tue, 7 Apr 2026 12:44:59 -0700 Subject: [PATCH 1/3] Adding the scripts required to run the 2021 SIMP tight selection optimization --- tools/simp-search-tools/README.txt | 36 + .../plot-making/ann_score_data_mc_overlay.py | 412 ++++++++ .../plot-making/make_maxZbi_grid_worker_v2.py | 247 +++++ .../plot-making/submit_maxZbi_grid.sh | 30 + .../plot-making/write_roc_overlay_all3.py | 429 ++++++++ .../slurm-running/bk_eff_selection.py | 187 ++++ .../slurm-running/decayLength8sel.py | 655 ++++++++++++ .../slurm-running/scan_yields_array_v9_ALL.sh | 61 ++ .../slurm-running/scanner_run3.sh | 6 + .../write_final_yields_v9_ALL3.py | 930 ++++++++++++++++++ 10 files changed, 2993 insertions(+) create mode 100644 tools/simp-search-tools/README.txt create mode 100644 tools/simp-search-tools/plot-making/ann_score_data_mc_overlay.py create mode 100644 tools/simp-search-tools/plot-making/make_maxZbi_grid_worker_v2.py create mode 100644 tools/simp-search-tools/plot-making/submit_maxZbi_grid.sh create mode 100644 tools/simp-search-tools/plot-making/write_roc_overlay_all3.py create mode 100644 tools/simp-search-tools/slurm-running/bk_eff_selection.py create mode 100644 tools/simp-search-tools/slurm-running/decayLength8sel.py create mode 100644 tools/simp-search-tools/slurm-running/scan_yields_array_v9_ALL.sh create mode 100644 tools/simp-search-tools/slurm-running/scanner_run3.sh create mode 100644 tools/simp-search-tools/slurm-running/write_final_yields_v9_ALL3.py diff --git a/tools/simp-search-tools/README.txt b/tools/simp-search-tools/README.txt new file mode 100644 index 00000000..df0a5546 --- /dev/null +++ b/tools/simp-search-tools/README.txt @@ -0,0 +1,36 @@ +In this directory, we include all the tools to perform tight selection optimization with the 2021 analysis. +It includes the following subdirectories: slurm-running, bdt-training ,ann-training, and plot-making. Here is a brief description of how to run everything. + +slurm-running +This contains the 5 scripts to run tight selection optimization on slurm's sbatch. Except for a repository to contain signal and background distributions, the bdt joblib file, and the ann's scaling npz file and classifier and adversary pickle files, this should be self contained and runable. I suggest you toggle the other inputs to your desiring. Here is a brief description of each file and dependendcies: + +scanner_run3.sh: submits ~12 files to sbatch slurm with a different index correspdoning to one of the scan values (either determining epsilon, mass, or one of the scan values for projected significance and/or ann, intuition cut, or bdt cut value. + +scan_yields_array_v9_ALL.sh: slurm job that actually is run by roma, milano etc. Just evaluates the upcoming function writer for a copy of mass, epsilon, etc. You set the output directory of text files continaing persisting signal and background numbers here. + +write_final_yields_v9_ALL3.py: the workhorse for this analysis. Takes dependences from decayLength8sel.py and bk_eff_selection.py to estimate the signal and background abundancies first and then apply fractional cuts to this. The input parameters feed into this and it applies consecutive tight selection (psum, proj sig, hit category, and ann/bdt/min y0) fractions to this and finally writes a text file with surviving signal, background, significance, and scan values used. + +decayLength8sel.py: contains tools required to calculate F(z) (fractional acceptance), radiative fraction and acceptance fits, mass resolution curves, branching fractions, and effectively the vast majority of things required to calculate signal abundance. + +bk_eff_selection.y: contains any tools deemed entirely corresponding to background. Largely obsolete, but should retain and you will need to input background root file locations in here. + + +plot-making +This file contains many/most of the scripts required for plotting tight optimization estimated yields (after optimizing things) as well as roc curves for individual scans (ann,bdt,miny0) to compare things relatively. The scripts contained therein are: ann_score_data_mc_overlay.py make_maxZbi_grid_worker_v2.py submit_maxZbi_grid.sh write_roc_overlay_all3.py + +ann_score_data_mc_overlay.py: +Plots the ann response curve for data and data-like MC on top of eachother given the locations of ann npz scaling files and classifier pickle files. Is used as a final test to establish that indeed it is not learning data MC discriminating features rather than signal background discriminating features. + +make_maxZbi_grid_worker_v2.py: +This file reads in the text files created by write_final_yields_v9_ALL3.py for cut, ann, bdt, and all hit categories and makes significance, signal, background yield plots for mass and epsilon as well as what cut values were used for those bins. Run with submit_maxZbi_grid.sh + +submit_maxZbi_grid.sh: +Submits 12 jobs to run make_maxZbi_grid_worker_v2.py concurrently. Making the plot (from reading txt files) actually takes a decent bit, long enough that parrallelization acrosss slurm batches is necessary. + +write_roc_overlay_all3.py: +Writes a comparative roc curve for the ann, bdt, and miny0 curves. Necessary to evaluate whether the slurm-running code is going to work (one often runs the long slurm job in there, finds one bungled the ann variable order in training and got very poor performance, and wasted 2 hours of slurm time and alot of machine resources if one doesn't do this first). + +ann-training +This repository contains the code used to train the ann to be adversarial to mass training. It only contains the most recent version (i.e. the one that does mixed background and signal samples). I use a special command to run these python notebooks; I will include said argument here too. + + diff --git a/tools/simp-search-tools/plot-making/ann_score_data_mc_overlay.py b/tools/simp-search-tools/plot-making/ann_score_data_mc_overlay.py new file mode 100644 index 00000000..dd1cb90e --- /dev/null +++ b/tools/simp-search-tools/plot-making/ann_score_data_mc_overlay.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +import os +import sys +import gc +import argparse + +import numpy as np +import awkward as ak +import uproot +import torch +from torch import nn +import ROOT as r + +r.gROOT.SetBatch(True) +r.gStyle.SetOptStat(0) +r.gStyle.SetPadTopMargin(0.04) +r.gStyle.SetPadBottomMargin(0.12) +r.gStyle.SetPadRightMargin(0.05) +r.gStyle.SetPadLeftMargin(0.12) + +# ============================================================ +# User-editable configuration, copied from MC_data_comparison_standalone.py +# ============================================================ +DO_RATIO = False +OUTDIR = os.getcwd() + +DATA_FILE = "/sdf/data/hps/physics2021/preselection/v8/data_1pc_z0_calb_run_by_run/merged_hps_014713_job462.root" +#"/sdf/data/hps/physics2021/preselection/v2/data/merged_hps_014272_job64.root" +TRITRIG_FILE = "/sdf/data/hps/users/sgaiser/analysis/pass_v9/tritrig_HPS_Run2021Pass1_v9_14272_hitSmearKill_1000files.root" +WAB_FILE = "/sdf/data/hps/users/sgaiser/analysis/pass_v9/wab_HPS_Run2021Pass1_v9_14272_hitSmearKill_2000files.root" +TREE_NAME = "preselection" +LUMI_14272 = 0.012653 + +ANN_HIST_CONFIG = { + "nbins": 100, + "xmin": 0.0, + "xmax": 1.0, + "xtitle": "ANN response score", +} + + +# ============================================================ +# ANN utilities, copied from write_roc_overlay_all5.py +# ============================================================ +class ANNClassifier(nn.Module): + """Architecture matched to classifier_adv_2021_v9_pass5_run42QualCuts_*.pt.""" + def __init__(self, in_features: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 64, bias=False), + nn.BatchNorm1d(64), + nn.LeakyReLU(), + nn.Linear(64, 1), + ) + + def forward(self, x): + return self.net(x) + + +def _as_float32_col(arr): + return np.asarray(arr, dtype=np.float32).reshape(-1, 1) + + +def ann_predict_score(model, scaler_mean, scaler_scale, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + X_scaled = (X_chunk.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled) + with torch.no_grad(): + scores[start:end] = torch.sigmoid(model(X_tensor)).cpu().numpy().ravel() + return scores + + +def load_ann_model(whichmass): + ann_model_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/classifier_adv_2021_v9_pass5_run42QualCuts_{int(whichmass)}.pt" + ann_scaler_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_arrays_{int(whichmass)}.npz" + + sys.modules['numpy._core'] = np.core + ann_scaler = np.load(ann_scaler_path) + ann_scaler_mean = ann_scaler["mean"].astype(np.float32) + ann_scaler_scale = ann_scaler["scale"].astype(np.float32) + + ann_model = ANNClassifier(in_features=34) + ann_state = torch.load(ann_model_path, map_location="cpu") + ann_model.load_state_dict(ann_state) + ann_model.eval() + return ann_model, ann_scaler_mean, ann_scaler_scale + + +# ============================================================ +# Branch loading and feature building, copied from write_roc_overlay_all5.py +# but adapted to run directly on the data-like ROOT files used in +# MC_data_comparison_standalone.py +# ============================================================ +def common_branch_list(): + return [ + "psum", + "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_", + "ele_L1_iso_significance", "pos_L1_iso_significance", + ] + + +def build_ann_matrix(arrays): + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + feats = [ + _as_float32_col(vertex_pos["fX"]), + _as_float32_col(vertex_pos["fY"]), + _as_float32_col(vertex_pos["fZ"]), + _as_float32_col(ak.to_numpy(arrays["psum"])), + ] + track_keys = [ + "n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", + "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_" + ] + for key in track_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"ele.track_.{key}"]))) + for key in track_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"pos.track_.{key}"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vertex.chi2_"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["pos_L1_iso_significance"]))) + return np.hstack(feats) + + +def derive_l1l1_from_ak(hit_layers): + if hit_layers is None: + return None + hasL0 = ak.to_numpy(ak.any(hit_layers == 0, axis=-1)) + hasL1 = ak.to_numpy(ak.any(hit_layers == 1, axis=-1)) + return np.asarray(hasL0 & hasL1, dtype=bool) + + +def build_common_mask(psum, l1l1_mask, proj_sig, proj_fixed): + psum_mask = (psum >= 1.5) & (psum <= 3.0) + proj_mask = (proj_sig < proj_fixed) + return psum_mask & l1l1_mask & proj_mask + + +def load_arrays(root_path, tree_name): + with uproot.open(root_path) as f: + tree = f[tree_name] + arrays = tree.arrays(common_branch_list(), library="ak", how=dict) + return arrays + + +def load_sample_for_ann(root_path, tree_name, proj_fixed): + arrays = load_arrays(root_path, tree_name) + psum = ak.to_numpy(arrays["psum"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + + ele_L1L1 = derive_l1l1_from_ak(arrays.get("ele.track_.hit_layers_")) + pos_L1L1 = derive_l1l1_from_ak(arrays.get("pos.track_.hit_layers_")) + if ele_L1L1 is None: + ele_L1L1 = np.ones_like(psum, dtype=bool) + if pos_L1L1 is None: + pos_L1L1 = np.ones_like(psum, dtype=bool) + + l1l1_mask = ele_L1L1 & pos_L1L1 + common_mask = build_common_mask(psum, l1l1_mask, proj_sig, proj_fixed) + X_ann = build_ann_matrix(arrays) + return X_ann, common_mask + + +# ============================================================ +# ROOT histogram / drawing helpers copied from MC_data_comparison_standalone.py +# ============================================================ +def get_scale_factor(sample, lumi=LUMI_14272): + if "wab" in sample or "WAB" in sample: + xsec = 8.249 * 1e10 + N_gen = 2000. * 20 * 10000 + elif "tritrig" in sample: + xsec = 4.025 * 1e9 + N_gen = 1000. * 8 * 10000 + elif "rad" in sample: + xsec = 3.44 * 1e7 + N_gen = 387. * 5 * 10000 + else: + return 1.0 + return (xsec * lumi) / N_gen + + +def sanitize(name): + out = name + for old, new in [ + (".", "_"), + ("(", ""), + (")", ""), + ("[", "_"), + ("]", ""), + ("{", "_"), + ("}", ""), + ("+", "plus"), + ("-", "minus"), + ("*", "times"), + ("/", "div"), + (" ", ""), + (":", "_"), + ]: + out = out.replace(old, new) + return out + + +def make_hist_from_scores(scores, weights, tag, nbins, xmin, xmax): + hname = f"h_ann_score_{sanitize(tag)}" + hist = r.TH1F(hname, hname, nbins, xmin, xmax) + hist.Sumw2() + for val, wgt in zip(scores, weights): + hist.Fill(float(val), float(wgt)) + hist.SetDirectory(0) + return hist + + +def normalize(hist): + integral = hist.Integral() + if integral > 0: + hist.Scale(1.0 / integral) + + +def style_hist(hist, color, width=2): + hist.SetLineColor(color) + hist.SetMarkerColor(color) + hist.SetLineWidth(width) + + +def make_ratio_hist(num, den, name): + ratio = num.Clone(name) + ratio.SetDirectory(0) + ratio.Divide(den) + return ratio + + +def draw_comparison(h_data, h_mc, outbase, xtitle, do_ratio=False): + pdf_name = os.path.join(OUTDIR, outbase + ".pdf") + png_name = os.path.join(OUTDIR, outbase + ".png") + + style_hist(h_data, r.kBlack, 3) + style_hist(h_mc, r.kRed + 1, 3) + + leg = r.TLegend(0.58, 0.75, 0.90, 0.90) + leg.SetBorderSize(0) + leg.SetFillStyle(0) + leg.AddEntry(h_data, "run 14272 pass5_v9", "l") + leg.AddEntry(h_mc, "tritrig + wab", "l") + + if do_ratio: + can = r.TCanvas("can_ann_score", "can_ann_score", 800, 800) + pad1 = r.TPad("pad1_ann_score", "", 0.0, 0.30, 1.0, 1.0) + pad2 = r.TPad("pad2_ann_score", "", 0.0, 0.0, 1.0, 0.30) + + pad1.SetBottomMargin(0.02) + pad2.SetTopMargin(0.03) + pad2.SetBottomMargin(0.32) + + pad1.Draw() + pad2.Draw() + + pad1.cd() + ymax = 1.20 * max(h_data.GetMaximum(), h_mc.GetMaximum()) + h_data.SetMaximum(ymax) + h_data.SetTitle(f";{xtitle};Events") + h_data.Draw("hist") + h_mc.Draw("hist same") + leg.Draw() + + pad2.cd() + ratio = make_ratio_hist(h_mc, h_data, "ratio_ann_score") + style_hist(ratio, r.kBlue + 1, 2) + ratio.SetTitle("") + ratio.GetYaxis().SetTitle("MC/data") + ratio.GetXaxis().SetTitle(xtitle) + ratio.GetYaxis().SetNdivisions(505) + ratio.GetYaxis().SetTitleSize(0.10) + ratio.GetYaxis().SetTitleOffset(0.45) + ratio.GetYaxis().SetLabelSize(0.09) + ratio.GetXaxis().SetTitleSize(0.12) + ratio.GetXaxis().SetLabelSize(0.10) + ratio.SetMinimum(0.59) + ratio.SetMaximum(1.43) + ratio.Draw("hist") + + line = r.TLine(ANN_HIST_CONFIG["xmin"], 1.0, ANN_HIST_CONFIG["xmax"], 1.0) + line.SetLineStyle(2) + line.Draw("same") + + can.SaveAs(pdf_name) + can.SaveAs(png_name) + else: + can = r.TCanvas("can_ann_score", "can_ann_score", 800, 600) + normalize(h_data) + normalize(h_mc) + ymax = 1.20 * max(h_data.GetMaximum(), h_mc.GetMaximum()) + h_data.SetMaximum(ymax) + h_data.SetTitle(f";{xtitle};normalized") + h_data.Draw("hist") + h_mc.Draw("hist same") + leg.Draw() + can.SaveAs(pdf_name) + can.SaveAs(png_name) + + +# ============================================================ +# Main +# ============================================================ +def main(): + ap = argparse.ArgumentParser(description="Plot ANN response score for data and data-like MC.") + ap.add_argument("--mass", type=float, default=150.0, help="Mass hypothesis in MeV used to choose the ANN model. Default: 150") + ap.add_argument("--proj-fixed", type=float, default=10.0, help="Fixed proj_sig cut used in the common preselection. Default: 10") + ap.add_argument("--outfile-base", type=str, default=None, help="Base name for output files, without extension") + ap.add_argument("--data-file", type=str, default=DATA_FILE, help="Data ROOT file") + ap.add_argument("--tritrig-file", type=str, default=TRITRIG_FILE, help="TriTrig ROOT file") + ap.add_argument("--wab-file", type=str, default=WAB_FILE, help="WAB ROOT file") + ap.add_argument("--tree-name", type=str, default=TREE_NAME, help="TTree name") + ap.add_argument("--do-ratio", action="store_true", help="Draw MC/data ratio panel instead of normalized overlays") + ap.add_argument("--debug", action="store_true", help="Print event counts and bookkeeping") + args = ap.parse_args() + + os.makedirs(OUTDIR, exist_ok=True) + + whichmass = 1.8 * args.mass / 3.0 + ann_model, ann_scaler_mean, ann_scaler_scale = load_ann_model(whichmass) + + X_data, mask_data = load_sample_for_ann(args.data_file, args.tree_name, args.proj_fixed) + X_tritrig, mask_tritrig = load_sample_for_ann(args.tritrig_file, args.tree_name, args.proj_fixed) + X_wab, mask_wab = load_sample_for_ann(args.wab_file, args.tree_name, args.proj_fixed) + + ann_data = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_data) + ann_tritrig = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_tritrig) + ann_wab = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_wab) + + del X_data, X_tritrig, X_wab + gc.collect() + + tritrig_sf = get_scale_factor("tritrig", LUMI_14272) + wab_sf = get_scale_factor("wab", LUMI_14272) + + h_data = make_hist_from_scores( + ann_data[mask_data], + np.ones(np.count_nonzero(mask_data), dtype=np.float64), + "data", + ANN_HIST_CONFIG["nbins"], + ANN_HIST_CONFIG["xmin"], + ANN_HIST_CONFIG["xmax"], + ) + h_tritrig = make_hist_from_scores( + ann_tritrig[mask_tritrig], + np.full(np.count_nonzero(mask_tritrig), tritrig_sf, dtype=np.float64), + "tritrig", + ANN_HIST_CONFIG["nbins"], + ANN_HIST_CONFIG["xmin"], + ANN_HIST_CONFIG["xmax"], + ) + h_wab = make_hist_from_scores( + ann_wab[mask_wab], + np.full(np.count_nonzero(mask_wab), wab_sf, dtype=np.float64), + "wab", + ANN_HIST_CONFIG["nbins"], + ANN_HIST_CONFIG["xmin"], + ANN_HIST_CONFIG["xmax"], + ) + + h_mc = h_tritrig.Clone("h_ann_score_mc_total") + h_mc.SetDirectory(0) + h_mc.Add(h_wab) + + outfile_base = args.outfile_base + if outfile_base is None: + outfile_base = f"ann_score_data_mc_m{int(round(args.mass))}" + + draw_comparison(h_data, h_mc, outfile_base, ANN_HIST_CONFIG["xtitle"], do_ratio=args.do_ratio or DO_RATIO) + + if args.debug: + print("Using output directory:", OUTDIR) + print("Using tritrig scale factor =", tritrig_sf) + print("Using wab scale factor =", wab_sf) + print("Data events after common preselection =", int(np.count_nonzero(mask_data))) + print("TriTrig events after common preselection =", int(np.count_nonzero(mask_tritrig))) + print("WAB events after common preselection =", int(np.count_nonzero(mask_wab))) + print("Saved plot base:", os.path.join(OUTDIR, outfile_base)) + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/make_maxZbi_grid_worker_v2.py b/tools/simp-search-tools/plot-making/make_maxZbi_grid_worker_v2.py new file mode 100644 index 00000000..0ceb3b7c --- /dev/null +++ b/tools/simp-search-tools/plot-making/make_maxZbi_grid_worker_v2.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +""" +make_maxZbi_grid_worker.py +""" + +import os +import sys +import glob +import math +import argparse + +import numpy as np +import matplotlib.pyplot as plt + + +ALL_SUFFIXES = [ + "ann", + "bdt", + "cut", + "isL1L1_ann", + "isL1L1_cut", + "isL2L2_ann", + "isL2L2_cut", + "isL3L3_ann", + "isL3L3_cut", + "isL1L2_ann", + "isL1L2_cut", + "isL2L3_ann", + "isL2L3_cut", +] + + +def parse_result_file(path): + vals = {} + with open(path, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split() + if len(parts) < 2: + continue + vals[parts[0]] = parts[1] + + try: + mass = float(vals["mass_MeV"]) + eps = float(vals["epsilon"]) + Val = int(float(vals["Val"])) + Val2 = int(float(vals["Val2"])) + if math.isnan(float(vals["S_yield"])): + S_yield = -1.0 + B_yield = -1.0 + Zbi = -1.0 + else: + S_yield = math.floor(float(vals["S_yield"])) + B_yield = float(vals["B_yield"]) + Zbi = float(vals["Zbi"]) + except KeyError as e: + raise RuntimeError(f"Missing key {e} in file {path}") + + return mass, eps, Val, Val2, S_yield, B_yield, Zbi + + +def collect_best_by_mass_eps(directory, suffix): + pattern = os.path.join(directory, f"m*_Val*_epsIdx*_projIdx*_{suffix}.txt") + files = sorted(glob.glob(pattern)) + if not files: + raise RuntimeError(f"No files found matching {pattern}") + + best = {} + bad = [] + + for path in files: + try: + mass, eps, Val, Val2, S_yield, B_yield, Zbi = parse_result_file(path) + except RuntimeError: + print(f"[{suffix}] WARNING — skipping malformed file: {path}") + continue + + if (B_yield < 0) or math.isnan(B_yield): + bad.append((path, B_yield)) + continue + if B_yield == 0: + continue + if S_yield <= 0: + continue + + key = (mass, eps) + if key not in best or Zbi > best[key]["Zbi"]: + best[key] = {"Zbi": Zbi, "Val": Val, "Val2": Val2, + "S": S_yield, "B": B_yield} + + if bad: + print(f"[{suffix}] WARNING — files with negative/NaN B_yield (skipped):") + for p, bv in bad: + print(f" {p} B_yield={bv}") + else: + print(f"[{suffix}] No negative/NaN B_yield files.") + + if not best: + raise RuntimeError( + f"[{suffix}] No valid (mass, epsilon) combinations found.") + + return best + + +def build_grids(best_dict): + masses = sorted({m for (m, _e) in best_dict.keys()}) + epsilons = sorted({e for (_m, e) in best_dict.keys()}) + nm, ne = len(masses), len(epsilons) + + mass_to_idx = {m: i for i, m in enumerate(masses)} + eps_to_idx = {e: i for i, e in enumerate(epsilons)} + + Z_grid = np.full((ne, nm), np.nan) + Val_grid = np.full((ne, nm), np.nan) + Val2_grid = np.full((ne, nm), np.nan) + S_grid = np.full((ne, nm), np.nan) + B_grid = np.full((ne, nm), np.nan) + + for (m, e), info in best_dict.items(): + i_m = mass_to_idx[m] + i_e = eps_to_idx[e] + Z_grid[i_e, i_m] = info["Zbi"] + Val_grid[i_e, i_m] = info["Val"] + Val2_grid[i_e, i_m] = info["Val2"] + S_grid[i_e, i_m] = info["S"] + B_grid[i_e, i_m] = info["B"] + + return (np.array(masses), np.array(epsilons), + Z_grid, Val_grid, Val2_grid, S_grid, B_grid) + + +def make_bin_edges_from_centers_lin(centers): + centers = np.asarray(centers) + if centers.size < 2: + raise ValueError("Need at least two centers to build edges.") + edges = np.empty(centers.size + 1) + edges[1:-1] = 0.5 * (centers[:-1] + centers[1:]) + edges[0] = centers[0] - 0.5 * (centers[1] - centers[0]) + edges[-1] = centers[-1] + 0.5 * (centers[-1] - centers[-2]) + return edges + + +def make_bin_edges_from_centers_log(centers): + centers = np.asarray(centers) + if centers.size < 2: + raise ValueError("Need at least two centers to build edges.") + if np.any(centers <= 0): + raise ValueError("Epsilon centers must be positive for log-scale edges.") + edges = np.empty(centers.size + 1) + edges[1:-1] = np.sqrt(centers[:-1] * centers[1:]) + edges[0] = centers[0] / np.sqrt(centers[1] / centers[0]) + edges[-1] = centers[-1] * np.sqrt(centers[-1] / centers[-2]) + return edges + + +def plot_2d_grid(masses, eps, grid, outfile, label, title="", + fmt="{:.2f}", cmap="cividis"): + x_edges = make_bin_edges_from_centers_lin(masses) + y_edges = make_bin_edges_from_centers_log(eps) + + fig, ax = plt.subplots(figsize=(9, 7)) + mesh = ax.pcolormesh(x_edges, y_edges, grid, shading="auto", cmap=cmap) + ax.set_yscale("log") + ax.set_ylim(eps.min(), eps.max()) + ax.set_xlabel("mass [MeV]") + ax.set_ylabel("epsilon") + if title: + ax.set_title(title) + + cbar = fig.colorbar(mesh, ax=ax) + cbar.set_label(label) + + norm = mesh.norm + cmap_fn = mesh.cmap + for i_e, eps_val in enumerate(eps): + for i_m, mass_val in enumerate(masses): + val = grid[i_e, i_m] + if np.isnan(val): + continue + rgba = cmap_fn(norm(val)) + r, g, b, _ = rgba + luminance = 0.299*r + 0.587*g + 0.114*b + ax.text(mass_val, eps_val, fmt.format(val), + ha="center", va="center", fontsize=7, + color="white" if luminance < 0.5 else "black") + + fig.tight_layout() + fig.savefig(outfile, dpi=150) + plt.close(fig) + print(f" Saved {outfile}") + + +def make_plots_for_suffix(indir, outdir, suffix): + subdir = os.path.join(outdir, suffix) + os.makedirs(subdir, exist_ok=True) + + best = collect_best_by_mass_eps(indir, suffix) + masses, epsilons, Z_grid, Val_grid, Val2_grid, S_grid, B_grid = build_grids(best) + + Val_cut_grid = 0.5 * (Val_grid / 25) + Val2_cut_grid = 50.0 * (Val2_grid / 10) + 1.0 + Val3_cut_grid = .797*(1-Val_grid/25.0)+.9983*(Val_grid/25.0) + + def out(name): + return os.path.join(subdir, name) + + plot_2d_grid(masses, epsilons, Z_grid, + outfile=out("maxZbi_grid.png"), + label="max Zbi", title=f"Max Zbi [{suffix}]") + + plot_2d_grid(masses, epsilons, Val_cut_grid, + outfile=out("ValCut_at_maxZbi_grid.png"), + label="z0 cut value") + + plot_2d_grid(masses, epsilons, Val2_cut_grid, + outfile=out("Val2Cut_at_maxZbi_grid.png"), + label="proj cut") + + plot_2d_grid(masses, epsilons, Val3_cut_grid, + outfile=out("Val3Cut_at_maxZbi_grid.png"), + label="z0 cut value") + + plot_2d_grid(masses, epsilons, S_grid, + outfile=out("Signal_at_maxZbi_grid.png"), + label="S_yield") + + plot_2d_grid(masses, epsilons, B_grid, + outfile=out("Background_at_maxZbi_grid.png"), + label="B_yield") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--task-id", type=int, required=True) + ap.add_argument("--indir", required=True) + ap.add_argument("--outdir", required=True) + args = ap.parse_args() + + suffix = ALL_SUFFIXES[args.task_id] + + make_plots_for_suffix(args.indir, args.outdir, suffix) + + +if __name__ == "__main__": + main() diff --git a/tools/simp-search-tools/plot-making/submit_maxZbi_grid.sh b/tools/simp-search-tools/plot-making/submit_maxZbi_grid.sh new file mode 100644 index 00000000..988f0b94 --- /dev/null +++ b/tools/simp-search-tools/plot-making/submit_maxZbi_grid.sh @@ -0,0 +1,30 @@ +#!/bin/bash +#SBATCH --time=00:10:00 +#SBATCH --mem=3000M +#SBATCH --job-name=hpstr +#SBATCH --account=hps:hps-prod +#SBATCH --partition=roma +#SBATCH --output=/sdf/scratch/users/r/rodwyer1/job.%A_%a.stdout +#SBATCH --array=0-12 +#SBATCH --ntasks=1 + +# Usage: +# sbatch submit_maxZbi_grid.sh +# +# Example: +# sbatch submit_maxZbi_grid.sh textfileoutALLTogether33026 my_plots + +INDIR="$1" +OUTDIR="$2" + +if [ -z "$INDIR" ] || [ -z "$OUTDIR" ]; then + echo "Usage: sbatch submit_maxZbi_grid.sh " + exit 1 +fi + +mkdir -p logs +mkdir -p "$OUTDIR" + +/sdf/group/hps/users/rodwyer1/sw/acts/ParOpt_PyEnv36/bin/python3 make_maxZbi_grid_worker_v2.py --task-id "$SLURM_ARRAY_TASK_ID" \ + --indir "$INDIR" \ + --outdir "$OUTDIR" diff --git a/tools/simp-search-tools/plot-making/write_roc_overlay_all3.py b/tools/simp-search-tools/plot-making/write_roc_overlay_all3.py new file mode 100644 index 00000000..311d542a --- /dev/null +++ b/tools/simp-search-tools/plot-making/write_roc_overlay_all3.py @@ -0,0 +1,429 @@ +#!/usr/bin/env python3 +import os +import sys +import gc +import math +import argparse +from pathlib import Path + +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt +import joblib +import torch +from torch import nn +from sklearn.metrics import roc_curve, auc + +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +HBAR_C = 1.973e-14 # GeV*cm + + +class ANNClassifier(nn.Module): + """Architecture matched to classifier_adv_2021_v9_pass5_run42QualCuts_*.pt.""" + def __init__(self, in_features: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 64, bias=False), + nn.BatchNorm1d(64), + nn.LeakyReLU(), + nn.Linear(64, 1), + ) + + def forward(self, x): + return self.net(x) + + +def _as_float32_col(arr): + return np.asarray(arr, dtype=np.float32).reshape(-1, 1) + + +def ann_predict_score(model, scaler_mean, scaler_scale, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + X_scaled = (X_chunk.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled) + with torch.no_grad(): + scores[start:end] = torch.sigmoid(model(X_tensor)).cpu().numpy().ravel() + return scores + + +def bdt_predict_score_batched(model, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end, :32], nan=0.0, posinf=0.0, neginf=0.0) + scores[start:end] = model.predict_proba(X_chunk)[:, 1].astype(np.float32, copy=False) + return scores + + +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) + + +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) + + +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def load_models(whichmass): + ann_model_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/classifier_adv_2021_v9_pass5_run42QualCuts_{int(whichmass)}.pt" + ann_scaler_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_arrays_{int(whichmass)}.npz" + bdt_model_path = "/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer_2426/bdt_model.joblib" + + sys.modules['numpy._core'] = np.core + ann_scaler = np.load(ann_scaler_path) + ann_scaler_mean = ann_scaler["mean"].astype(np.float32) + ann_scaler_scale = ann_scaler["scale"].astype(np.float32) + + ann_model = ANNClassifier(in_features=34) + ann_state = torch.load(ann_model_path, map_location="cpu") + ann_model.load_state_dict(ann_state) + ann_model.eval() + + bdt_model = joblib.load(bdt_model_path) + return ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model + + +def common_branch_list(): + return [ + "vertex.invM_", "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_", + "ele_L1_iso_significance", "pos_L1_iso_significance", + ] + + +def build_feature_matrix_from_bg_arrays(arrays): + """psum = ak.to_numpy(arrays["psum"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + + feats = [_as_float32_col(psum)] + for fld in vertex_pos.dtype.names: + feats.append(_as_float32_col(vertex_pos[fld]))""" + psum = ak.to_numpy(arrays["psum"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + + feats = [] + for fld in vertex_pos.dtype.names: + feats.append(_as_float32_col(vertex_pos[fld])) + feats.append(_as_float32_col(psum)) + + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"pos.track_.{key}"]))) + + feats.append(_as_float32_col(ak.to_numpy(arrays["vertex.chi2_"]))) + feats.append(_as_float32_col(proj_sig)) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["pos_L1_iso_significance"]))) + X = np.hstack(feats) + return X + + +def build_feature_matrix_from_sig_events(events): + """feats = [_as_float32_col(np.asarray(events["psum"]))] + feats.append(_as_float32_col(np.asarray(events["vertex.pos_.fX"]))) + feats.append(_as_float32_col(np.asarray(events["vertex.pos_.fY"]))) + feats.append(_as_float32_col(np.asarray(events["vertex.pos_.fZ"])))""" + + feats = [] + feats.append(_as_float32_col(np.asarray(events["vertex.pos_.fX"]))) + feats.append(_as_float32_col(np.asarray(events["vertex.pos_.fY"]))) + feats.append(_as_float32_col(np.asarray(events["vertex.pos_.fZ"]))) + feats.append(_as_float32_col(np.asarray(events["psum"]))) + + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"pos.track_.{key}"]))) + + feats.append(_as_float32_col(np.asarray(events["vertex.chi2_"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(np.asarray(events["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(np.asarray(events["pos_L1_iso_significance"]))) + X = np.hstack(feats) + return X + + +def derive_l1l1_from_ak(hit_layers): + if hit_layers is None: + return None + hasL0 = ak.to_numpy(ak.any(hit_layers == 0, axis=-1)) + hasL1 = ak.to_numpy(ak.any(hit_layers == 1, axis=-1)) + return np.asarray(hasL0 & hasL1, dtype=bool) + + +def derive_l1l1_from_events(events, side): + flag_key = f"{side}.hasL0L1" + if flag_key in events: + return np.asarray(events[flag_key], dtype=bool) + layers_key = f"{side}.track_.hit_layers_" + if layers_key in events: + layers_ak = ak.Array(events[layers_key]) + hasL0 = np.asarray((layers_ak == 0).any(axis=1), dtype=bool) + hasL1 = np.asarray((layers_ak == 1).any(axis=1), dtype=bool) + return hasL0 & hasL1 + return None + + +def load_background(base, mass_mev): + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + raise RuntimeError("Background module/path unavailable.") + + bg_path = bg.BACKGROUND_PATH + with uproot.open(bg_path) as f: + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + raise RuntimeError("No TTree found in background file.") + arrays = tree.arrays(common_branch_list(), library="ak", how=dict) + + invM = ak.to_numpy(arrays["vertex.invM_"]) + psum = ak.to_numpy(arrays["psum"]) + ele_z0 = ak.to_numpy(arrays["ele.track_.z0_"]) + pos_z0 = ak.to_numpy(arrays["pos.track_.z0_"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + + ele_L1L1 = derive_l1l1_from_ak(arrays.get("ele.track_.hit_layers_")) + pos_L1L1 = derive_l1l1_from_ak(arrays.get("pos.track_.hit_layers_")) + if ele_L1L1 is None: + ele_L1L1 = np.ones_like(invM, dtype=bool) + if pos_L1L1 is None: + pos_L1L1 = np.ones_like(invM, dtype=bool) + l1l1_mask = ele_L1L1 & pos_L1L1 + + X_bg = build_feature_matrix_from_bg_arrays(arrays) + return { + "invM": invM, + "psum": psum, + "ele_z0": ele_z0, + "pos_z0": pos_z0, + "proj_sig": proj_sig, + "l1l1_mask": l1l1_mask, + "X": X_bg, + } + + +def load_signal(base, mass_mev, epsilon): + mkey = base._mass_key(1.8 * mass_mev / 3.0) + events = base._events_cache(mkey) + + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + proj_sig = np.asarray(events["vtx_proj_sig"]) + + s_e_hasL0L1 = derive_l1l1_from_events(events, "ele") + s_p_hasL0L1 = derive_l1l1_from_events(events, "pos") + if s_e_hasL0L1 is None: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + if s_p_hasL0L1 is None: + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + l1l1_mask = s_e_hasL0L1 & s_p_hasL0L1 + + X_sig = build_feature_matrix_from_sig_events(events) + + alpha_D = 0.01 + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8 * mass_mev / 3.0 + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width / total_width + phi_fraction = phi_width / total_width + + rho_width_vis = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, 0.511, True) + phi_width_vis = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, 0.511, False) + rho_length = (1000 * HBAR_C * 10.0) / rho_width_vis + phi_length = (1000 * HBAR_C * 10.0) / phi_width_vis + + z_temp = np.asarray(events["true_vd.vtx_z_"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000 * psum_temp / m_V_D + z_temp = z_temp * (z_temp >= 0) + + frac_tot = rho_fraction + phi_fraction + rho_fraction /= frac_tot + phi_fraction /= frac_tot + + p_accept_temp = rho_fraction * np.exp(-z_temp / (gamma_temp * rho_length)) / (rho_length * gamma_temp) + p_accept_temp += phi_fraction * np.exp(-z_temp / (gamma_temp * phi_length)) / (phi_length * gamma_temp) + p_accept_temp /= np.max(p_accept_temp) + + return { + "psum": s_psum, + "ele_z0": ele_z0, + "pos_z0": pos_z0, + "proj_sig": proj_sig, + "l1l1_mask": l1l1_mask, + "X": X_sig, + "weights": p_accept_temp.astype(np.float64), + } + + +def build_common_mask(psum, l1l1_mask, proj_sig, proj_fixed): + psum_mask = (psum >= 1.5) & (psum <= 3.0) + proj_mask = (proj_sig < proj_fixed) + return psum_mask & l1l1_mask & proj_mask + + +def main(): + ap = argparse.ArgumentParser(description="Overlay ROC curves for cut-based, BDT, and ANN selections.") + ap.add_argument("--mass", type=float, default=150.0, help="A' mass in MeV. Default: 150") + ap.add_argument("--epsilon", type=float, default=1e-3, help="Kinetic mixing epsilon. Default: 1e-3") + ap.add_argument("--base-module", type=str, default="decayLength8sel", help="Signal base module name. Default: decayLength8sel") + ap.add_argument("--proj-fixed", type=float, default=10.0, help="Fixed proj_sig cut used as common preselection. Default: 1e9") + ap.add_argument("--outfile", type=str, default="roc_overlay_m150_eps1e-3.png", help="Output plot filename") + ap.add_argument("--debug", action="store_true", help="Enable debug printing") + args = ap.parse_args() + + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + whichmass = base._mass_key(1.8 * args.mass / 3.0) + ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model = load_models(whichmass) + + bg_data = load_background(base, args.mass) + sig_data = load_signal(base, args.mass, args.epsilon) + + bg_common = build_common_mask(bg_data["psum"], bg_data["l1l1_mask"], bg_data["proj_sig"], args.proj_fixed) + sig_common = build_common_mask(sig_data["psum"], sig_data["l1l1_mask"], sig_data["proj_sig"], args.proj_fixed) + + ann_scores_bg = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, bg_data["X"]) + ann_scores_sig = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, sig_data["X"]) + + bdt_scores_bg = bdt_predict_score_batched(bdt_model, bg_data["X"]) + bdt_scores_sig = bdt_predict_score_batched(bdt_model, sig_data["X"]) + + cut_scores_bg = np.minimum(np.abs(bg_data["ele_z0"]), np.abs(bg_data["pos_z0"])).astype(np.float32) + cut_scores_sig = np.minimum(np.abs(sig_data["ele_z0"]), np.abs(sig_data["pos_z0"])).astype(np.float32) + + del bg_data["X"], sig_data["X"] + gc.collect() + + y_true = np.concatenate([ + np.zeros(np.count_nonzero(bg_common), dtype=np.int8), + np.ones(np.count_nonzero(sig_common), dtype=np.int8), + ]) + sample_weight = np.concatenate([ + np.ones(np.count_nonzero(bg_common), dtype=np.float64), + sig_data["weights"][sig_common].astype(np.float64), + ]) + + ann_all = np.concatenate([ann_scores_bg[bg_common], ann_scores_sig[sig_common]]) + bdt_all = np.concatenate([bdt_scores_bg[bg_common], bdt_scores_sig[sig_common]]) + cut_all = np.concatenate([cut_scores_bg[bg_common], cut_scores_sig[sig_common]]) + + fpr_ann, tpr_ann, _ = roc_curve(y_true, ann_all, sample_weight=sample_weight) + fpr_bdt, tpr_bdt, _ = roc_curve(y_true, bdt_all, sample_weight=sample_weight) + fpr_cut, tpr_cut, _ = roc_curve(y_true, cut_all, sample_weight=sample_weight) + + auc_ann = auc(fpr_ann, tpr_ann) + auc_bdt = auc(fpr_bdt, tpr_bdt) + auc_cut = auc(fpr_cut, tpr_cut) + + fig, ax = plt.subplots(figsize=(7, 5.5)) + ax.plot(fpr_ann, tpr_ann, linewidth=2.0, label=f"ANN, AUC = {auc_ann:.4f}") + ax.plot(fpr_bdt, tpr_bdt, linewidth=2.0, label=f"BDT, AUC = {auc_bdt:.4f}") + ax.plot(fpr_cut, tpr_cut, linewidth=2.0, label=f"Cut-based min(|z0_e|,|z0_p|), AUC = {auc_cut:.4f}") + ax.plot([0.0, 1.0], [0.0, 1.0], linestyle="--", linewidth=1.2, label="Random") + ax.set_xlabel("Background efficiency (FPR)") + ax.set_ylabel("Signal efficiency (TPR)") + ax.set_title(f"ROC overlay, m = {args.mass:.0f} MeV, epsilon = {args.epsilon:.1e}") + ax.set_xlim(0.0, .0001) + #ax.set_xlim(0.0, 1) + ax.set_ylim(0.0, 1.0) + ax.grid(True, alpha=0.3) + ax.legend(loc="lower right", fontsize=9) + fig.tight_layout() + plt.savefig(args.outfile, dpi=200) + + if args.debug: + print(f"Background events after common preselection: {np.count_nonzero(bg_common)}") + print(f"Signal events after common preselection: {np.count_nonzero(sig_common)}") + print(f"ANN AUC: {auc_ann:.6f}") + print(f"BDT AUC: {auc_bdt:.6f}") + print(f"Cut AUC: {auc_cut:.6f}") + print(f"Saved plot to: {args.outfile}") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/slurm-running/bk_eff_selection.py b/tools/simp-search-tools/slurm-running/bk_eff_selection.py new file mode 100644 index 00000000..cf194669 --- /dev/null +++ b/tools/simp-search-tools/slurm-running/bk_eff_selection.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# See module docstring below for details. +""" +Compute the fraction of background events that pass a tight selection, per mass bin. + +Args (only these three are accepted): + --II : integer mass-bin index + --L : integer, total number of mass bins + --Val : integer, parameter controlling tight selection (e.g., z-threshold) + +Behavior: +- Reads a background ROOT file with a 'preselection' tree (same structure as your signal numerator files). +- Applies a mass window around target mass m(II,L) both to numerator and denominator. +- Applies a tight selection (parameterized by Val) only to the numerator. +- Writes the fraction (numerator/denominator) to an output txt file. + +Edit BACKGROUND_PATH to point to your background file if necessary. +""" + +import argparse +import numpy as np +import uproot +import awkward as ak + +BACKGROUND_PATH = "/sdf/group/hps/users/rodwyer1/run/reach_curves/datafiles/pres3/bigpreselectblind.root" +#"/sdf/data/hps/physics2021/preselection/v8/data_1pc_z0_calb_run_by_run/merged_hps_014536_job316.root" +#bigpreselectblind.root" +#"/sdf/group/hps/users/rodwyer1/run/reach_curves/datafiles/pres/bigpreselect.root" # change if needed +TREE_CANDIDATES = ["preselection", "preselection;1"] + +BRANCHES = [ + "psum", "vertex.invM_", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + # [HIT CATEGORY] TTree flag for hit category. Swap "isL1L1" for e.g. "isL2L2", "isL1L2" etc. to change category. + "isL1L1" +] + +VECTOR_BRANCHES = [ + "ele.track_.hit_layers_", + "pos.track_.hit_layers_", + #"ele.track_.lambda_kinks_", + #"pos.track_.lambda_kinks_", +] + +OUT_TMPL = "/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/outputText/bg_eff_output_I{II}_L{L}_V{Val}.txt" + +def _open_first_tree(file): + for name in TREE_CANDIDATES: + if name in file: + return file[name] + for _, obj in file.items(): + try: + if obj.classname.startswith("TTree"): + return obj + except Exception: + pass + raise KeyError("No TTree found (expected 'preselection').") + +def _extract_z_from_arrays(arrays): + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def _target_mass_mev(II, L): + #Altered to turn into mV, hopefully all that is required + #I dont think we divide for the scripts outside of this script (i.e. the plotting one) + #This is because the division only affects the mA epsilon/2pi (alpha) measurement. All aspects + #of the actual getFrac etc don't need it + #When we do histograms with actual abundance, this will change + + + #return (240.0 / float(L)) * float(II)/1.8 + return (240.0 / float(L)) * float(II) + +def _mass_window_mask(invM, mass_mev): + center = mass_mev/1000.0 # GeV + low = center - 10.0/1000.0 + high = center + 10.0/1000.0 + return (invM > low) & (invM < high) + +def _tight_selection_mask(events, Val): + #z = events["vertex.pos_.fZ"] + elez0 = events.get("ele.track_.z0_") + posz0 = events.get("pos.track_.z0_") + proj_sig = events.get("vtx_proj_sig") + psum = events.get("psum") + print(proj_sig) + #zthr = 2.0 * float(Val) * (1/25.0) + 1.5 + zthr=.5*(float(Val)*(1.0/25.0)) + mask = np.isfinite(psum) & ((posz0 > zthr)|(posz0 < -zthr)) & ((elez0 > zthr)|(elez0 < -zthr)) + # [HIT CATEGORY] Use TTree isL1L1 flag directly instead of deriving from hit_layers_. + # To switch hit category, swap "isL1L1" for e.g. "isL2L2", "isL1L2", etc. (must also update BRANCHES and main2 load above). + _hitcat = events.get("isL1L1") + if _hitcat is not None: + mask &= np.asarray(_hitcat, dtype=bool) + mask &= (psum>=1.5)&(psum<=3.0) + mask &= (proj_sig<1.6) + #(proj_sig<5.0*(float(Val)*(1/25.0))) + return mask + +def main2(): + ap = argparse.ArgumentParser() + ap.add_argument("--II", type=int, required=True) + ap.add_argument("--L", type=int, required=True) + ap.add_argument("--Val", type=int, required=True) + args = ap.parse_args() + + mass_mev = _target_mass_mev(args.II, args.L) + + with uproot.open(BACKGROUND_PATH) as f: + t = _open_first_tree(f) + arrays = t.arrays(BRANCHES + VECTOR_BRANCHES, library="ak") + + events = {} + zvals = _extract_z_from_arrays(arrays) + events["vertex.pos_.fZ"] = zvals + if "vertex.invM_" not in arrays.fields: + raise KeyError("vertex.invM_ not found in background file.") + events["vertex.invM_"] = np.asarray(ak.to_numpy(arrays["vertex.invM_"])) + events["vtx_proj_sig"] = np.asarray(ak.to_numpy(arrays["vtx_proj_sig"])) + events["ele.track_.z0_"] = np.asarray(ak.to_numpy(arrays["ele.track_.z0_"])) + events["pos.track_.z0_"] = np.asarray(ak.to_numpy(arrays["pos.track_.z0_"])) + events["psum"] = np.asarray(ak.to_numpy(arrays["psum"])) + + # [HIT CATEGORY] Load isL1L1 directly from TTree branch. + # To switch hit category, change "isL1L1" here and in BRANCHES above to e.g. "isL2L2", "isL1L2", etc. + if "isL1L1" in arrays.fields: + events["isL1L1"] = np.asarray(ak.to_numpy(arrays["isL1L1"]), dtype=bool) + + # ele.track_.hit_layers: does this event have hits on BOTH L0 and L1? + if "ele.track_.hit_layers" in arrays.fields: + ele_layers = arrays["ele.track_.hit_layers"] # ak.Array (jagged) + ele_has0 = ak.any(ele_layers == 0, axis=-1) + ele_has1 = ak.any(ele_layers == 1, axis=-1) + # Ensure no Nones and force boolean dtype + events["ele.hasL0"] = np.asarray(ak.fill_none(ele_has0, False), dtype=bool) + events["ele.hasL1"] = np.asarray(ak.fill_none(ele_has1, False), dtype=bool) + events["ele.hasL0L1"] = np.asarray(ak.fill_none(ele_has0 & ele_has1, False), dtype=bool) + + # (optional) positron side, same pattern: + if "pos.track_.hit_layers" in arrays.fields: + pos_layers = arrays["pos.track_.hit_layers"] + pos_has0 = ak.any(pos_layers == 0, axis=-1) + pos_has1 = ak.any(pos_layers == 1, axis=-1) + events["pos.hasL0L1"] = np.asarray(ak.fill_none(pos_has0 & pos_has1, False), dtype=bool) + + mask_mass = _mass_window_mask(events["vertex.invM_"], mass_mev) + + denom = int(np.sum(mask_mass)) + + mask_tight = _tight_selection_mask(events, args.Val) + numer = int(np.sum(mask_mass & mask_tight)) + + frac = float(numer) / float(denom) if denom > 0 else 0.0 + + outfile = OUT_TMPL.format(II=args.II, L=args.L, Val=args.Val) + with open(outfile, "w") as fo: + fo.write(f"{frac:.10g}\n") + + print(f"mass(MeV)={mass_mev:.3f}, Val={args.Val}, denom={denom}, numer={numer}, frac={frac:.6g}") + print(f"Wrote: {outfile}") + +if __name__ == "__main__": + main2() + diff --git a/tools/simp-search-tools/slurm-running/decayLength8sel.py b/tools/simp-search-tools/slurm-running/decayLength8sel.py new file mode 100644 index 00000000..e234e39d --- /dev/null +++ b/tools/simp-search-tools/slurm-running/decayLength8sel.py @@ -0,0 +1,655 @@ +import numpy as np +import matplotlib.pyplot as plt +import argparse +import uproot +import awkward as ak +import sys +from functools import lru_cache +import joblib # Added to load BDT model + +HBAR_C = 1.973e-14 # in GeV*cm + +L = 36 +MASSES = [25 + 5 * i for i in range(L)] +ALICLOC = "/sdf/data/hps/users/rodwyer1/alicSIMPs/gen/" +#/fs/ddn/sdf/group/hps/users/alspellm/projects/THESIS/mc/2021/simps/gen/recon/ +PRIORRECON = [ALICLOC+"slic/simps_3pt7/tuple_ana/files/hadd_mass_" + str(25 + 5 * i) + "_simp_slic_ana.root" for i in range(L)] +POSTRECON = [ALICLOC+"recon/simps_3pt7/tuple_ana/files/hadd_mass_" + str(25 + 5 * i) + "_simp_recon_ana.root" for i in range(L)] + +def beta(x, y): + return (1 + (y ** 2) - (x ** 2) - 2 * y) * (1 + (y ** 2) - (x ** 2) + 2 * y) + +def decay_length_dark_photon_to_vector_boson(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + """Sec 2.12-2.14: Partial width for Aprime to V_D + pi_D (returns width in GeV).""" + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (np.pi ** 4)) + ratio_terms = (m_Ap / m_pi_D) ** 2 * (m_V_D / m_pi_D) ** 2 * (m_pi_D / f_pi_D) ** 4 + width = prefactor * ratio_terms * m_Ap * beta(x, y) ** 1.5 + print(width) + return width + +def decay_length_dark_photon_to_pions(alpha_D, m_pi_D, m_V_D, m_Ap): + """Sec 2.9: Width for A' -> pi_D pi_D with phase space and VMD factor (returns width in GeV).""" + term1 = (1 - (4.0 * (m_pi_D ** 2)) / ((m_Ap ** 2))) + term2 = ((m_V_D ** 2) / ((m_Ap ** 2) - (m_V_D ** 2))) ** 2 + width = ((2.0 * alpha_D) / 3.0) * m_Ap * (term1 ** 1.5) * term2 + print(width) + return width + +def total_length(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + """Sum of widths for hidden-sector channels (returns total width in GeV).""" + rho = decay_length_dark_photon_to_vector_boson(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, 0.75) + phi = decay_length_dark_photon_to_vector_boson(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, 1.5) + invis = decay_length_dark_photon_to_pions(alpha_D, m_pi_D, m_V_D, m_Ap) + return rho + phi + invis + +def decay_length_vector_to_leptons(alpha_D, epsilon, f_pi_D, m_V, m_Ap, m_lep, is_rho=True): + """Leptonic width for V_D -> l+l-, convert to proper length L0 (cm).""" + F = ((m_V ** 2) / ((m_Ap ** 2) - (m_V ** 2))) ** 2 + PhaseSpace = np.sqrt(1.0 - ((4.0 * (m_lep ** 2)) / ((m_V ** 2)))) * (1.0 + (2.0 * (m_lep ** 2)) / (m_V ** 2)) + coeff = 1.0 if is_rho else 2.0 + width = coeff * ((16.0 * np.pi * alpha_D * (epsilon ** 2) * (f_pi_D ** 2)) / (3.0 * (m_V ** 2))) * F * PhaseSpace * (m_V/1000) + L0 = HBAR_C / width + return L0 + +# ==================== CACHED PSUM FOR ENERGY SAMPLING (unchanged behavior) ==================== + +@lru_cache(maxsize=None) +def _psum_hist_cache(index): + with uproot.open(POSTRECON[index]) as f: + hist = f["vtxana_kf_vtxSelection/vtxana_kf_vtxSelection_Psum_h"] + counts, edges = hist.to_numpy() + counts = np.asarray(counts, dtype=float) + total = counts.sum() + pdf = (np.ones_like(counts, dtype=float) / len(counts)) if total <= 0 else (counts / total) + return np.asarray(edges, dtype=float), pdf + +def getAEnergy(mV): + INDEX = int(sum([i * (MASSES[i] <= mV) * (MASSES[i + 1] > mV) for i in range(len(MASSES) - 1)])) + edges, pdf = _psum_hist_cache(INDEX) + bin_index = np.random.choice(len(pdf), p=pdf) + sample = float(np.random.uniform(edges[bin_index], edges[bin_index + 1])) + return sample + +##THE NEW WAY WE DO THINGS BELOW +PSUM_PATH_TMPL = "/sdf/data/hps/users/rodwyer1/SIMPS/allmassesD/psum{mass}.root" +PSUM_HIST_NAME = "h_psum_vector" +@lru_cache(maxsize=None) +def _gamma_cache(mass_key): + psum_path = PSUM_PATH_TMPL.format(mass=int(mass_key)) + with uproot.open(psum_path) as f: + h = f[PSUM_HIST_NAME] + edges = np.asarray(h.axes[0].edges()) + vals = np.asarray(h.values(),dtype=float) + return edges, vals + + + + +# ==================== NEW SIMP FILE STRUCTURE WITH CACHED I/O ==================== + +AVAILABLE_NEW_MASSES = [30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210] #[60, 90, 120, 150, 180, 210, 240] +#LOCATION = "/sdf/data/hps/users/rodwyer1/SIMPS/allmassesD/" +LOCATION = "/sdf/group/hps/users/rodwyer1/run/BigSIMPCollection2021/PRESELECTION/" +DEN_PATH_TMPL = LOCATION+"logger_{mass}.root" +DEN_HIST_NAME = "h_z_eepair" +#NUM_PATH_TMPL = LOCATION+"{mass}MeVpres.root" +NUM_PATH_TMPL = LOCATION+"simp{mass}v2.root" +#"simp{mass}pres.root" +NUM_TREE_CANDIDATES = ["preselection", "preselection;1"] + + +# Branches to load from numerator files (based on Rory's BDT backbone) +BRANCHES = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "true_vd.vtx_z_", + "ele_L1_iso_significance","pos_L1_iso_significance", + # [HIT CATEGORY] All hit category TTree flags loaded here so they are available in the events cache. + # The active category used in tight_selection is controlled separately below. + "isL1L1", "isL2L2", "isL3L3", "isL1L2", "isL2L3" +] +# Vector-like branches to load (kept in awkward; we reduce to per-event scalars/booleans) +VECTOR_BRANCHES = [ + "ele.track_.hit_layers_", + "pos.track_.hit_layers_", + #"ele.track_.lambda_kinks_", + #"pos.track_.lambda_kinks_", +] + +def _closest_available_mass(m): + return min(AVAILABLE_NEW_MASSES, key=lambda mm: abs(mm - float(m))) + +@lru_cache(maxsize=None) +def _mass_key(m): + return int(_closest_available_mass(float(m))) + +def _open_first_tree(file): + for name in NUM_TREE_CANDIDATES: + if name in file: + return file[name] + for _, obj in file.items(): + try: + if obj.classname.startswith("TTree"): + return obj + except Exception: + pass + raise KeyError("No TTree found (expected 'preselection').") + +@lru_cache(maxsize=None) +def _den_cache(mass_key): + """Cache denominator histogram once per mass.""" + den_path = DEN_PATH_TMPL.format(mass=int(mass_key)) + with uproot.open(den_path) as f: + h = f[DEN_HIST_NAME] + edges = np.asarray(h.axes[0].edges()) + vals = np.asarray(h.values(), dtype=float) + return edges, vals + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +@lru_cache(maxsize=None) +def _events_cache(mass_key): + """Load once per mass: all requested branches into numpy arrays; keep in memory for fast selections.""" + num_path = NUM_PATH_TMPL.format(mass=int(mass_key)) + with uproot.open(num_path) as f: + t = _open_first_tree(f) + arrays = t.arrays(BRANCHES + VECTOR_BRANCHES, library="ak") + events = {} + # z coordinate + zvals = _extract_z_from_arrays(arrays) + zvals = zvals[np.isfinite(zvals)] + events["vertex.pos_.fZ"] = zvals + + + events["ele.track_.z0_"] = np.asarray(ak.to_numpy(arrays["ele.track_.z0_"])) + events["pos.track_.z0_"] = np.asarray(ak.to_numpy(arrays["pos.track_.z0_"])) + + # --- Derived, per-event reductions from vector-like branches --- + # Convert jagged vectors into 1D numpy arrays that your tight_selection can use. + + # [HIT CATEGORY] Load all hit category flags from TTree. All are stored so write_final_yields + # can apply whichever it needs without reloading. To add a new category, add its branch name + # to BRANCHES above and include it in this loop. + for _cat in ["isL1L1", "isL2L2", "isL3L3", "isL1L2", "isL2L3"]: + if _cat in arrays.fields: + events[_cat] = np.asarray(ak.to_numpy(arrays[_cat]), dtype=bool) + + # ele.track_.hit_layers: does this event have hits on BOTH L0 and L1? + if "ele.track_.hit_layers_" in arrays.fields: + ele_layers = arrays["ele.track_.hit_layers_"] # ak.Array (jagged) + ele_has0 = ak.any(ele_layers == 0, axis=-1) + ele_has1 = ak.any(ele_layers == 1, axis=-1) + # Ensure no Nones and force boolean dtype + events["ele.hasL0"] = np.asarray(ak.fill_none(ele_has0, False), dtype=bool) + events["ele.hasL1"] = np.asarray(ak.fill_none(ele_has1, False), dtype=bool) + events["ele.hasL0L1"] = np.asarray(ak.fill_none(ele_has0 & ele_has1, False), dtype=bool) + #events["ele.track_.hit_layers_"] = arrays["ele.track_.hit_layers_"] + + # (optional) positron side, same pattern: + if "pos.track_.hit_layers_" in arrays.fields: + pos_layers = arrays["pos.track_.hit_layers_"] + pos_has0 = ak.any(pos_layers == 0, axis=-1) + pos_has1 = ak.any(pos_layers == 1, axis=-1) + events["pos.hasL0L1"] = np.asarray(ak.fill_none(pos_has0 & pos_has1, False), dtype=bool) + #events["pos.track_.hit_layers_"] = arrays["pos.track_.hit_layers_"] + + # ele.track_.lambda_kinks_: fixed-length (e.g. 14) vector per event -> reduce to a scalar + if "ele.track_.lambda_kinks_" in arrays.fields: + lam_e = arrays["ele.track_.lambda_kinks_"] + # Example reduction: max absolute kink per event + events["ele.lambda_kinks_maxabs"] = ak.to_numpy(ak.max(ak.abs(lam_e), axis=-1)) + + # pos.track_.lambda_kinks_: same pattern if you need it + if "pos.track_.lambda_kinks_" in arrays.fields: + lam_p = arrays["pos.track_.lambda_kinks_"] + events["pos.lambda_kinks_maxabs"] = ak.to_numpy(ak.max(ak.abs(lam_p), axis=-1)) + + # Other branches + for key in BRANCHES: + if key not in arrays.fields: + continue + a = arrays[key] + if hasattr(a, "fields") and len(ak.fields(a)) > 0: + for sub in ak.fields(a): + try: + subarr = ak.to_numpy(a[sub]) + if np.issubdtype(subarr.dtype, np.number): + events[f"{key}.{sub}"] = np.asarray(subarr) + except Exception: + continue + else: + try: + arr = ak.to_numpy(a) + if np.issubdtype(arr.dtype, np.number): + events[key] = np.asarray(arr) + except Exception: + pass + # Consistent length + lengths = [len(v) for v in events.values() if isinstance(v, np.ndarray)] + if not lengths: + raise RuntimeError("No usable branches were loaded from numerator file.") + N = min(lengths) + for k in list(events.keys()): + v = events[k] + if isinstance(v, np.ndarray) and len(v) != N: + events[k] = v[:N] + return events + +def preload_caches(mass_list=None): + masses = mass_list or AVAILABLE_NEW_MASSES + for m in masses: + mk = _mass_key(m) + _ = _den_cache(mk) + _ = _events_cache(mk) + _ = _gamma_cache(mk) + +# Load the trained BDT model once +BDT_MODEL_PATH = "/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer_2426/bdt_model.joblib" +try: + bdt_model = joblib.load(BDT_MODEL_PATH) +except Exception as e: + sys.stderr.write(f"[error] Could not load BDT model: {e}\n") + bdt_model = None + +def tight_selection(events, Val, Val2, hitcat="isL1L1"): + """ + Return a boolean mask of events to keep, using BDT score cut instead of z0 cut. + Available keys include those in BRANCHES, plus 'vertex.pos_.fZ'. + """ + z = events.get("vertex.pos_.fZ") + invM = events.get("vertex.invM_") + proj_sig = events.get("vtx_proj_sig") + elez0 = events.get("ele.track_.z0_") + posz0 = events.get("pos.track_.z0_") + psum = events.get("psum") + + # Base mask: finite z + mask = np.isfinite(z) + mask = np.asarray(mask, dtype=bool) + # [HIT CATEGORY] Use the hitcat parameter to select which TTree flag to apply. + # Pass hitcat=None to skip the hit category cut entirely (no-category mode). + # To switch category, pass e.g. hitcat="isL2L2". All categories must be loaded in BRANCHES/_events_cache. + _hitcat = events.get(hitcat) if hitcat is not None else None + if _hitcat is not None: + mask &= np.asarray(_hitcat, dtype=bool) + mask &= (psum>=1.5)&(psum<=3.0) + + print("How many survived L1L1 and psum: "+str(sum([m==True for m in mask]))) + + # Compute BDT score and apply BDT cut (replaces zval cut) + threshold_val = 1.0 - ((1.0-float(Val)/25.0)**3.0) + #1.0 * (float(Val) / 25.0) + if bdt_model is not None: + # Prepare feature matrix for BDT + features = [] + # vertex.invM_, psum + #features.append(events["vertex.invM_"]) + features.append(events["psum"]) + # vertex.pos_ fields + features.append(events["vertex.pos_.fX"]) + features.append(events["vertex.pos_.fY"]) + features.append(events["vertex.pos_.fZ"]) + # electron track features + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", + "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + features.append(events[f"ele.track_.{key}"]) + # positron track features + pos_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", + "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in pos_keys: + features.append(events[f"pos.track_.{key}"]) + # vertex chi2 and invMerr + features.append(events["vertex.chi2_"]) + #features.append(events["vertex.invMerr_"]) + # vertex projection features + features.append(events["vtx_proj_sig"]) + features.append(events["vtx_proj_x_sig"]) + features.append(events["vtx_proj_y_sig"]) + # Stack features into matrix + X = np.column_stack([f.reshape(-1) for f in features]) + + import gc + del features + gc.collect() + + X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0) + bdt_scores = bdt_model.predict_proba(X)[:,1] + print("The BDT scores are as follows: ") + print(bdt_scores) + print("The threshold: ") + print(threshold_val) + print("Val: ") + print(Val) + mask &= (bdt_scores > threshold_val) + print("Things above threshold") + print(mask.sum()) + else: + # If model not loaded, skip BDT cut (conservative: no additional cut) + pass + + projval=50.0*(float(Val2)/10)+3 + #1.0+np.max([0,float(Val2)/10.0-4.0])**3.0 + mask &= (proj_sig.20) + mask = np.isfinite(z) + # ensure base mask is explicit bool (optional but robust) + mask = np.asarray(mask, dtype=bool) + _ehas = events.get("ele.hasL0L1") + if _ehas is not None: + _ehas = np.asarray(_ehas, dtype=bool) # force bool, avoid object + mask &= _ehas + + _phas = events.get("pos.hasL0L1") + if _phas is not None: + _phas = np.asarray(_ehas, dtype=bool) # force bool, avoid object + mask &= _phas + mask &= (psum>=1.5)&(psum<=3.0) + #print("I am printing positron y0") + #print(posz0) + #print((posz0<-2.0*(float(Val)*(1.0/25.0))-1.5)) + #print(((posz0>2.0*(float(Val)*(1.0/25.0))+1.5))) + #print("Do I exceed threshold") + #print(((posz0>2.0*(float(Val)*(1.0/25.0))+1.5)|(posz0<-2.0*(float(Val)*(1.0/25.0))-1.5))) + #print("I am printing electron y0") + #print(elez0) + #print("Do I exceed threshold") + #print(((elez0>2.0*(float(Val)*(1.0/25.0))+1.5)|(elez0<-2.0*(float(Val)*(1.0/25.0))-1.5))) + #print("Am I below significance") + #print((proj_sig<1.8)) + #print(mask) + zval=.5*(float(Val)*(1.0/25.0)) + mask &=((posz0>zval)|(posz0<-zval)) + mask &=((elez0>zval)|(elez0<-zval)) + projval=2*(float(Val2)/10)+3 + #(float(Val)/25.0) + mask &= (proj_sig -100.0) & (invM < 0.18) + # Example additional cuts (uncomment to use): + # chi2 = events.get("vertex.chi2_") + # if chi2 is not None: mask &= (chi2 < 10.0) + # nhit_e = events.get("ele.track_.n_hits_") + # nhit_p = events.get("pos.track_.n_hits_") + # if nhit_e is not None: mask &= (nhit_e >= 12) + # if nhit_p is not None: mask &= (nhit_p >= 12) + return mask""" + +def read_den_hist(mass): + mk = _mass_key(mass) + return _den_cache(mk) + +def read_psum_hist(mass): + mk = _mass_key(mass) + return _gamma_cache(mk) + +def getFrac(mV, z,Val): + """ + Acceptance fraction at a given z: N_acc(z) / N_gen(z), + using cached numerator arrays and user-editable `tight_selection`. + """ + den_edges, den_vals = read_den_hist(mV) + if not (den_edges[0] <= z < den_edges[-1]): + return 0.0 + mk = _mass_key(mV) + events = _events_cache(mk) + mask = tight_selection(events,Val) + zvals = events["true_vd.vtx_z_"][mask] + #zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + i = np.searchsorted(den_edges, z, side="right") - 1 + Ngen = float(den_vals[i]) + Nacc = float(num_vals[i]) + print("z value is ") + print(zvals) + print("The number of entries in bins") + print(num_vals) + print("Which Bin") + print(i) + print("Number generated") + print(Ngen) + print("Number accepted") + print(Nacc) + print("\n") + return (Nacc / Ngen) if Ngen > 0 else 0.0 + +def plot_ratio_for_mass(mass, out_png=None, title=None): + """Quick validator: (numerator/denominator) vs z after tight_selection().""" + den_edges, den_vals = read_den_hist(mass) + mk = _mass_key(mass) + events = _events_cache(mk) + mask = tight_selection(events,Val) + zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + with np.errstate(divide="ignore", invalid="ignore"): + ratio = np.where(den_vals > 0, num_vals / den_vals, np.nan) + centers = 0.5 * (den_edges[:-1] + den_edges[1:]) + plt.figure(figsize=(7.2, 4.2)) + plt.step(centers, ratio, where="mid") + plt.xlabel("z [mm]") + plt.ylabel("acceptance N_acc / N_gen") + plt.title(title or f"Acceptance ratio (num/den), mass≈{_closest_available_mass(mass)} MeV") + plt.grid(True, alpha=0.3) + if out_png: + plt.savefig(out_png, dpi=140, bbox_inches="tight") + print(f"[plot] wrote {out_png}") + return centers, ratio + +# ==================== Physics driver code (unchanged) ==================== + +def getProb(z, epsilon, mA,Val): + """Sec 5.2: exponential decay along z, weighted by F(z).""" + mpi = mA / 3.0 + mV = mA / 1.8 + gamma1 = (getAEnergy(mV)*1000)/mV + gamma2 = (getAEnergy(mV)*1000)/mV + L0rho = decay_length_vector_to_leptons(.01, epsilon, mpi / (3.0 * 3.1415926), mV, mA, .000511, True) + L0phi = decay_length_vector_to_leptons(.01, epsilon, mpi / (3.0 * 3.1415926), mV, mA, .000511, False) + Lrho = gamma1 * L0rho * 10.0 + Lphi = gamma2 * L0phi * 10.0 + frac = getFrac(mV, z,Val) + print(frac) + print(gamma1) + print(gamma2) + print(Lrho) + print(Lphi) + prho = frac * np.exp(-z / Lrho) / Lrho + pphi = frac * np.exp(-z / Lphi) / Lphi + return prho, pphi + +def getSum(epsilon, mA,Val): + """Integral over z of getProb to obtain acceptance-weighted probabilities per channel.""" + Zmax = 240 + sR = 0.0 + sP = 0.0 + for z in range(Zmax): + pr, pp = getProb(z, epsilon, mA,Val) + sR += pr + sP += pp + return sR, sP + +def mass_func(m_V_D): + x = m_V_D/1000 + m = (-6860.03 + 299358 * x - 4087220 * (x * x) + 25209900 * (x ** 3) - 73485900 * (x ** 4) + 82579800 * (x ** 5)) / 82.9268041667 + return m/1000.0 + +def ratio(x): + x = x / 1000 + f = -.16647 + 8.0747 * x - 111.31 * x * x + 727.92 * (x ** 3) - 2241.3 * (x ** 4) + 2604.3 * (x ** 5) + A = .091562 - 10.339 * x + 256.94 * (x * x) - 1940.0 * (x ** 3) + 5812.8 * (x ** 4) - 5999.9 * (x ** 5) + if (x < .05) or (x > .25): + return 0.0 + return ((f / A)) + +def aprime_yield(mass_mev, eps2, scale_const, eot_norm=1.0, cap=None): + mA = float(mass_mev) + core = scale_const * ratio(mA) * mA * eps2 + print("Scale conts: " + str(scale_const)) + print("ratio: " + str(ratio(mA))) + print("mass: " + str(mA)) + print("eps2: " + str(eps2)) + print("core: " + str(np.log(core) / np.log(10.0)) + "\\n") + val = eot_norm * core + if cap is not None: + val = min(val, cap) + return val + +def parralel_aprime(I, LL, eps2_min=1e-10, eps2_max=1e-4): + Length = int(LL) + mA = (240.0 / float(Length)) * float(I) + eps2_vals = [10.0 ** (-4.0 - (6.0 * j) / float(Length)) for j in range(Length)] + Value = 3 * np.pi / (2 * 1 * (1 / 137.0459991)) + row = [aprime_yield(mA, e2, Value) for e2 in eps2_vals] + outpath = "/sdf/group/hps/users/rodwyer1/run/reach_curves/outputTxt/aprime_only_output" + str(I) + ".txt" + from functools import lru_cache as _sys + print(row) + with open(outpath, "w") as f: + _old = _sys.stdout + _sys.stdout = f + print(row) + _sys.stdout = _old + return row + +def makeBRplot(): + mA = 100.0 + mpi = mA / 3.0 + mV = mA / 1.8 + N = 100 + Xaxis = [3.0 * (1 - float(i) / N) + 4.0 * 3.1415926 * float(i) / N for i in range(int(N))] + rho = [decay_length_dark_photon_to_vector_boson(.01, ((mA / float(9)) * (1 / x)), mpi, mV, mA, 3.0 / 4.0) / total_length(.01, ((mA / float(9)) * (1 / x)), mpi, mV, mA) for x in Xaxis] + phi = [decay_length_dark_photon_to_vector_boson(.01, ((mA / float(9)) * (1 / x)), mpi, mV, mA, 3.0 / 2.0) / total_length(.01, ((mA / float(9)) * (1 / x)), mpi, mV, mA) for x in Xaxis] + plt.plot(Xaxis, rho, label='rho fraction') + plt.plot(Xaxis, phi, label='phi fraction') + plt.legend() + plt.show() + +def plotAbundancesB4Frac(): + N = 100 + Length = 100 + Xaxis = [3.0 * (1 - float(i) / N) + 4.0 * 3.1415926 * float(i) / N for i in range(int(N))] + rho = [decay_length_dark_photon_to_vector_boson(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0, 3.0 / 4.0) / total_length(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0) for x in Xaxis] + phi = [decay_length_dark_photon_to_vector_boson(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0, 3.0 / 2.0) / total_length(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0) for x in Xaxis] + Highfrho = np.array([[np.log(min([329.2643 * (10.0 * i) * (10 ** (-4.0 - (6.0 * j) / Length)) * rho[len(rho) - 1], 1.0])) for j in range(Length)] for i in range(Length)]) + Highfphi = np.array([[np.log(min([329.2643 * (10.0 * i) * (10 ** (-4.0 - (6.0 * j) / Length)) * phi[len(phi) - 1], 1.0])) for j in range(Length)] for i in range(Length)]) + x_vals = [10.0 * i for i in range(Length)] + y_vals = [10 ** (-4.0 - (6.0 * j) / float(Length)) for j in range(Length)] + Highfrho = Highfrho.T + X, Y = np.meshgrid(x_vals, y_vals) + plt.pcolormesh(X, Y, Highfrho, shading='auto', cmap='plasma') + plt.colorbar(label="Signal over Background") + plt.xlabel("Mass in 10 MeV") + plt.ylabel("Epsilon Squared") + plt.title("Rate of Signal to Background prior to Acceptance Effects") + plt.yscale("log") + plt.xscale("linear") + plt.show() + +def plotAbundances(): + N = 100 + Length = 100 + Xaxis = [3.0 * (1 - float(i) / N) + 4.0 * 3.1415926 * float(i) / N for i in range(int(N))] + rho = [decay_length_dark_photon_to_vector_boson(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0, 3.0 / 4.0) / total_length(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0) for x in Xaxis] + phi = [decay_length_dark_photon_to_vector_boson(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0, 3.0 / 2.0) / total_length(.01, (10.0 / float(9)) * (1 / x), 10.0 / 3.0, 10.0 / 1.8, 10.0) for x in Xaxis] + CON = 329.2643 + window = 4.0 + Highfrho = np.array([[np.log(min([CON * ratio((10.0 * i)) * mass_func(10.0*i)* window * (10.0 * i) * (10 ** (-4.0 - (6.0 * j) / Length)) * rho[len(rho) - 1] * getSum(np.sqrt(10 ** (-4.0 - (6.0 * j) / Length)), (10.0) * float(i))[0], 1.0])) for j in range(Length)] for i in range(Length)]) + Highfphi = np.array([[np.log(min([CON * ratio((10.0 * i)) * mass_func(10.0*i)* window * (10.0 * i) * (10 ** (-4.0 - (6.0 * j) / Length)) * phi[len(phi) - 1] * getSum(np.sqrt(10 ** (-4.0 - (6.0 * j) / Length)), (10.0) * float(i))[1], 1.0])) for j in range(Length)] for i in range(Length)]) + Highfsum = np.array([[Highfrho[j][i] + Highfphi[j][i] for i in range(Length)] for j in range(Length)]) + Highfsum = Highfsum.T + x_vals = [10.0 * i for i in range(Length)] + y_vals = [10 ** (-4.0 - (6.0 * j) / float(Length)) for j in range(Length)] + X, Y = np.meshgrid(x_vals, y_vals) + plt.pcolormesh(X, Y, Highfsum, shading='auto', cmap='plasma') + plt.colorbar(label="Signal over Background") + plt.xlabel("Mass in 10 MeV") + plt.ylabel("Epsilon Squared") + plt.title("Rate of Signal to Background with Acceptance Effects") + plt.yscale("log") + plt.xscale("linear") + plt.savefig("dingdop.png") + plt.show() + +def parralel(I, LL,Val): + mA = 100.0 + mpi = mA / 3.0 + mV = mA / 1.8 + N = 100 + Length = LL + Xaxis = [3.0 * (1 - float(i) / N) + 4.0 * 3.1415926 * float(i) / N for i in range(int(N))] + rho = [decay_length_dark_photon_to_vector_boson(.01, ((240.0 / float(Length)) * (1 / x)), ((240.0 / float(Length)) * I) / 3.0, ((240.0 / float(Length)) * I) / 1.8, ((240.0 / float(Length)) * I), 3.0 / 4.0) / total_length(.01, ((240.0 / float(Length)) * (1 / x)), ((240.0 / float(Length)) * I) / 3.0, ((240.0 / float(Length)) * I) / 1.8, ((240.0 / float(Length)) * I)) for x in Xaxis] + phi = [decay_length_dark_photon_to_vector_boson(.01, ((240.0 / float(Length)) * (1 / x)), ((240.0 / float(Length)) * I) / 3.0, ((240.0 / float(Length)) * I) / 1.8, ((240.0 / float(Length)) * I), 3.0 / 2.0) / total_length(.01, ((240.0 / float(Length)) * (1 / x)), ((240.0 / float(Length)) * I) / 3.0, ((240.0 / float(Length)) * I) / 1.8, ((240.0 / float(Length)) * I)) for x in Xaxis] + TOT = 1.0 + CON = 3 * np.pi / (2 * 1 * (1 / 137.0459991)) + Highfrho = [TOT * min([CON * ratio((240.0 / float(Length)) * float(I)) * ((240.0 / float(Length)) * I) * (10 ** (-4.0 - (6.0 * j) / Length)) * rho[len(rho) - 1] * getSum(np.sqrt(10 ** (-4.0 - (6.0 * j) / Length)), (240.0 / float(Length)) * float(I),Val)[0], 1.0]) for j in range(Length)] + Highfphi = [TOT * min([CON * ratio((240.0 / float(Length)) * float(I)) * ((240.0 / float(Length)) * I) * (10 ** (-4.0 - (6.0 * j) / Length)) * phi[len(phi) - 1] * getSum(np.sqrt(10 ** (-4.0 - (6.0 * j) / Length)), (240.0 / float(Length)) * float(I),Val)[1], 1.0]) for j in range(Length)] + Highfsum = [Highfrho[j] + 1.0 * Highfphi[j] for j in range(Length)] + with open("/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/outputText/output" + str(I) + "_"+str(Val)+".txt", "w") as f: + sys.stdout = f + print(Highfsum) + sys.stdout = sys.__stdout__ + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Sample script with command line arguments.") + parser.add_argument('--II', type=int, required=False, help='Index Of Mass') + parser.add_argument('--L', type=int, required=False, help='Length of Array') + parser.add_argument('--Val', type=int, required=False, help='Value of Hyperparameter') + args = parser.parse_args() + #plt.plot([float(i) for i in range(100)],[getProb(float(i), .0005, 60) for i in range(100)],"r") + #plt.savefig("Helper.png") + + #plot_ratio_for_mass(55,"hello_ding_10232025.png") + #makeBRplot() + #plotAbundances() + #parralel_aprime(args.II,args.L) + parralel(args.II,args.L,args.Val) diff --git a/tools/simp-search-tools/slurm-running/scan_yields_array_v9_ALL.sh b/tools/simp-search-tools/slurm-running/scan_yields_array_v9_ALL.sh new file mode 100644 index 00000000..438e1efa --- /dev/null +++ b/tools/simp-search-tools/slurm-running/scan_yields_array_v9_ALL.sh @@ -0,0 +1,61 @@ +#!/bin/bash +#SBATCH --time=03:00:00 +#SBATCH --mem=5000M +#SBATCH --job-name=hpstr +#SBATCH --account=hps:hps-prod +#SBATCH --partition=roma +#SBATCH --output=/dev/null +#SBATCH --array=0-12 + +#/dev/null +#/sdf/scratch/users/r/rodwyer1/job.%A_%a.stdout +set -euo pipefail + +# Usage: sbatch this_script.sh [VAL] +VAL="${1:-25}" +VAL2="${2:-3}" + +OUTDIR="textfileoutALLTogether_NowMCInvariant" +mkdir -p "${OUTDIR}" + +# Paths +PYTHON="/sdf/group/hps/users/rodwyer1/sw/acts/ParOpt_PyEnv36/bin/python3" +SCRIPT="./write_final_yields_v9_ALL3.py" # adjust if needed +BASEMOD="decayLength8sel" + +# Map array index -> mass +# indices 0..12 -> masses 50..350 step 25 +IDX="${SLURM_ARRAY_TASK_ID}" +MASS=$((50 + 25 * IDX)) + +echo "SLURM_ARRAY_TASK_ID=${IDX} -> MASS=${MASS} MeV" + +# 12 log-spaced epsilon values between 1e-2 and 1e-5 (inclusive) +N_EPS=12 +N_VAL2=10 +LOG10_MAX=-2.0 +LOG10_MIN=-5.0 + +for ((i=0; i<${N_EPS}; i++)); do + for ((j=0; j<${N_VAL2}; j++)); do + # Compute epsilon (log-spaced) + EPS=$(awk -v i="$i" -v n="$N_EPS" -v lmax="$LOG10_MAX" -v lmin="$LOG10_MIN" 'BEGIN{ + exp10 = lmax + (lmin - lmax) * i / (n - 1.0); + val = exp(log(10.0) * exp10); + printf "%.8e", val; + }') + + OUTTXT="${OUTDIR}/m${MASS}_Val${VAL}_epsIdx${i}_projIdx${j}.txt" + + echo "Running mass=${MASS} MeV, eps=${EPS}, Val=${VAL},Val2=${j} -> ${OUTTXT}" + + "${PYTHON}" "${SCRIPT}" \ + --mass "${MASS}" \ + --epsilon "${EPS}" \ + --Val "${VAL}" \ + --Val2 "${j}" \ + --base-module "${BASEMOD}" \ + --outtxt "${OUTTXT}" + done +done + diff --git a/tools/simp-search-tools/slurm-running/scanner_run3.sh b/tools/simp-search-tools/slurm-running/scanner_run3.sh new file mode 100644 index 00000000..6f9fd518 --- /dev/null +++ b/tools/simp-search-tools/slurm-running/scanner_run3.sh @@ -0,0 +1,6 @@ +#/bin/bash +for I in $(seq 0 1 25); do + #sbatch scan_yields_array_v2.sh $I + #sbatch scan_yields_array_v8_ann.sh $I + sbatch scan_yields_array_v9_ALL.sh $I +done diff --git a/tools/simp-search-tools/slurm-running/write_final_yields_v9_ALL3.py b/tools/simp-search-tools/slurm-running/write_final_yields_v9_ALL3.py new file mode 100644 index 00000000..8e7d16b1 --- /dev/null +++ b/tools/simp-search-tools/slurm-running/write_final_yields_v9_ALL3.py @@ -0,0 +1,930 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path +import joblib # Added to load BDT model +import gc +# ===== ANN CHANGE START ===== +import torch +from torch import nn +# ===== ANN CHANGE END ===== +print("I GOT HERE 1 31226") +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) +print("I GOT HERE 2 31226") +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B.""" + if B < 0: + return float('nan') + if B < 0.5: + return -1.0 + #return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" +print("I GOT HERE 3 31226") + +# ===== ANN CHANGE START ===== +class ANNClassifier(nn.Module): + """Architecture inferred from classifier_adv_2021_v9_pass5_run42QualCuts.pt.""" + def __init__(self, in_features: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 64, bias=False), + nn.BatchNorm1d(64), + nn.LeakyReLU(), + nn.Linear(64, 1), + ) + + def forward(self, x): + return self.net(x) + +def ann_predict_score(model, scaler_mean, scaler_scale, X): + '''X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0) + #X_scaled = scaler.transform(X) + X_scaled = (X.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled.astype(np.float32)) + with torch.no_grad(): + logits = model(X_tensor).squeeze(1) + scores = torch.sigmoid(logits).cpu().numpy() + return scores''' + batch_size = 100000 + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + + X_chunk = X[start:end] + X_chunk = np.nan_to_num(X_chunk) + + X_scaled = (X_chunk.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled) + + with torch.no_grad(): + chunk_scores = torch.sigmoid(model(X_tensor)).cpu().numpy().ravel() + + scores[start:end] = chunk_scores + + return scores + +# ===== LOW-RAM CHANGE START ===== +def _as_float32_col(arr): + return np.asarray(arr, dtype=np.float32).reshape(-1, 1) + +def bdt_predict_score_batched(model, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + scores[start:end] = model.predict_proba(X_chunk)[:, 1].astype(np.float32, copy=False) + return scores + +# ===== ORDER FIX START ===== +# The ANN scaler/model expect the ANN-notebook feature order: +# vertex_pos_x, vertex_pos_y, vertex_pos_z, psum, +# ele block, pos block, vertex_chi2, +# vtx_proj_sig, vtx_proj_x_sig, vtx_proj_y_sig, +# ele_L1_iso_significance, pos_L1_iso_significance. +# +# The BDT expects the training-script order: +# psum, vertex_pos_x, vertex_pos_y, vertex_pos_z, +# ele block, pos block, vertex_chi2, +# vtx_proj_sig, vtx_proj_x_sig, vtx_proj_y_sig, +# ele_L1_iso_significance, pos_L1_iso_significance. +# +# So we must build TWO matrices and never feed the ANN matrix into the BDT. +def build_ann_matrix_from_bg_arrays(arrays): + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + feats = [ + _as_float32_col(vertex_pos["fX"]), + _as_float32_col(vertex_pos["fY"]), + _as_float32_col(vertex_pos["fZ"]), + _as_float32_col(ak.to_numpy(arrays["psum"])), + ] + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"pos.track_.{key}"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vertex.chi2_"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["pos_L1_iso_significance"]))) + return np.hstack(feats) + +def build_bdt_matrix_from_bg_arrays(arrays): + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + feats = [ + _as_float32_col(ak.to_numpy(arrays["psum"])), + _as_float32_col(vertex_pos["fX"]), + _as_float32_col(vertex_pos["fY"]), + _as_float32_col(vertex_pos["fZ"]), + ] + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"pos.track_.{key}"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vertex.chi2_"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["pos_L1_iso_significance"]))) + return np.hstack(feats) + +def build_ann_matrix_from_sig_events(events): + feats = [ + _as_float32_col(np.asarray(events["vertex.pos_.fX"])), + _as_float32_col(np.asarray(events["vertex.pos_.fY"])), + _as_float32_col(np.asarray(events["vertex.pos_.fZ"])), + _as_float32_col(np.asarray(events["psum"])), + ] + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"pos.track_.{key}"]))) + feats.append(_as_float32_col(np.asarray(events["vertex.chi2_"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(np.asarray(events["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(np.asarray(events["pos_L1_iso_significance"]))) + return np.hstack(feats) + +def build_bdt_matrix_from_sig_events(events): + feats = [ + _as_float32_col(np.asarray(events["psum"])), + _as_float32_col(np.asarray(events["vertex.pos_.fX"])), + _as_float32_col(np.asarray(events["vertex.pos_.fY"])), + _as_float32_col(np.asarray(events["vertex.pos_.fZ"])), + ] + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"pos.track_.{key}"]))) + feats.append(_as_float32_col(np.asarray(events["vertex.chi2_"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(np.asarray(events["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(np.asarray(events["pos_L1_iso_significance"]))) + return np.hstack(feats) +# ===== ORDER FIX END ===== +# ===== LOW-RAM CHANGE END ===== +# ===== ANN CHANGE END ===== + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + fig, ax = plt.subplots(figsize=(6,4)) + ax.plot(np.linspace(0,2,100), rho_width, label="A'->rho pi", color="red") + ax.plot(np.linspace(0,2,100), phi_width, label="A'->phi pi", color="blue") + ax.plot(np.linspace(0,2,100), invis_width, label="A'->pi pi", color="green") + ax.set_xlabel("mpi_D / f_pi_D") + ax.set_ylabel("Width [GeV]") + ax.set_title("Partial widths vs f ratio") + ax.legend() + ax.set_yscale("log") + plt.savefig(outdir+"/rates.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR +print("I GOT HERE 4 31226") + +# ===== MULTISCAN CHANGE START ===== +def _suffix_outtxt(path, suffix): + root, ext = os.path.splitext(path) + if ext == "": + ext = ".txt" + return root + "_" + suffix + ext + +def build_bg_cutflow_from_masks(stage_names, stage_masks, total_events, mass_mask, initial_in, initial_out, N_b_massbin): + bg_cutflow_local = [] + for i, (stage, stage_mask) in enumerate(zip(stage_names, stage_masks)): + n_in = np.count_nonzero(stage_mask & mass_mask) + n_out = np.count_nonzero(stage_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + if i == 0: + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + frac_survive = (n_in / initial_in) if initial_in > 0 else 0.0 + frac_survive_out = (n_out / initial_out) if initial_out > 0 else 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow_local.append((stage, n_in, n_out, frac_in, frac_out, exp_yield_in, exp_yield_out)) + return bg_cutflow_local + +def build_sig_cutflow_from_masks(stage_names, stage_masks, acc_yield, vis_yield, total_sig_events, p_accept_temp): + sig_cutflow_local = [] + baseline_eff = (acc_yield / vis_yield) if vis_yield != 0 else 0.0 + sig_cutflow_local.append(("After acceptance", acc_yield, baseline_eff)) + for stage, stage_mask in zip(stage_names[1:], stage_masks[1:]): + frac_survive = np.sum(p_accept_temp[stage_mask]) / total_sig_events if total_sig_events > 0 else 0.0 + yield_stage = acc_yield * frac_survive + sig_cutflow_local.append((stage, yield_stage, frac_survive * 100.0)) + return sig_cutflow_local + +def build_sig_table_from_cutflows(sig_cutflow_local, bg_cutflow_local, bg_available): + sig_table_local = [] + if bg_available: + bg_yields = {row[0]: row[5] for row in (bg_cutflow_local or [])} + for stage, S_yield, _eff in sig_cutflow_local: + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table_local.append((stage, S_yield, B_yield, Zbi_val)) + else: + for stage, S_yield, _eff in sig_cutflow_local: + sig_table_local.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + return sig_table_local + +def write_sig_table_to_txt(outtxt_path, mass_mev, epsilon, Val, Val2, sig_table_local): + if sig_table_local: + final_stage, final_S, final_B, final_Z = sig_table_local[-1] + else: + final_stage, final_S, final_B, final_Z = ("After all cuts", 0.0, 0.0, 0.0) + with open(outtxt_path, "w") as fout: + fout.write(f"mass_MeV {mass_mev}\n") + fout.write(f"epsilon {epsilon}\n") + fout.write(f"Val {Val}\n") + fout.write(f"Val2 {Val2}\n") + fout.write(f"stage {final_stage}\n") + fout.write(f"S_yield {final_S:.6e}\n") + fout.write(f"B_yield {final_B:.6e}\n") + fout.write(f"Zbi {final_Z:.6f}\n") +# ===== MULTISCAN CHANGE END ===== + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow and final Zbi for signal/background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--base-module", type=str, default="decayLength8sel", help="Signal base module name.") # updated default base module + ap.add_argument("--outtxt", type=str, required=True, help="Output text file to store final yields and Zbi.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + # ===== ANN CHANGE START ===== + ap.add_argument("--ann-model", type=str, default="classifier_adv_2021_v9_pass5_run42QualCuts.pt", + help="Path to the trained ANN classifier .pt state_dict file.") + ap.add_argument("--ann-scaler", type=str, default="scaler_2021_v9_pass5_run42_QualCuts.pkl", + help="Path to the StandardScaler .pkl file used during ANN training.") + # ===== ANN CHANGE END ===== + args = ap.parse_args() + print("I GOT HERE 6 31226") + mass_mev = args.mass + epsilon = args.epsilon + Val = args.Val + + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + Val2 = args.Val2 + whichmass = base._mass_key(1.8*mass_mev/3.0) + # ===== ANN CHANGE START ===== + # Load the trained ANN model and the exact StandardScaler used during training. + ANN_MODEL_PATH = "/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/classifier_adv_2021_v9_pass5_run42QualCuts_"+str(int(whichmass))+".pt" + #ANN_SCALER_PATH = "/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_2021_v9_pass5_run42_QualCuts_proto4_"+str(int(mass_mev))+".pkl" + ANN_SCALER_PATH = "/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_arrays_"+str(int(whichmass))+".npz" + print("I GOT HERE 7 31226") + try: + sys.modules['numpy._core'] = np.core + ann_scaler = np.load(ANN_SCALER_PATH) + ann_scaler_mean = ann_scaler["mean"].astype(np.float32) + ann_scaler_scale = ann_scaler["scale"].astype(np.float32) + except Exception as e: + sys.stderr.write(f"[error] Could not load ANN scaler '{ANN_SCALER_PATH}': {e}\n") + sys.exit(1) + + print("I GOT HERE 7.5 31226") + try: + ann_model = ANNClassifier(in_features=34) + ann_state = torch.load(ANN_MODEL_PATH, map_location="cpu") + ann_model.load_state_dict(ann_state) + ann_model.eval() + except Exception as e: + sys.stderr.write(f"[error] Could not load ANN model '{ANN_MODEL_PATH}': {e}\n") + sys.exit(1) + # ===== ANN CHANGE END ===== + print("I GOT HERE 8 31226") + + # ===== MULTISCAN CHANGE START ===== + # ===== MASS-DEPENDENT BDT LOAD FIX START ===== + BDT_MODEL_PATH = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer_31026_massdep/bdt_model_{int(whichmass)}.joblib" + try: + bdt_model = joblib.load(BDT_MODEL_PATH) + except Exception as e: + sys.stderr.write(f"[error] Could not load BDT model '{BDT_MODEL_PATH}': {e}\n") + sys.exit(1) + # ===== MASS-DEPENDENT BDT LOAD FIX END ===== + # ===== MULTISCAN CHANGE END ===== + # Import the signal base module (e.g., decayLength8sel.py) dynamically + + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + Mkey = base._mass_key(1.8*mass_mev/3.0) + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "vertex.invM_", "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_", + "ele_L1_iso_significance","pos_L1_iso_significance", + # [HIT CATEGORY] All hit category TTree flags loaded once here. + # To add a new category, add its branch name here and in the loops below. + "isL1L1", "isL2L2", "isL3L3", "isL1L2", "isL2L3" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + print("I GOT HERE 9 31226") + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + # [HIT CATEGORY] Build a mask dict for every hit category loaded above, plus a no-category entry. + # To add a new category, add its name to HIT_CATEGORIES in both places below. + # "nocat" = all-True mask (no hit category cut applied). + HIT_CATEGORIES = ["isL1L1", "isL2L2", "isL3L3", "isL1L2", "isL2L3"] + bg_hitcat_masks = {"nocat": np.ones(len(invM), dtype=bool)} + for _cat in HIT_CATEGORIES: + if _cat in arrays: + bg_hitcat_masks[_cat] = np.asarray(ak.to_numpy(arrays[_cat]), dtype=bool) + else: + bg_hitcat_masks[_cat] = np.ones(len(invM), dtype=bool) + # Keep L1L1_mask as an alias so existing code below that references it still works. + L1L1_mask = bg_hitcat_masks["isL1L1"] + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + + # ===== ANN CHANGE START ===== + # ===== ORDER FIX START ===== + # Build separate ANN and BDT matrices so each model sees the feature ordering it was trained on. + X_bg_ann = build_ann_matrix_from_bg_arrays(arrays) + X_bg_bdt = build_bdt_matrix_from_bg_arrays(arrays) + + #MAYBE FIX RAM DRAW + del arrays + gc.collect() + # ===== ORDER FIX END ===== + + print("I GOT HERE 10 31226") + # Predict ANN score (probability of signal) for each event + ann_scores_bg = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_bg_ann) + print(ann_scores_bg) + threshold_val = 1.0 - ((1.0-float(Val)/25.0)**3.0) + #1.0 * (float(Val) / 25.0) + print(threshold_val) + ann_mask = (ann_scores_bg > threshold_val) + # ===== MULTISCAN CHANGE START ===== + # ===== LOW-RAM CHANGE NOTE ===== + # Run the BDT in batches directly from X_bg so we do not materialize a second full BDT feature matrix. + bdt_scores_bg = bdt_predict_score_batched(bdt_model, X_bg_bdt) + bdt_mask = (bdt_scores_bg > threshold_val) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + # ===== MULTISCAN CHANGE END ===== + # ===== I AM EDITING TO DECREASE RAM HERE START ===== + del X_bg_ann + del X_bg_bdt + del ann_scores_bg + # ===== MULTISCAN CHANGE START ===== + del bdt_scores_bg + # ===== MULTISCAN CHANGE END ===== + gc.collect() + # ===== I AM EDITING TO DECREASE RAM HERE END ===== + + + # ===== ANN CHANGE END ===== + + print("I GOT HERE 11 31226") + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = float(Mkey)/1000.0 + #1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # ===== MULTISCAN CHANGE START ===== + projval = 50.0*(float(Val2)/10)+3.0 + common_stage_names = ["No cuts", "After psum cut", "After psum+hitcat", "After psum+hitcat+proj", "After all cuts"] + + x_gev = mass_mev / (1000.0) + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) + N_B_TOTAL = 3.0e9 + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 + + # [HIT CATEGORY] Build cutflows for every category (and nocat). + # Each entry: bg_cutflows_ann[cat], bg_cutflows_bdt[cat], bg_cutflows_cut[cat]. + # "nocat" gets ann+bdt+cut; named categories get ann+cut only. + bg_cutflows_ann = {} + bg_cutflows_bdt = {} + bg_cutflows_cut = {} + for _cat, _hitmask in bg_hitcat_masks.items(): + _bg_nocuts = np.ones(total_events, dtype=bool) + _bg_psum = _bg_nocuts & psum_mask + _bg_hitcat = _bg_psum & _hitmask + _bg_proj = _bg_hitcat & (proj_sig < projval) + _masks_ann = [_bg_nocuts, _bg_psum, _bg_hitcat, _bg_proj, _bg_proj & ann_mask] + _masks_cut = [_bg_nocuts, _bg_psum, _bg_hitcat, _bg_proj, _bg_proj & z0_mask] + bg_cutflows_ann[_cat] = build_bg_cutflow_from_masks(common_stage_names, _masks_ann, total_events, mass_mask, initial_in, initial_out, N_b_massbin) + bg_cutflows_cut[_cat] = build_bg_cutflow_from_masks(common_stage_names, _masks_cut, total_events, mass_mask, initial_in, initial_out, N_b_massbin) + if _cat == "nocat": + _masks_bdt = [_bg_nocuts, _bg_psum, _bg_hitcat, _bg_proj, _bg_proj & bdt_mask] + bg_cutflows_bdt[_cat] = build_bg_cutflow_from_masks(common_stage_names, _masks_bdt, total_events, mass_mask, initial_in, initial_out, N_b_massbin) + # Legacy aliases so any downstream code referencing the old names still works. + bg_cutflow_ann = bg_cutflows_ann["nocat"] + bg_cutflow_bdt = bg_cutflows_bdt["nocat"] + bg_cutflow_cut = bg_cutflows_cut["nocat"] + bg_cutflow = bg_cutflow_ann + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + try: + events = base._events_cache(mkey) + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + z_temp = np.asarray(events["true_vd.vtx_z_"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print("z_temp: ") + print(z_temp) + z_temp=z_temp*(z_temp>=0) + print("gamma_temp: ") + print(gamma_temp) + print("rho_length: ") + print(rho_length) + print("exponand: ") + print(z_temp/(gamma_temp*rho_length)) + print("rho_fraction: ") + tot_frac=rho_fraction+phi_fraction + rho_fraction/=tot_frac + phi_fraction/=tot_frac + print(rho_fraction) + print("phi_fraction: ") + print(phi_fraction) + print("probability: ") + print(rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + print("max_p_accept_temp: ") + print(max(p_accept_temp)) + p_accept_temp/= max(p_accept_temp) + + '''rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print("uniform: ") + print(u) + print("comparison prob: ") + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + print("The length of events is: "+str(len(events["psum"]))) + #events = events[mask] + #print("GOT HERE")''' + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict) + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # [HIT CATEGORY] Build a mask dict for every hit category from the cached events dict, plus nocat. + # "nocat" = all-True (no hit category cut). Named categories use the TTree flag directly. + # To add a new category, add its name to HIT_CATEGORIES here (must also be in decayLength8sel BRANCHES). + HIT_CATEGORIES = ["isL1L1", "isL2L2", "isL3L3", "isL1L2", "isL2L3"] + sig_hitcat_masks = {"nocat": np.ones_like(s_psum, dtype=bool)} + for _cat in HIT_CATEGORIES: + if _cat in events: + sig_hitcat_masks[_cat] = np.asarray(events[_cat], dtype=bool) + else: + sig_hitcat_masks[_cat] = np.ones_like(s_psum, dtype=bool) + # Legacy alias so existing code referencing s_L1L1_mask still works. + s_L1L1_mask = sig_hitcat_masks["isL1L1"] + + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + # ===== ANN CHANGE START ===== + # ===== ORDER FIX START ===== + # Build separate ANN and BDT matrices so each model sees the feature ordering it was trained on. + s_threshold_val = .92*(1-float(Val)/25.0)+.9996*(float(Val)/25.0) + #1.0 - ((1.0-float(Val)/25.0)**6.0) + #1.0 * (float(Val) / 25.0) + X_sig_ann = build_ann_matrix_from_sig_events(events) + X_sig_bdt = build_bdt_matrix_from_sig_events(events) + gc.collect() + # ===== ORDER FIX END ===== + + ann_scores_sig = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_sig_ann) + ann_sig_mask = (ann_scores_sig > s_threshold_val) + s_threshold_val = 1.0 - ((1.0-float(Val)/25.0)**3.0) + # ===== MULTISCAN CHANGE START ===== + # ===== LOW-RAM CHANGE NOTE ===== + # Run the BDT in batches directly from X_sig so we do not materialize a second full BDT feature matrix. + bdt_scores_sig = bdt_predict_score_batched(bdt_model, X_sig_bdt) + bdt_sig_mask = (bdt_scores_sig > s_threshold_val) + z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + # ===== MULTISCAN CHANGE END ===== + # ===== I AM EDITING TO DECREASE RAM HERE START ===== + del X_sig_ann + del X_sig_bdt + del ann_scores_sig + # ===== MULTISCAN CHANGE START ===== + del bdt_scores_sig + # ===== MULTISCAN CHANGE END ===== + gc.collect() + # ===== I AM EDITING TO DECREASE RAM HERE END ===== + + + + # ===== ANN CHANGE END ===== + + projval = 50.0*(float(Val2)/10)+3.0 + #1.0+np.max([0,float(Val2)/10.0-4.0])**3.0 + s_proj_mask = (s_proj < projval) + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + print("Used this route") + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + + + print("scale_const: "+str(scale_const)) + print("ratio_val: "+str(ratio_val)) + print("mass_mev: "+str(mass_mev)) + print("epsilon: "+str(epsilon)) + + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + N_B_TOTAL = 3.0e9 + x_gev = mass_mev / 1000.0 + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.002 GeV window if no selection + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element): + alpha_D = 0.01 # fixed in decayLength8sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + den_edges, den_vals = base.read_den_hist(m_V_D) + mk = base._mass_key(m_V_D) + mask_hist = base.tight_selection(events,Val,Val2) # using updated tight selection logic upstream + #zvals = events["vertex.pos_.fZ"][mask_hist] + zvals = events["true_vd.vtx_z_"][mask_hist] + print(zvals) + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + for J in range(len(psum_vals)): + psum_val = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum_val/m_V_D + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + if(z_cent<0): + z_cent=0 + phi_length=(1000*HBAR_C*10.0)/phi_width + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + print("Nacc: "+ str(Nacc)) + print("zcent: "+str(z_cent)) + print("gamma times rho_length: "+str(gamma*rho_length)) + print("gamma: "+str(gamma)) + prho += psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi += psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("Signal processing complete") + + print("m_fraction: "+str(m_fraction)) + print("core: "+str(core)) + print("N_b_massbin: "+str(N_b_massbin)) + print("prho: "+str(prho)) + print("rho_fraction: "+str(rho_fraction)) + print("Prod_yield: "+str(prod_yield)) + print("Vis_yield: "+str(vis_yield)) + + # Now selection stages: + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + irint(threshold_val) + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # ===== MULTISCAN CHANGE START ===== + sig_stage_names = ["No cuts", "After psum cut", "After psum+hitcat", "After psum+hitcat+proj", "After all cuts"] + sig_mask_nocuts = np.ones(total_sig_events, dtype=bool) + sig_mask_psum = sig_mask_nocuts & s_psum_mask + + # [HIT CATEGORY] Build signal cutflows and write output files for every category. + # "nocat" produces ann + bdt + cut output files (3 files). + # Each named category produces ann + cut output files (2 files each, 5 categories = 10 files). + # Total: 3 + 10 = 13 output files. + # To add a new category, add its name to HIT_CATEGORIES in sig_hitcat_masks above; no changes needed here. + ann_outtxt = _suffix_outtxt(args.outtxt, "ann") + bdt_outtxt = _suffix_outtxt(args.outtxt, "bdt") + cut_outtxt = _suffix_outtxt(args.outtxt, "cut") + + try: + for _cat, _s_hitmask in sig_hitcat_masks.items(): + _sig_hitcat = sig_mask_psum & _s_hitmask + _sig_proj = _sig_hitcat & s_proj_mask + + _masks_ann = [sig_mask_nocuts, sig_mask_psum, _sig_hitcat, _sig_proj, _sig_proj & ann_sig_mask] + _masks_cut = [sig_mask_nocuts, sig_mask_psum, _sig_hitcat, _sig_proj, _sig_proj & s_z0_mask] + + _bg_ann = bg_cutflows_ann.get(_cat) if bg is not None else None + _bg_cut = bg_cutflows_cut.get(_cat) if bg is not None else None + + _cf_ann = build_sig_cutflow_from_masks(sig_stage_names, _masks_ann, acc_yield, vis_yield, total_sig_events, p_accept_temp) + _cf_cut = build_sig_cutflow_from_masks(sig_stage_names, _masks_cut, acc_yield, vis_yield, total_sig_events, p_accept_temp) + + _tbl_ann = build_sig_table_from_cutflows(_cf_ann, _bg_ann, bg is not None) + _tbl_cut = build_sig_table_from_cutflows(_cf_cut, _bg_cut, bg is not None) + + # Output file suffix: "ann" / "cut" for nocat; "_ann" / "_cut" for named categories. + _sfx_ann = "ann" if _cat == "nocat" else f"{_cat}_ann" + _sfx_cut = "cut" if _cat == "nocat" else f"{_cat}_cut" + write_sig_table_to_txt(_suffix_outtxt(args.outtxt, _sfx_ann), mass_mev, epsilon, Val, Val2, _tbl_ann) + write_sig_table_to_txt(_suffix_outtxt(args.outtxt, _sfx_cut), mass_mev, epsilon, Val, Val2, _tbl_cut) + + if _cat == "nocat": + # BDT only for no-category run + _masks_bdt = [sig_mask_nocuts, sig_mask_psum, _sig_hitcat, _sig_proj, _sig_proj & bdt_sig_mask] + _bg_bdt = bg_cutflows_bdt.get("nocat") if bg is not None else None + _cf_bdt = build_sig_cutflow_from_masks(sig_stage_names, _masks_bdt, acc_yield, vis_yield, total_sig_events, p_accept_temp) + _tbl_bdt = build_sig_table_from_cutflows(_cf_bdt, _bg_bdt, bg is not None) + write_sig_table_to_txt(_suffix_outtxt(args.outtxt, "bdt"), mass_mev, epsilon, Val, Val2, _tbl_bdt) + + except Exception as e: + sys.stderr.write(f"[error] Failed to write output text file set rooted at '{args.outtxt}': {e}\n") + sys.exit(1) + + # ===== LOW-RAM CHANGE START ===== + del sig_mask_nocuts, sig_mask_psum + gc.collect() + # ===== LOW-RAM CHANGE END ===== + # ===== MULTISCAN CHANGE END ===== + + if args.debug: + print(f"[done] Wrote nocat ANN result to {_suffix_outtxt(args.outtxt, 'ann')}") + print(f"[done] Wrote nocat BDT result to {_suffix_outtxt(args.outtxt, 'bdt')}") + print(f"[done] Wrote nocat cut result to {_suffix_outtxt(args.outtxt, 'cut')}") + for _cat in ["isL1L1", "isL2L2", "isL3L3", "isL1L2", "isL2L3"]: + print(f"[done] Wrote {_cat} ANN result to {_suffix_outtxt(args.outtxt, _cat+'_ann')}") + print(f"[done] Wrote {_cat} cut result to {_suffix_outtxt(args.outtxt, _cat+'_cut')}") + +if __name__ == "__main__": + print("I GOT HERE 5 31226") + main() + + + From c1844183cb17338294c9ec049620bfef8b30ca18 Mon Sep 17 00:00:00 2001 From: rodwyer100 Date: Tue, 7 Apr 2026 15:17:00 -0700 Subject: [PATCH 2/3] Adding training material for BDT and ANN --- tools/simp-search-tools/README.txt | 13 +- .../ann-training/ANN_NHP1_v5.ipynb | 2225 +++++++++++++++++ .../data_process_branches_only.ipynb | 182 ++ .../simp-search-tools/ann-training/mytools.py | 547 ++++ .../ann-training/scalershifting.py | 11 + .../simp-search-tools/ann-training/temp.ipynb | 314 +++ .../bdt-training/train_bdt_classifier.py | 200 ++ .../train_bdt_classifier_mass_dependent.py | 199 ++ 8 files changed, 3688 insertions(+), 3 deletions(-) create mode 100755 tools/simp-search-tools/ann-training/ANN_NHP1_v5.ipynb create mode 100755 tools/simp-search-tools/ann-training/data_process_branches_only.ipynb create mode 100755 tools/simp-search-tools/ann-training/mytools.py create mode 100755 tools/simp-search-tools/ann-training/scalershifting.py create mode 100755 tools/simp-search-tools/ann-training/temp.ipynb create mode 100644 tools/simp-search-tools/bdt-training/train_bdt_classifier.py create mode 100644 tools/simp-search-tools/bdt-training/train_bdt_classifier_mass_dependent.py diff --git a/tools/simp-search-tools/README.txt b/tools/simp-search-tools/README.txt index df0a5546..223e2fb2 100644 --- a/tools/simp-search-tools/README.txt +++ b/tools/simp-search-tools/README.txt @@ -31,6 +31,13 @@ write_roc_overlay_all3.py: Writes a comparative roc curve for the ann, bdt, and miny0 curves. Necessary to evaluate whether the slurm-running code is going to work (one often runs the long slurm job in there, finds one bungled the ann variable order in training and got very poor performance, and wasted 2 hours of slurm time and alot of machine resources if one doesn't do this first). ann-training -This repository contains the code used to train the ann to be adversarial to mass training. It only contains the most recent version (i.e. the one that does mixed background and signal samples). I use a special command to run these python notebooks; I will include said argument here too. - - +This repository contains the code used to train the ann to be adversarial to mass training. It only contains the most recent version (i.e. the one that does mixed background and signal samples). I use a special command to run these python notebooks; I will include said argument here too. The files included are ANN_NHP1_v5.ipynb, data_process_branches_only.ipynb, mytools.py, scalershifting.py, and temp.ipynb +ANN_NHP1_v5.ipynb: This file is the main python notebook which implements the ANN. It should be well documented; it takes a signal and background (background data and data-like MC) file and runs a classifying network pretraining, then adversary pretraining, then cotraining, followed by validation plots that indeed the mass remains relatively unshaped, that ROC AUC is high, and that the behavior on data and data-like MC is equivalent. +data_process_branches_only.ipynb: This file takes root files of signal, data, background, etc, and turns them into pickle files that can be read by the above python notebook for training +mytools.py: This file contains definitional classes and other useful tidbits for the ANN_HP1_v5 to use to define its ANN +scalershifting.py: This file creates and NPZ file that shapes input variables to be more useful for ANN discrimination; this improves stability in the ANN +temp.ipynb: This file converts the shaper output pickle file to an NPZ that can be used by the scanners (i.e. in actual tight selection) + +bdt-training +This repository only contains train-bdt-classifier.py and the mass dependent version. Really these just take a signal and background root files and does typical BDT mva training on them and produces a joblib file for later steps. Relatively simple, just need to provide the correct input and output files. + diff --git a/tools/simp-search-tools/ann-training/ANN_NHP1_v5.ipynb b/tools/simp-search-tools/ann-training/ANN_NHP1_v5.ipynb new file mode 100755 index 00000000..4ac334c8 --- /dev/null +++ b/tools/simp-search-tools/ann-training/ANN_NHP1_v5.ipynb @@ -0,0 +1,2225 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "c499d031", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using device: cpu\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pylab as plt\n", + "import torch \n", + "from torch import nn, optim\n", + "import torch.nn.functional as F\n", + "from torch.utils.data import TensorDataset, DataLoader\n", + "import sklearn\n", + "from sklearn.model_selection import train_test_split\n", + "import copy\n", + "import mytools\n", + "import os, random\n", + "from sklearn.preprocessing import StandardScaler\n", + "import joblib\n", + "\n", + "\n", + "\n", + "# Print and store device being used\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(\"Using device:\", device)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f5440b00", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "MASS = 135" + ] + }, + { + "cell_type": "markdown", + "id": "fe4c4da0", + "metadata": {}, + "source": [ + "# Define important parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8617f4c3", + "metadata": {}, + "outputs": [], + "source": [ + "# Number of class in invariant mass used by the adversary\n", + "Num_classes = 10\n", + "# Hyper-parameter for adversarial training of the classifier\n", + "lambda_ = 10.0\n", + "\n", + "run = 42\n", + "\n", + "# This ensures repeatability\n", + "def seed_everything(seed: int = 0):\n", + " os.environ[\"PYTHONHASHSEED\"] = str(seed)\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + "\n", + " torch.manual_seed(seed)\n", + " torch.cuda.manual_seed(seed)\n", + " torch.cuda.manual_seed_all(seed)\n", + "\n", + "seed_everything(int(run))" + ] + }, + { + "cell_type": "markdown", + "id": "38002755", + "metadata": {}, + "source": [ + "# Load and setup the data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "68ff500e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "data bkg: 4655616\n", + "data-like MC bkg: 258843\n", + "simp: 348767\n" + ] + } + ], + "source": [ + "# Read tritrig, wab, phiKK, and data files\n", + "df_bigpreselectblind = pd.read_pickle('bigpreselectblind.pk')\n", + "df_bigpreselectblind['PhiKK'] = 0.0 # Add label\n", + "df_simp150pres = pd.read_pickle('simp'+str(MASS)+'pres.pk')\n", + "df_simp150pres['PhiKK'] = 1.0 # Add label\n", + "\n", + "# Data-like MC files (hit-smear-kill tridents): used as background alongside real data.\n", + "df_wab_dm = pd.read_pickle('wab_HPS_Run2021Pass1_v9_14272_hitSmearKill_2000files.pk')\n", + "df_tritrig_dm = pd.read_pickle('tritrig_HPS_Run2021Pass1_v9_14272_hitSmearKill_1000files.pk')\n", + "df_wab_dm['PhiKK'] = 0.0 # background label – same as real data\n", + "df_tritrig_dm['PhiKK'] = 0.0\n", + "\n", + "# Combine data-like MC into one frame for convenience\n", + "df_mc_bkg = pd.concat([df_wab_dm, df_tritrig_dm], ignore_index=True, sort=False)\n", + "\n", + "print('data bkg: ', len(df_bigpreselectblind))\n", + "print('data-like MC bkg:', len(df_mc_bkg))\n", + "print('simp: ', len(df_simp150pres))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "831b9e02", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train size: 290260 Val size: 142965 Test size: 1105508\n", + "MC upweight factor: 28.778\n" + ] + } + ], + "source": [ + "# Split real data background and data-like MC background\n", + "df_bigpreselectblind_train, df_bigpreselectblind_test = train_test_split(df_bigpreselectblind, test_size=0.20, random_state=42)\n", + "df_mc_bkg_train, df_mc_bkg_test = train_test_split(df_mc_bkg, test_size=0.5, random_state=42)\n", + "df_simp150pres_train, df_simp150pres_test = train_test_split(df_simp150pres, test_size=0.5, random_state=42)\n", + "\n", + "# ── Build a 50/50 data/MC background by downsampling data to match MC count ──\n", + "# We want the background class to be half real data, half data-like MC.\n", + "# Since data >> MC, we subsample the data training pool to len(df_mc_bkg_train),\n", + "# then assign per-sample weights so each source contributes equally in expectation.\n", + "#\n", + "# Statistical reweighting:\n", + "# real data events : weight = 1.0\n", + "# MC bkg events : weight = N_data_train / N_mc_train (upweight to compensate for fewer MC)\n", + "#\n", + "# This means the classifier and mass-adversary losses treat MC events as more\n", + "# informative proportionally to how many fewer of them there are.\n", + "\n", + "N_mc_train = len(df_mc_bkg_train)\n", + "N_data_train = len(df_bigpreselectblind_train)\n", + "mc_weight = N_data_train / N_mc_train # upweight factor for MC bkg events\n", + "\n", + "# Subsample data background to match MC size for a balanced 50/50 mix\n", + "df_data_bkg_train_sub = df_bigpreselectblind_train.sample(n=N_mc_train, random_state=42)\n", + "\n", + "# Attach sample weights as a column before concatenation\n", + "df_data_bkg_train_sub = df_data_bkg_train_sub.copy()\n", + "df_mc_bkg_train = df_mc_bkg_train.copy()\n", + "df_simp150pres_train = df_simp150pres_train.copy()\n", + "\n", + "df_data_bkg_train_sub['sample_weight'] = 1.0\n", + "df_mc_bkg_train ['sample_weight'] = 1.0 #mc_weight\n", + "df_simp150pres_train ['sample_weight'] = 1.0 # signal weight; masked out in adv loss anyway\n", + "\n", + "# Do the same split arithmetic for validation — use the same weight ratio\n", + "N_mc_val = len(df_mc_bkg_test) # we'll take a val slice below\n", + "\n", + "# Combine and shuffle all training data\n", + "df_train = pd.concat([df_data_bkg_train_sub, df_mc_bkg_train, df_simp150pres_train],\n", + " ignore_index=True, sort=False)\n", + "df_train = df_train.sample(frac=1, random_state=42).reset_index(drop=True)\n", + "\n", + "one_hot_edges = mytools.get_one_hot_edges(df_bigpreselectblind.InvM, n_bins=Num_classes)\n", + "\n", + "# Split into train / val\n", + "df_train, df_val = train_test_split(df_train, test_size=0.33, random_state=42)\n", + "\n", + "# Test set: keep real data background only for the signal/background evaluation\n", + "# (consistent with the original — MC bkg test set used only for DM diagnostic below)\n", + "df_test = pd.concat([df_bigpreselectblind_test, df_simp150pres_test],\n", + " ignore_index=True, sort=False)\n", + "df_test = df_test.sample(frac=1, random_state=42).reset_index(drop=True)\n", + "\n", + "print('Train size:', len(df_train), ' Val size:', len(df_val), ' Test size:', len(df_test))\n", + "print(f'MC upweight factor: {mc_weight:.3f}')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "fbbee22e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mass window: [127.0, 143.0] MeV\n", + "Train events in window: 101853 / 290260\n", + "Val events in window: 50484 / 142965\n" + ] + } + ], + "source": [ + "# Make X and y for training and validation sets\n", + "# the adverserial network has its own labels\n", + "X_train = df_train.drop(columns=['InvM','PhiKK','sample_weight'])\n", + "y_train = df_train['PhiKK']\n", + "w_train = df_train['sample_weight'] # per-sample weights for classifier + adv losses\n", + "y_adv_train = mytools.get_adv_labels(df_train.InvM, one_hot_edges)\n", + "X_val = df_val.drop(columns=['InvM','PhiKK','sample_weight'])\n", + "y_val = df_val['PhiKK']\n", + "w_val = df_val['sample_weight']\n", + "y_adv_val = mytools.get_adv_labels(df_val.InvM, one_hot_edges)\n", + "\n", + "X_test = df_test.drop(columns=['InvM','PhiKK'])\n", + "y_test = df_test['PhiKK']\n", + "y_adv_test = mytools.get_adv_labels(df_test.InvM, one_hot_edges)\n", + "\n", + "# ── Classifier mass window mask (±8 MeV around the signal mass) ──\n", + "# The classifier BCE loss is only computed for events within this window.\n", + "# This prevents the classifier learning to identify the invariant mass region\n", + "# rather than genuine signal kinematics. InvM is in GeV; MASS is in MeV.\n", + "mass_window_GeV = 0.008 # 8 MeV half-width\n", + "mass_centre_GeV = MASS / 1000.0\n", + "\n", + "clas_mask_train = ((df_train.InvM >= mass_centre_GeV - mass_window_GeV) &\n", + " (df_train.InvM <= mass_centre_GeV + mass_window_GeV)).astype(np.float32).values\n", + "clas_mask_val = ((df_val.InvM >= mass_centre_GeV - mass_window_GeV) &\n", + " (df_val.InvM <= mass_centre_GeV + mass_window_GeV)).astype(np.float32).values\n", + "\n", + "print(f'Mass window: [{1000*(mass_centre_GeV-mass_window_GeV):.1f}, {1000*(mass_centre_GeV+mass_window_GeV):.1f}] MeV')\n", + "print(f'Train events in window: {clas_mask_train.sum():.0f} / {len(clas_mask_train)}')\n", + "print(f'Val events in window: {clas_mask_val.sum():.0f} / {len(clas_mask_val)}')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83113ee4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " vertex_pos_x vertex_pos_y vertex_pos_z psum ele_track_n_hits \\\n", + "210656 -1.081425 -0.125660 -0.676496 2.164677 1.149200 \n", + "63563 -2.369778 -3.677484 -2.471887 0.822169 1.909755 \n", + "85146 -1.480228 0.969679 -0.820242 0.223556 -0.371911 \n", + "236316 -0.375483 0.582120 -0.646878 -0.953043 0.388644 \n", + "23281 0.009208 0.492353 -0.403969 0.341588 1.149200 \n", + "... ... ... ... ... ... \n", + "95198 -0.047412 0.810264 0.243941 -0.146096 1.149200 \n", + "395800 -1.165446 0.036441 -0.543806 0.856691 1.149200 \n", + "25539 1.348141 2.919255 3.020415 0.030513 -1.132467 \n", + "354133 -0.296668 -0.988661 0.023149 1.004303 -0.371911 \n", + "181402 0.259531 -0.456845 1.386583 0.802062 0.388644 \n", + "\n", + " ele_track_d0 ele_track_phi0 ele_track_z0 ele_track_tan_lambda \\\n", + "210656 0.413980 -0.354893 0.043026 -0.920765 \n", + "63563 0.284706 -0.145466 0.078017 0.366379 \n", + "85146 -0.233592 -0.005871 0.114220 -1.257455 \n", + "236316 -0.067063 0.882817 0.146364 -0.584420 \n", + "23281 0.360787 -0.484863 0.333763 -1.168814 \n", + "... ... ... ... ... \n", + "95198 0.343246 -0.628594 -0.499084 1.111462 \n", + "395800 -0.048670 0.844698 0.127102 -0.883725 \n", + "25539 2.341738 -1.210330 -2.614362 1.072222 \n", + "354133 0.094136 -0.362935 0.055879 -0.617433 \n", + "181402 -0.910464 0.542111 -1.564123 0.886754 \n", + "\n", + " ele_track_px ... pos_track_chi2 pos_track_x_at_ecal \\\n", + "210656 0.207723 ... -1.040530 -1.132056 \n", + "63563 0.532350 ... 1.064635 -0.496841 \n", + "85146 -0.189412 ... 2.174597 -1.325758 \n", + "236316 1.204902 ... -0.149632 2.012071 \n", + "23281 -0.284438 ... -0.755176 -0.190854 \n", + "... ... ... ... ... \n", + "95198 -0.436642 ... 1.192794 1.258088 \n", + "395800 1.112815 ... -0.388935 -1.840603 \n", + "25539 -1.568814 ... 0.934260 0.260671 \n", + "354133 0.376804 ... -0.490239 -0.135642 \n", + "181402 0.662611 ... 0.240044 -1.521949 \n", + "\n", + " pos_track_y_at_ecal pos_track_z_at_ecal vertex_chi2 vtx_proj_sig \\\n", + "210656 0.842520 0.0 3.775134 -0.170755 \n", + "63563 -0.319954 0.0 -0.741171 3.700684 \n", + "85146 0.579324 0.0 -0.721968 1.104076 \n", + "236316 0.721592 0.0 1.272942 0.541553 \n", + "23281 0.902804 0.0 -0.536501 0.641808 \n", + "... ... ... ... ... \n", + "95198 -1.561283 0.0 -0.307469 -0.468911 \n", + "395800 0.774690 0.0 0.019744 -0.048586 \n", + "25539 -0.893422 0.0 3.679081 -0.413621 \n", + "354133 1.150578 0.0 -0.714812 0.839744 \n", + "181402 -0.882924 0.0 1.186311 -0.049485 \n", + "\n", + " vtx_proj_x_sig vtx_proj_y_sig ele_L1_iso_significance \\\n", + "210656 -1.046611 -0.310813 -0.111842 \n", + "63563 -0.314018 -4.043777 -0.095101 \n", + "85146 -1.743773 1.303804 0.451764 \n", + "236316 0.108556 0.914437 -0.114143 \n", + "23281 0.448393 0.996753 -0.106529 \n", + "... ... ... ... \n", + "95198 0.068958 -0.269252 0.451764 \n", + "395800 -1.455506 -0.022834 -0.112675 \n", + "25539 0.575250 -0.120700 0.451764 \n", + "354133 -0.352337 -1.448502 0.451764 \n", + "181402 -1.335571 0.090676 0.451764 \n", + "\n", + " pos_L1_iso_significance \n", + "210656 -0.085601 \n", + "63563 -0.087255 \n", + "85146 0.444980 \n", + "236316 -2.716457 \n", + "23281 -4.440900 \n", + "... ... \n", + "95198 0.444980 \n", + "395800 -0.067776 \n", + "25539 0.444980 \n", + "354133 0.444980 \n", + "181402 0.444980 \n", + "\n", + "[142965 rows x 34 columns]\n", + "Standardized 34 features using training-set statistics.\n" + ] + } + ], + "source": [ + "# Scall all input data\n", + "scaler = StandardScaler()\n", + "X_train = pd.DataFrame(scaler.fit_transform(X_train), columns=X_train.columns, index=X_train.index) # The scaler is fit to the the training data\n", + "X_val = pd.DataFrame(scaler.transform(X_val), columns=X_val.columns, index=X_val.index) # The same scaler is applied to validation and testing data \n", + "print(X_val)\n", + "X_test = pd.DataFrame(scaler.transform(X_test), columns=X_test.columns, index=X_test.index)\n", + "\n", + "# Save the scaler so it can be applied consistently during inference\n", + "joblib.dump(scaler, \"scaler_2021_v9_pass5_run\"+str(run)+\"_QualCuts_\"+str(MASS)+\"_v3.pkl\")\n", + "print(f\"Standardized {X_train.shape[1]} features using training-set statistics.\")" + ] + }, + { + "cell_type": "markdown", + "id": "5bf3bb65", + "metadata": {}, + "source": [ + "## Setup dataloaders" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c89314fb", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\Rory\\AppData\\Local\\Temp\\ipykernel_33224\\359436505.py:9: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at C:\\actions-runner\\_work\\pytorch\\pytorch\\pytorch\\torch\\csrc\\utils\\tensor_numpy.cpp:219.)\n", + " torch.from_numpy(clas_mask_train).unsqueeze(1))\n" + ] + } + ], + "source": [ + "bsize = 2000\n", + "\n", + "# Convert to tensor dataset and create dataloaders.\n", + "# clas_mask is carried in the dataset so the training loop can zero out\n", + "# classifier BCE loss for events outside the ±8 MeV mass window.\n", + "train_dataset = TensorDataset(\n", + " torch.from_numpy(X_train.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_train.to_numpy().astype(np.float32)).unsqueeze(1),\n", + " torch.from_numpy(clas_mask_train).unsqueeze(1))\n", + "train_loader = DataLoader(train_dataset, batch_size=bsize, shuffle=True)\n", + "\n", + "val_dataset = TensorDataset(\n", + " torch.from_numpy(X_val.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_val.to_numpy().astype(np.float32)).unsqueeze(1),\n", + " torch.from_numpy(clas_mask_val).unsqueeze(1))\n", + "val_loader = DataLoader(val_dataset, batch_size=bsize, shuffle=False)\n", + "\n", + "test_dataset = TensorDataset(torch.from_numpy(X_test.to_numpy().astype(np.float32)))\n", + "test_loader = DataLoader(test_dataset, batch_size=bsize, shuffle=False)\n" + ] + }, + { + "cell_type": "markdown", + "id": "41ed74e7", + "metadata": {}, + "source": [ + "## Initialize model, objective function, and optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3e8af74c", + "metadata": {}, + "outputs": [], + "source": [ + "clas = mytools.Classifier(in_features=X_train.shape[1]).to(device)\n", + "criterion_clas = nn.BCEWithLogitsLoss()\n", + "optimizer_clas = optim.Adam(clas.parameters(), lr=1e-2)\n", + "scheduler_clas = torch.optim.lr_scheduler.ExponentialLR(optimizer_clas, gamma=1.0)\n" + ] + }, + { + "cell_type": "markdown", + "id": "6c13d88a", + "metadata": {}, + "source": [ + "## Pre-train the classifier" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f5dd1357", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1\n", + "-------------------------------\n", + "Epoch 2\n", + "-------------------------------\n", + "Epoch 3\n", + "-------------------------------\n", + "Epoch 4\n", + "-------------------------------\n", + "Epoch 5\n", + "-------------------------------\n", + "Epoch 6\n", + "-------------------------------\n", + "Epoch 7\n", + "-------------------------------\n", + "Epoch 8\n", + "-------------------------------\n", + "Epoch 9\n", + "-------------------------------\n", + "Epoch 10\n", + "-------------------------------\n", + "Epoch 11\n", + "-------------------------------\n", + "Epoch 12\n", + "-------------------------------\n", + "Epoch 13\n", + "-------------------------------\n", + "Epoch 14\n", + "-------------------------------\n", + "Epoch 15\n", + "-------------------------------\n", + "Epoch 16\n", + "-------------------------------\n", + "Epoch 17\n", + "-------------------------------\n", + "Epoch 18\n", + "-------------------------------\n", + "Epoch 19\n", + "-------------------------------\n", + "Epoch 20\n", + "-------------------------------\n", + "Epoch 21\n", + "-------------------------------\n", + "Epoch 22\n", + "-------------------------------\n", + "Epoch 23\n", + "-------------------------------\n", + "Epoch 24\n", + "-------------------------------\n", + "Epoch 25\n", + "-------------------------------\n", + "Epoch 26\n", + "-------------------------------\n", + "Epoch 27\n", + "-------------------------------\n", + "Epoch 28\n", + "-------------------------------\n", + "Epoch 29\n", + "-------------------------------\n", + "Epoch 30\n", + "-------------------------------\n", + "Epoch 31\n", + "-------------------------------\n", + "Epoch 32\n", + "-------------------------------\n", + "Epoch 33\n", + "-------------------------------\n", + "Epoch 34\n", + "-------------------------------\n", + "Epoch 35\n", + "-------------------------------\n", + "Epoch 36\n", + "-------------------------------\n", + "Epoch 37\n", + "-------------------------------\n", + "Epoch 38\n", + "-------------------------------\n", + "Epoch 39\n", + "-------------------------------\n", + "Epoch 40\n", + "-------------------------------\n", + "Epoch 41\n", + "-------------------------------\n", + "Epoch 42\n", + "-------------------------------\n", + "Epoch 43\n", + "-------------------------------\n", + "Epoch 44\n", + "-------------------------------\n", + "Epoch 45\n", + "-------------------------------\n", + "Epoch 46\n", + "-------------------------------\n", + "Epoch 47\n", + "-------------------------------\n", + "Epoch 48\n", + "-------------------------------\n", + "Epoch 49\n", + "-------------------------------\n", + "Epoch 50\n", + "-------------------------------\n", + "Epoch 51\n", + "-------------------------------\n", + "Epoch 52\n", + "-------------------------------\n", + "Epoch 53\n", + "-------------------------------\n", + "Epoch 54\n", + "-------------------------------\n", + "Epoch 55\n", + "-------------------------------\n", + "Epoch 56\n", + "-------------------------------\n", + "Epoch 57\n", + "-------------------------------\n", + "Epoch 58\n", + "-------------------------------\n", + "Epoch 59\n", + "-------------------------------\n", + "Epoch 60\n", + "-------------------------------\n", + "Done!\n" + ] + } + ], + "source": [ + "# Implement early stopping in training loop\n", + "# Stop if validation loss has not decreased for the last [patience] epochs\n", + "# The model with the lowest validation loss is stored as final_classifier\n", + "# Only events within the ±8 MeV mass window contribute to the classifier BCE loss.\n", + "patience = 10\n", + "\n", + "BCE_pretrain = nn.BCEWithLogitsLoss(reduction='none')\n", + "\n", + "def train_clas_windowed(loader, model, optimizer, scheduler, device):\n", + " model.train()\n", + " total_loss = 0.0\n", + " for X_batch, y_batch, mask_batch in loader:\n", + " X_batch, y_batch, mask_batch = X_batch.to(device), y_batch.to(device), mask_batch.to(device)\n", + " optimizer.zero_grad()\n", + " out = model(X_batch)\n", + " per_sample = BCE_pretrain(out, y_batch) * mask_batch # zero outside window\n", + " loss = per_sample.sum() / mask_batch.sum().clamp(min=1)\n", + " loss.backward()\n", + " optimizer.step()\n", + " total_loss += loss.item()\n", + " scheduler.step()\n", + " return total_loss / len(loader)\n", + "\n", + "def validate_clas_windowed(loader, model, device):\n", + " model.eval()\n", + " total_loss = 0.0\n", + " with torch.no_grad():\n", + " for X_batch, y_batch, mask_batch in loader:\n", + " X_batch, y_batch, mask_batch = X_batch.to(device), y_batch.to(device), mask_batch.to(device)\n", + " out = model(X_batch)\n", + " per_sample = BCE_pretrain(out, y_batch) * mask_batch\n", + " loss = per_sample.sum() / mask_batch.sum().clamp(min=1)\n", + " total_loss += loss.item()\n", + " return total_loss / len(loader)\n", + "\n", + "Training_losses = np.array([])\n", + "Validation_losses = np.array([])\n", + "\n", + "epochs = 60\n", + "for t in range(epochs):\n", + " print(f\"Epoch {t+1}\\n-------------------------------\")\n", + "\n", + " Training_losses = np.append(Training_losses,\n", + " train_clas_windowed(train_loader, clas, optimizer_clas, scheduler_clas, device))\n", + " Validation_losses = np.append(Validation_losses,\n", + " validate_clas_windowed(val_loader, clas, device))\n", + "\n", + " # Keep a running copy of the model with the lowest loss\n", + " if Validation_losses[-1] == np.min(Validation_losses):\n", + " final_classifier = copy.deepcopy(clas)\n", + "\n", + " if len(Validation_losses) > patience:\n", + " if np.sum((Validation_losses[-1*np.arange(patience)-1] - Validation_losses[-1*np.arange(patience)-2]) < 0) == 0:\n", + " print(\"Stopping early!\")\n", + " break\n", + "\n", + "print(\"Done!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2618bc18", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGwCAYAAABB4NqyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAjMdJREFUeJztnQd4U/X3xt/u0tKWvffee08ZMkQFREVEQUQQFERBnAj8/qi4QFFQBEVQQBCUIUuGgOy9995QZktb2tI2/+d8b2+atmmbtGlG836e55Kbm5vk5rb0vjnnPed4GAwGAwghhBBC3AhPRx8AIYQQQoi9oQAihBBCiNtBAUQIIYQQt4MCiBBCCCFuBwUQIYQQQtwOCiBCCCGEuB0UQIQQQghxO7wdfQDOSEJCAq5evYqgoCB4eHg4+nAIIYQQYgHS2vD+/fsoVqwYPD3Tj/FQAJlBxE/JkiUtOdeEEEIIcTIuXbqEEiVKpLsPBZAZJPKjn8Dg4ODs+ekQ4mRERkaqb036l4DAwEBHHxIhhFhFeHi4CmDo1/H0oAAyg572EvFDAUTcBS8vL+O6/N5TABFCXBVL7Cs0QRNCCCHE7aAAIoQQQojbQQFECCGEELeDHiBCCMkE8fHxePjwIc8dIXbEx8cnmV8xK1AAEUKIlX1Grl+/jnv37vG8EeIA8uTJgyJFimS5Tx8FECGEWIEufgoVKoSAgAA2SyXEjl8+oqKiEBoaqu4XLVo0S69HAUQIIVakvXTxkz9/fp43QuxMrly51K2IIPl/mJV0GE3QhBBiIbrnRyI/hBDHoP//y6oHjwKIEEKshDMCCXH9/38UQIQQQghxOyiACCGEEOJ2UAARQgjJFGXKlME333xj8f4bNmxQ6Qu2ECDOAAWQHYmKjcPlu1EIvR9tz7clhLg5IjrSW8aOHZup1921axcGDhxo8f7NmjXDtWvXEBISguyEQou4jACaMmWK+ibh7++Pxo0bY+fOnenuv2DBAlSpUkXtX7NmTaxYsSLZ42n9J//yyy/hSH7ceBYtPl+PSWtPOfQ4CCHuhYgOfZGITXBwcLJtb7/9drJeK3FxcRa9bsGCBa2qiPP19bVJAztCcoQAmj9/PoYPH44xY8Zg7969qF27Njp27GhsdJSSrVu3olevXujfvz/27duHbt26qeXw4cPGfUz/Y8syY8YM9R+uR48ecCRB/lrbpfvRlv1xIYS4SHO22Di7L/K+liKiQ18k+iJ/D/X7x48fR1BQEFauXIn69evDz88PmzdvxpkzZ9C1a1cULlwYuXPnRsOGDbF27dp0U2Dyuj/99BO6d++uhFHFihWxdOnSNCMzM2fOVF19//nnH1StWlW9T6dOndTfbR0RY2+88YbaT3ovvfvuu+jbt6/6u59Z7t69iz59+iBv3rzqODt37oxTp5K+mF64cAFPPPGEejwwMBDVq1c3ftGW5/bu3VuJP+lJI5/xl19+yfSxEMfh8EaIEydOxIABA9CvXz91f+rUqVi+fLkSLe+9916q/SdNmqT+g4wcOVLdHzduHNasWYPJkyer5wryn9qUJUuWoE2bNihXrhwcSbC/j7q9H835QYTkFB48jEe10f/Y/X2P/l9HBPja7k+4/L396quv1N9JufBfunQJjz32GD755BMlin799VclCk6cOIFSpUql+Tr/+9//8MUXX6iI+3fffafEggiKfPnymd1fOvvK+/7222/w9PTECy+8oCJSc+bMUY9//vnnal1EhogkuQYsXrxY/U3PLC+99JISPCLOJBomoko+69GjR9Wsqddffx2xsbH477//lACS7SLOhI8++kjdF8FYoEABnD59Gg8ePMj0sRA3FUDyC7Znzx68//77xm3yH6B9+/bYtm2b2efIdokYmSIRI/kPYY4bN24oQTVr1qw0jyMmJkYtOuHh4cgOGAEihDgr//d//4dHH33UeF8Ei0TkdeTL5qJFi5RoGDJkSLriQqL0wqeffopvv/1W2Rrki6s5pJmdfHktX768ui+vLceiIyJKrhESVRLky25K24M16MJny5YtypMkiMAqWbKkuo4888wzuHjxosoYiMVCMP3yLI/VrVsXDRo0MEbBiGviUAF069Yt1VpeQqymyH0Jy6Y1h8fc/rLdHCJ8JLz71FNPpXkc48ePV99aspsgYwSIKTBCcgq5fLxUNMYR72tL9Au6TkREhDJHyxdISUlJKkoiHSIA0qNWrVrGdYmeSIQlLUuDICkoXfzo8530/cPCwtSX2EaNGhkfl9EHkqpLSEjI1Oc8duwYvL29ld9UR1JrlStXVo8JknIbPHgwVq9erb6QixjSP5dsl/ti2ejQoYNKxelCirgWDvcAZTeSSpMQrBim00K+Xch/NH2R0G92RoAiYiiACMkpiKdFUlH2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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot Training and Validation Loss\n", + "# Indicate where the final model stopped training\n", + "\n", + "best_epoch = np.argmin(Validation_losses)+1\n", + "\n", + "plt.plot(np.arange(len(Training_losses))+1,Training_losses,label=\"Training Loss\")\n", + "plt.plot(np.arange(len(Validation_losses))+1,Validation_losses,label=\"Validation Loss\")\n", + "plt.axvline(best_epoch,label=\"Final model stopped here\",color='k')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('BCE Loss')\n", + "#plt.yscale('log')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ba0d35cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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j9n2hx9+kKuuusp0jI2OjpZOnTrJQw89VONQ/u+++05+85vfSGpqqsTExJgRQqNHj5Yff/xR6kOPY9iwYabTeUpKitx1113mukjHs2rVKrnoooskKSlJTjvtNLnpppvk8OHDPut88803cuGFF5p1mjRpIhkZGfLvf//7mME0zzzzjPmc+nlbtmwpTzzxhM86RUVFcv/990ubNm3MOm3btpWZM2eKPxFuAAC20uvyeB9TpkyRhIQEn2V33nlnQEpcA8WsWbPk7rvvPuWTqfcz7Ny5U/73f/9XNmzYYIJJdV999ZX069fPjHKbP3++CSl6otdwpEFCl3t9+OGH8otf/MKc+N966y1Zt26d/P3vf5fExER58MEH63xcemFHDTa672XLlskbb7xh3k8DVW127dolQ4YMkY4dO8rXX38tCxcuNKFMw56XBp1LLrlEWrdubdZZunSpxMfHm4CjI5m8/vjHP8orr7xiAs769etl3rx50rdvX5/307C3ePFiefXVV025vfPOO9K5c2fxKyvE5OTk6JhtM7XTym0HrDb3fGid++fFtu4XQOg6cuSItXbtWjP1Ki8vt/KLShx56HvX12uvvWYlJib6LJsxY4bVpUsXKzo62urcubM1derUqteKioqsMWPGWGlpaeb11q1bWxMnTjSvtWnTxvx+ex86fzyvv/669Ytf/MI6dOiQFRcXZ2VmZvq8PmrUKOtXv/rVMdt99tlnZv8HDx6s9TM8//zzPucSLZszzjjD6tOnj1VWVuaz7urVqy2Px2M9+eSTZj4/P99KTk62rrjiihqP2/u+dbFgwQIrLCzMysrKqlo2bdo0KyEhwZRlTV566SUrJSXF5zj/85//mM/z008/mflvvvnGzFcvs6PX0e9mRESEtX79+lqP75///Kcpu/3795/0d/5kzt9c58ZmHuECWwD850hJmZzx0CJHinjtoxkSF3Vqpw2tpdBahRdeeEHOPvts0zSjTTGNGjWSUaNGyfPPP2/+9f/uu++aWoPt27ebh7eZRJtdXnvtNVOrcKLroGhNwW9/+1tTG3LppZeaGo361IrUZs+ePTJ37lzz/t5jWL16taxdu1befvvtY4bu9+jRw9SUaI3FPffcI4sWLZJ9+/aZGqWaaDOQlzbhaI1KbU15y5cvl7POOss0bXllZGTILbfcYmpjtIyPprVFekHI6scZ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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Now test the pretrained model \n", + "test_pred = mytools.test_clas(val_loader, final_classifier, device)\n", + "\n", + "test_pred=torch.sigmoid(torch.tensor(test_pred)).numpy()\n", + "\n", + "fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_val, test_pred, pos_label=1)\n", + "auc_test = sklearn.metrics.auc(fpr, tpr)\n", + "#print(f'Test AUROC: {auc_test:0.4f}')\n", + "\n", + "plt.plot(fpr, tpr, label=f'Test AUROC: {auc_test:0.4f}')\n", + "plt.xlabel('FPR')\n", + "plt.ylabel('TPR')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "83164ad3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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a6RFixaydu1aefHFF01wfeONN7zr6Hnpz64joN5//31TbrNmzZKmTZtKQFlhJisrS3v+mqmd1u08YrUY86F1+QvLbd0vgPCVl5dnbdy40Uw9SkpKrNz8QkdeeuzqevPNN63k5GS/ZbNmzbLat29vxcXFWe3atbOmT5/u/Sw/P98aOXKklZqaaj5v3ry5NXHiRPNZixYtzPe356XzlXnrrbesyy67zDp27JiVmJho7dq1y+/zYcOGWTfeeOMZ23366adm/0ePHq3wZ/jjH//ody3RsunQoYPVo0cPq7i42G/d9evXWxEREdbkyZPNfG5urtWgQQPrpptuKve8PcetiiVLlliRkZFWZmamd9mMGTOspKQkU5blef31162UlBS/8/zPf/5jfp4ff/zRzI8dO9b8LL4WLVpkxcfHW9nZ2Wb+tddes+rVq+d3nDFjxpjfqe+5tGrVyiooKDjnv/lzuX5znxu7QqJdOwKASuQVFkuHccscKaONEwZKYuz5XTa0lkJrFV599VXp2rWrqRXQpphatWrJsGHD5I9//KOpIZg/f740b95cMjIyzEv9+9//Ns0ub775pqlRONt9UGbPni2/+c1vTG3IddddZ2o0qlMrUpEDBw7IwoULzfE957B+/XrZuHGjvPvuu2cM3e/cubOpKfnb3/5mmnaWLVsmhw4dMjVK5dGmIo+WLVuaGpWKmvJWr14tl156qWna8hg4cKCMGDHC1MZoGZ9Oa4v0hpC+55mQkGCmK1euNDU6uo42NfnSdU6ePGlqaX7+85+bY19xxRVmX77HfuGFF+To0aNSr14987vs3bu3aZb6xz/+YZrrhgwZYsrBjscshHSz1PTp080vUAtSq/TWrFlT6fradtq+fXuzvv5SA912Vx0RYs8YfQBwo/Hjx8vLL78sN998s1x44YVmOmrUKHn99de9/Ue078nll19umjt0etttt5nP9MLoufhrHxDPfHl+/PFH00ykfWOUhhwNRed6246srCzzSAANYRokPv30U3PB1nnl6Sdz8cUXl7u9Lveso+em9Dp2Nq1bt5YGDRpU+HlmZqZfsFGNyub1s/JcddVV5jNtRtJmIw0inuYmbXrzhBRt5tJApk1f2hw3YcIEv3Wqcuxt27aZ5ijdh16rNVzq7/+5556TQHK85kbb8UaPHi0zZ840wUbbZ7VQtV1OE/rptLD1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7vLLLzdPmy4qKpL7779fHn/8cYrYRhVdB/XJ4Xl5eaa/UyBQcwNXmTx5sunsunDhQtOpEPbIycmRoUOHms7bDRo0oFgDqKSkxNSOvfHGG9K9e3dJT0+XJ554QmbOnEm520g7uWqN2GuvvSbr1q2TBQsWyOLFi+XZZ5+lnF2Ampsq0i/0qKgo2b9/v99ynU9NTS13G11enfVxbuXs8dJLL5lw8/HHH0unTp0oThv/nn/66SfZsWOHGSXhexFW0dHRsmXLFmndujVlfp7lrHSEVExMjNnO4+KLLzb/Atbml9jYWMrZhnJ+6qmnTGC/5557zLyOZs3NzZV7773XhElt1sL5q+g6mJSUFLBaG8Vvr4r0C0X/FbV8+XK/L3ed1/bx8uhy3/XVRx99VOH6OLdyVlOmTDH/4lq6dKn06NGDorT571lvZ7BhwwbTJOV53XDDDdK/f3/zXofR4vzLWfXt29c0RXnCo/rhhx9M6CHY2PP37Ombd3qA8QRKHrloH8eugwHtruzCoYY6dPCtt94yQ9ruvfdeM9QwMzPTfD506FDrscce8xsKHh0dbb300ktmiPL48eMZCh6Acp48ebIZAvr+++9b+/bt875ycnLs/yMI43I+HaOlAlPOu3btMqP9HnjgAWvLli3Whx9+aKWkpFjPPffcef7G3a265azfx1rOf/vb38xw5X/9619W69atzShXVEy/V/W2G/rSCDF16lTzfufOneZzLWMt69OHgj/yyCPmOqi37WAoeAjSMfrNmzc3F1Mdevjll196P7vyyivNF76v+fPnW23btjXr63C4xYsXO3DW7i7nFi1amP/JTn/plxfsK+fTEW4C8/esVq1aZW4boRdrHRb+/PPPm2H4sK+cCwsLraefftoEmvj4eCstLc367W9/ax09epRirsSnn35a7vetp2x1qmV9+jZdunQxvxf9e37zzTetQIvQ/wS2bggAACB46HMDAABchXADAABchXADAABchXADAABchXADAABchXADAABchXADAABchXADAABchXADAABchXADIOTdeeedEhERccZLHzDp+5k+QLFNmzYyYcIEKSoqMtuuWLHCb5uGDRvKoEGDzINAAbgT4QZAjXDttdfKvn37/F4XXnih32c//vij/P73v5enn35aXnzxRb/tt2zZYtZZtmyZ5Ofny+DBg6WgoMChnwZAIBFuANQIcXFxkpqa6veKiory+6xFixYyYsQIGTBggCxatMhv+5SUFLNOt27d5KGHHpKMjAzZvHmzQz8NgEAi3ABwnYSEhAprZbKysmTu3LnmvTZjAXCfaKdPAACq4sMPP5TatWt756+77jp57733/NaxLEuWL19ump4efPBBv8+aNWtmprm5uWZ6ww03SPv27Sl8wIUINwBqhP79+8uMGTO887Vq1Toj+BQWFkpJSYkMGTLE9Lvx9fnnn0tiYqJ8+eWXMnHiRJk5c2ZQzx9A8BBuANQIGmZ0JFRlwUebmZo0aSLR0Wd+tWnn47p160q7du3kwIEDkp6eLp999lkQzhxAsNHnBoBrgk/z5s3LDTanGzlypHz33XeycOHCoJwfgOAi3AAIO9o8NXz4cBk/frzppwPAXQg3AMLSAw88IJs2bTqjUzKAmi/C4p8tAADARai5AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAArkK4AQAA4ib/H+fhDniCBZLRAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Now test the pretrained model \n", + "test_pred = mytools.test_clas(test_loader, final_classifier, device)\n", + "\n", + "#bad_idx = np.where(~np.isfinite(test_pred))[0]\n", + "#print(bad_idx[:20])\n", + "\n", + "#X_test_arr = np.asarray(X_test)\n", + "#print(\"X_test nan count:\", np.isnan(X_test_arr).sum())\n", + "#print(\"X_test inf count:\", np.isinf(X_test_arr).sum())\n", + "\n", + "#print(\"bad rows in X_test:\")\n", + "#print(X_test_arr[bad_idx[:10]])\n", + "\n", + "#raw_pred = mytools.test_clas(test_loader, final_classifier, device)\n", + "#raw_pred_arr = np.asarray(raw_pred)\n", + "\n", + "#print(\"raw_pred nan count:\", np.isnan(raw_pred_arr).sum())\n", + "#print(\"raw_pred inf count:\", np.isinf(raw_pred_arr).sum())\n", + "#print(\"first bad raw pred indices:\", np.where(~np.isfinite(raw_pred_arr))[0][:20])\n", + "\n", + "#test_pred = torch.sigmoid(torch.tensor(raw_pred)).numpy()\n", + "#print(\"sigmoid pred nan count:\", np.isnan(test_pred).sum())\n", + "#print(\"first bad sigmoid indices:\", np.where(~np.isfinite(test_pred))[0][:20])\n", + "\n", + "test_pred=torch.sigmoid(torch.tensor(test_pred)).numpy()\n", + "\n", + "\n", + "#print(\"y_test nan count:\", np.isnan(np.asarray(y_test)).sum())\n", + "#print(\"test_pred nan count:\", np.isnan(np.asarray(test_pred)).sum())\n", + "\n", + "#print(\"y_test shape:\", np.asarray(y_test).shape)\n", + "#print(\"test_pred shape:\", np.asarray(test_pred).shape)\n", + "\n", + "#print(\"test_pred min/max:\", np.nanmin(test_pred), np.nanmax(test_pred))\n", + "\n", + "fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_test, test_pred, pos_label=1)\n", + "auc_test = sklearn.metrics.auc(fpr, tpr)\n", + "#print(f'Test AUROC: {auc_test:0.4f}')\n", + "\n", + "plt.plot(fpr, tpr, label=f'Test AUROC: {auc_test:0.4f}')\n", + "plt.xlabel('FPR')\n", + "plt.ylabel('TPR')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5540b09a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGwCAYAAABB4NqyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAgTBJREFUeJzt3Qd8U2X3B/DTvReUUfbeCDIFBw4UnCAKCPiKwB9fnAwFwVcUFygIooIgDsSB4ABRFFQQUdlbhgzZo0BL9175f34n3JC2aWlL2yTN7/v5xDT33iS3l0gO5znPc9xMJpNJiIiIiFyIu71PgIiIiKi8MQAiIiIil8MAiIiIiFwOAyAiIiJyOQyAiIiIyOUwACIiIiKXwwCIiIiIXI6nvU/AEeXk5MiZM2ckKChI3Nzc7H06REREVARY2jAxMVFq1Kgh7u6F53gYANmA4Kd27dpFudZERETkYE6ePCm1atUq9BgGQDYg82NcwODg4LL50yEiIqJSlZCQoAkM43u8MAyAbDCGvRD8MAAiIiJyLkUpX2ERNBEREbkcBkBERETkchgAERERkcthDRAREZVIdna2ZGZm8upRufHy8hIPD49SeS0GQEREVOy1Vs6ePStxcXG8clTuQkNDpXr16le8Th8DICIiKhYj+Klatar4+/tzwVgqt8A7JSVFzp8/r48jIiKu6PUYABERUbGGvYzgp3LlyrxyVK78/Pz0HkEQPoNXMhzGImgiIioyo+YHmR8iezA+e1daf8YAiIiIio19EsnZP3sMgIiIiMjlsAaIiIhKxem4VIlNzii3qxkW4C01Q801IUTFxQCIiIhKJfjpPn2tpGZml9vV9PPykFVPd6sQQVC9evVk1KhReisrn3zyib4+ly8wYwBERERXDJkfBD8z+7eVRlUDy/yK/ns+SUYt3qnvW9QA6OGHH5YFCxbIlClTZPz48Zbt3333ndx77706zbqisBVQ9e/fX+644w67npcjYQBERESlBsFPq5ohDntFfX195Y033pD//ve/EhYWJq42hdyYRk4sgnZoiTFpEnUiUe+JiOjKde/eXVcRRhaoMN9++620bNlSfHx8NJsyffr0Qo/ftWuX3HTTTRIUFCTBwcHSvn172bp1q2X/X3/9Jddff70GILVr15annnpKkpOTC3w9DFP93//9n1SpUkVf7+abb9b3sPbDDz9Ix44dNagLDw/XLBbceOONcvz4cRk9erTOmDJmTWEIDKsoW5szZ440bNhQvL29pWnTpvLZZ5/l2o/nfvjhh/ramH7euHFj+f7776Ui4CwwB4WgZ+GkjfLV5C16zyCIiOjKYeG8yZMny7vvviunTp2yecy2bdukX79+8sADD8ju3btl0qRJMnHiRA0gCjJo0CCpVauWbNmyRZ+PITb0rYLDhw9Lz5495b777pO///5bFi9erAHRE088UeDr9e3bVxf7W7Fihb5eu3bt5JZbbpGYmBjd/+OPP2pQgiGtHTt2yOrVq6VTp066b8mSJXouL7/8skRGRurNlqVLl8rIkSPl6aeflj179mhWbMiQIbJmzZpcx7300kt6PXDueD/8rsZ5ODUT5RMfH4+BYL23l/PHE0yz/rvatHHZYb3HYyIie0tNTTXt27dP763tPhVnqvvscr0vDyV5v8GDB5t69eqlP19zzTWmoUOH6s9Lly7Vv/MNAwcONN166625njt27FhTixYtCnztoKAg0yeffGJz37Bhw0yPPPJIrm1//vmnyd3d3XId69ata3rrrbcs+4KDg01paWm5ntOwYUPT+++/rz936dLFNGjQoALPx/r1DPPnzzeFhIRYHnft2tU0fPjwXMf07dvXdMcdd1ge47o8//zzlsdJSUm6bcWKFSZH+wwW9/ubGSAHF1TJ196nQERU4aAOCAXR//zzT7592Hbttdfm2obHhw4d0lYgtowZM0aHrDDE9vrrr2vWx4ChK2SPAgMDLbcePXpITk6OHD16NN9r4fikpCRtNWL9HBxrvO7OnTs1I3Ql/ing98x7Ta666irLzwEBATokZ/TjcmYsgiYiIpdzww03aBAyYcIEnR12pTBMNnDgQB2awrDViy++KIsWLdJhKgQzGF5C3U9ederUybcNx6PR5++//55vn1HDU57FzF4Xh/Ks64IQvDk7BkBEROSSkKlp27atFv9aa968uaxbty7XNjxu0qRJoc03sR83FB8PGDBA5s+frwEQ6nf27dsnjRo1KtJ54fizZ8+Kp6enFmDbgqwM6n5Qs2MLipoLylbl/T0HDx6c6/ds0aKFuAIGQEREVKrr8zjL+7Ru3VoLet95551c21EUjNlVr7zyiq6ds2HDBpk1a5a89957Nl8nNTVVxo4dK/fff7/Ur19fi6tRDI2iZ3j22Wflmmuu0aJnDJNhGAkB0a+//qqvmxeG0bp06SK9e/eWqVOnalB15swZS+Fzhw4dNMOEITDM4EKxdlZWlvz000/6XoDA6Y8//tB9mMmGWWJ5jR07Voubr776an1PzCpDAfWqVavEFTAAIiKiUmlLgZWZsThhecH74X2vBGZKYVZW3gzMV199JS+88IIGQRiOwnEFDZUhK3ThwgV56KGH5Ny5cxps9OnTR2dPGdmatWvXyv/+9z+dCo/aYgQuCK5swRATghkcjwxPVFSUTt3HsF21atUsU92//vprPT9kslCXg/3WvxeG3fA+6enpNhd57N27t7z99tvy5ptv6mwwBG/IWuG1XYEbKqHtfRKOJiEhQUJCQiQ+Pl4/VPaA9X8wBf6mB5vJms/3S7/nOkqVOkF2ORciIkNaWpoW4+LLEuvPWGMvMLL3Z7A439/MABERUalAS4qK0JeLXAOnwRMREZHLYQBERERELodDYE4KrTHSkjLFN9CLiyUSEREVEwMgJ+4TlpWRI57e7jJw0jUMgoiIiIqBQ2BOCJkfBD8d7qin93hMRERERccAyImxTxgREVHJMAAiIiIil8MaICIiKh1xJ0VSLpTf1fSvLBJau1ze6tixY7rw3o4dO7R/WFnBatNxcXHy3Xffldl7kBkDICIiKp3gZ3YnkcyU8ruaXv4ij28utyCoPAIqtKYojwYN27dv175h6FmGVh7oWzZjxgwJDAy0HHPixAl59NFHZc2aNbodTVOnTJmiTVoB5z506FA5dOiQ3HTTTbJgwQKpVKmS7kNvss6dO8ucOXOkU6dO4ogYABER0ZVD5gfBT58PRMKblP0VjT4osmS4+X2dMAAqCNo4lDU0Vu3evbv2IkMzVrSPGDVqlGafvvnmGz0GneTvvPNO7UG2fv16iYyM1F5nXl5eMnnyZD0GjV1vvvlm7aWGn7EdfcVg+vTpcu211zps8AOsASIiotKD4KdG27K/lSDIwpc7OsD7+flJ5cqVNQhITk627P/www+lefPm2l+qWbNmBXZ/N+zZs0duv/12zY6gSel//vMfiY6OtuzPycnRbu6NGjXSjux16tSR1157Tfch+wPoxI7mp0YDUgQhaFJqQCPTp556SqpWrarndd1112nWxvD777/r81evXq1d4v39/aVr165y4MCBAs97+fLlGsjMnj1bmjZtqp3v586dK99++638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l5dVPymRYq8GTY4p2KOyNKjeXuT4d/Y+FSIiIvsHQIMHD5Zt27bpekAsgq54PPwDdBr8uanTJPD1++x9OkRERI4RAP3444/y888/y3XXcbXgiiQxJk3vPULRFf6sSHq6SEKyvU+LiIjIMWqAateuXWhndXIu6Arv6e0uW386pvd4TEREVNEVOwOEJqTjxo3T5qeoASLnhq7wAyddI2lJmRr84N6muJMiKRdE/CuLhNYu79MkIiKyfy+wlJQUadiwobbByFsEHRMTU5rnR+UUBOEGeQOgyvEmkd27JfPTx8TLO1kifQNFHvqeawMREZFrBUAzZ84smzMhxxMVJ299kC1umQvlsEeQBE3sK/emrhJZ/Ygsu3e5RARG2PsMiYiIyj4AyszMlLVr18rEiROlfv36JXtHch6JKeKbKWK6raHIL4clPttHUt3dRXIyJDY9lgEQERG5RhE0hru+/fbbsjsbciwxFxe7TNypd6dMBdQHERERVfRZYL1795bvvuPieBWej4+4fblC0rxEYrsO0k2zTv96aX/iOfudGxERUXnXADVu3FhefvllWbdunbRv314CArBw3iVPPfXUlZ4TOYDa77wtsen7ZPTW8fJwZR/pLCJPXP2EeISkyqj9H4ukxdv7FImIiMovAProo48kNDRUV4PGzZqbmxsDoArCs0oVCavUXlKO+suSQ0s0AGpeqbkkBsba+9SIiIjKPwA6evTolb8rOQXM8lrWa5nE/r1NZP5YCfcLl0RhAERERC5YA2TNZDLpjSp2EFQ/hDP+iIioYilRAPTpp59K69atxc/PT29XXXWVfPbZZ6V/dmTX3mBRJxItPcKIiIhceghsxowZug7QE088Iddee61u++uvv2TEiBESHR0to0ePLovzpHK2Yu5uvUd/sD4DK/H6ExGRawdA7777rsyZM0ceeughy7Z77rlHWrZsKZMmTWIAVIF0uKOeNklNS825tDEqztweg4iIyJUCoMjISOnatWu+7diGfVQxusNDlTpBufZl4c/36RnyVk62yNVxIs3sdJJERETlXQPUqFEj+eqrr/JtX7x4sa4RRBWjOzxuRoNUQ1ZsrLilZ2p7DElItts5EhERlXsG6KWXXpL+/fvLH3/8YakBwqKIq1evthkYkfMpqDM8ERGRy2aA7rvvPtm0aZOEh4drSwzc8PPmzZvl3nvvLZuz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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df_test[\"Class_adv\"] = test_pred\n", + "df_bkg = df_test.loc[ (df_test.PhiKK == 0)]\n", + "per = np.percentile(df_bkg['Class_adv'],90)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "per = np.percentile(df_bkg['Class_adv'],95)\n", + "df_cut2 = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "per = np.percentile(df_bkg['Class_adv'],99)\n", + "df_cut3 = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "per = np.percentile(df_bkg['Class_adv'],99.5)\n", + "df_cut4 = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals = 1000*df_cut.InvM\n", + "x_vals2 = 1000*df_cut2.InvM\n", + "x_vals3 = 1000*df_cut3.InvM\n", + "x_vals4 = 1000*df_cut4.InvM\n", + "\n", + "plt.figure()\n", + "plt.hist(1000*df_bkg.InvM, bins=np.arange(40,300,1), histtype='step', label='No selection', density=True)\n", + "plt.hist(x_vals, bins=np.arange(40,300,1), histtype='step', label= 'selection 90%', density=True)\n", + "plt.hist(x_vals2, bins=np.arange(40,300,1), histtype='step', label= 'selection 95%', density=True)\n", + "plt.hist(x_vals3, bins=np.arange(40,300,1), histtype='step', label= 'selection 99%', density=True)\n", + "plt.hist(x_vals4, bins=np.arange(40,300,1), histtype='step', label= 'selection 99.5%', density=True)\n", + "#plt.axvline(1019.461, color='k', linestyle='dashed', linewidth=1, label='PDG phi mass')\n", + "plt.xlabel('Invariant Mass [MeV]')\n", + "plt.ylabel('Normalized Counts')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c5a88fcc", + "metadata": {}, + "outputs": [], + "source": [ + "# Save the pretrained classifier model\n", + "#torch.save(final_classifier.state_dict(), \"/Users/mghrear/data/ML_data/patch/pretrained_classifier_2021_v9_pass5_run\"+str(run)+\"_limited.pt\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "71598f50", + "metadata": {}, + "source": [ + "# Setup the adversary model" + ] + }, + { + "cell_type": "markdown", + "id": "b5712e93", + "metadata": {}, + "source": [ + "## Initialize model, objective function, and optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "f131695a", + "metadata": {}, + "outputs": [], + "source": [ + "adv = mytools.Adversary_small(n_classes=Num_classes).to(device)\n", + "criterion_adv = nn.CrossEntropyLoss(reduction='none') # returns loss per sample so we can weight it (signal events are weighted as 0)\n", + "opt_adv = torch.optim.Adam(adv.parameters(), lr=1e-2)\n", + "scheduler_adv = torch.optim.lr_scheduler.ExponentialLR(opt_adv, gamma=1.0)\n" + ] + }, + { + "cell_type": "markdown", + "id": "b504165c", + "metadata": {}, + "source": [ + "## Setup dataloaders" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "51fbb17f", + "metadata": {}, + "outputs": [], + "source": [ + "# Convert to tensor dataset and dataloader\n", + "\n", + "# the structure is: feature, adversary label, weight\n", + "# Weight is 0 for signal (PhiKK==1) so the adversary never trains on signal.\n", + "# For background rows: weight = sample_weight, upweighting MC events proportionally.\n", + "\n", + "adv_w_train = ((y_train == 0).astype(float) * w_train).values.astype(np.float32)\n", + "adv_w_val = ((y_val == 0).astype(float) * w_val ).values.astype(np.float32)\n", + "\n", + "train_adv_dataset = TensorDataset(\n", + " torch.from_numpy(X_train.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_adv_train),\n", + " torch.from_numpy(adv_w_train))\n", + "train_adv_loader = DataLoader(train_adv_dataset, batch_size=bsize, shuffle=True)\n", + "\n", + "val_adv_dataset = TensorDataset(\n", + " torch.from_numpy(X_val.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_adv_val),\n", + " torch.from_numpy(adv_w_val))\n", + "val_adv_loader = DataLoader(val_adv_dataset, batch_size=bsize, shuffle=True)\n", + "\n", + "# Weights are not relevant for testing\n", + "test_adv_dataset = TensorDataset(\n", + " torch.from_numpy(X_test.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_adv_test))\n", + "test_adv_loader = DataLoader(test_adv_dataset, batch_size=bsize, shuffle=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "90db4162", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1\n", + "-------------------------------\n", + "Training loss: 2.208937\n", + "Validation loss: 2.196308 \n", + "\n", + "Epoch 2\n", + "-------------------------------\n", + "Training loss: 2.166036\n", + "Validation loss: 2.192175 \n", + "\n", + "Epoch 3\n", + "-------------------------------\n", + "Training loss: 2.160563\n", + "Validation loss: 2.164609 \n", + "\n", + "Epoch 4\n", + "-------------------------------\n", + "Training loss: 2.158243\n", + "Validation loss: 2.160684 \n", + "\n", + "Epoch 5\n", + "-------------------------------\n", + "Training loss: 2.155244\n", + "Validation loss: 2.162622 \n", + "\n", + "Epoch 6\n", + "-------------------------------\n", + "Training loss: 2.154786\n", + "Validation loss: 2.156184 \n", + "\n", + "Epoch 7\n", + "-------------------------------\n", + "Training loss: 2.154560\n", + "Validation loss: 2.157246 \n", + "\n", + "Epoch 8\n", + "-------------------------------\n", + "Training loss: 2.152307\n", + "Validation loss: 2.158321 \n", + "\n", + "Epoch 9\n", + "-------------------------------\n", + "Training loss: 2.152895\n", + "Validation loss: 2.154768 \n", + "\n", + "Epoch 10\n", + "-------------------------------\n", + "Training loss: 2.152749\n", + "Validation loss: 2.154998 \n", + "\n", + "Epoch 11\n", + "-------------------------------\n", + "Training loss: 2.152374\n", + "Validation loss: 2.156323 \n", + "\n", + "Epoch 12\n", + "-------------------------------\n", + "Training loss: 2.151667\n", + "Validation loss: 2.154462 \n", + "\n", + "Epoch 13\n", + "-------------------------------\n", + "Training loss: 2.151923\n", + "Validation loss: 2.154134 \n", + "\n", + "Epoch 14\n", + "-------------------------------\n", + "Training loss: 2.151908\n", + "Validation loss: 2.154047 \n", + "\n", + "Epoch 15\n", + "-------------------------------\n", + "Training loss: 2.152027\n", + "Validation loss: 2.153711 \n", + "\n", + "Epoch 16\n", + "-------------------------------\n", + "Training loss: 2.152188\n", + "Validation loss: 2.156731 \n", + "\n", + "Epoch 17\n", + "-------------------------------\n", + "Training loss: 2.151523\n", + "Validation loss: 2.154776 \n", + "\n", + "Epoch 18\n", + "-------------------------------\n", + "Training loss: 2.152080\n", + "Validation loss: 2.153457 \n", + "\n", + "Epoch 19\n", + "-------------------------------\n", + "Training loss: 2.151765\n", + "Validation loss: 2.154002 \n", + "\n", + "Epoch 20\n", + "-------------------------------\n", + "Training loss: 2.151392\n", + "Validation loss: 2.157788 \n", + "\n", + "Epoch 21\n", + "-------------------------------\n", + "Training loss: 2.151384\n", + "Validation loss: 2.154090 \n", + "\n", + "Epoch 22\n", + "-------------------------------\n", + "Training loss: 2.151750\n", + "Validation loss: 2.155164 \n", + "\n", + "Epoch 23\n", + "-------------------------------\n", + "Training loss: 2.151045\n", + "Validation loss: 2.154352 \n", + "\n", + "Epoch 24\n", + "-------------------------------\n", + "Training loss: 2.151812\n", + "Validation loss: 2.153605 \n", + "\n", + "Epoch 25\n", + "-------------------------------\n", + "Training loss: 2.150890\n", + "Validation loss: 2.154026 \n", + "\n", + "Epoch 26\n", + "-------------------------------\n", + "Training loss: 2.151347\n", + "Validation loss: 2.154309 \n", + "\n", + "Epoch 27\n", + "-------------------------------\n", + "Training loss: 2.150775\n", + "Validation loss: 2.154708 \n", + "\n", + "Epoch 28\n", + "-------------------------------\n", + "Training loss: 2.151244\n", + "Validation loss: 2.154899 \n", + "\n", + "Epoch 29\n", + "-------------------------------\n", + "Training loss: 2.150551\n", + "Validation loss: 2.153645 \n", + "\n", + "Epoch 30\n", + "-------------------------------\n", + "Training loss: 2.150613\n", + "Validation loss: 2.154540 \n", + "\n", + "Epoch 31\n", + "-------------------------------\n", + "Training loss: 2.150848\n", + "Validation loss: 2.153644 \n", + "\n", + "Epoch 32\n", + "-------------------------------\n", + "Training loss: 2.150854\n", + "Validation loss: 2.152991 \n", + "\n", + "Epoch 33\n", + "-------------------------------\n", + "Training loss: 2.150488\n", + "Validation loss: 2.154376 \n", + "\n", + "Epoch 34\n", + "-------------------------------\n", + "Training loss: 2.151000\n", + "Validation loss: 2.154271 \n", + "\n", + "Epoch 35\n", + "-------------------------------\n", + "Training loss: 2.151224\n", + "Validation loss: 2.153613 \n", + "\n", + "Epoch 36\n", + "-------------------------------\n", + "Training loss: 2.151075\n", + "Validation loss: 2.154468 \n", + "\n", + "Epoch 37\n", + "-------------------------------\n", + "Training loss: 2.150989\n", + "Validation loss: 2.154715 \n", + "\n", + "Epoch 38\n", + "-------------------------------\n", + "Training loss: 2.150914\n", + "Validation loss: 2.154353 \n", + "\n", + "Epoch 39\n", + "-------------------------------\n", + "Training loss: 2.150980\n", + "Validation loss: 2.152791 \n", + "\n", + "Epoch 40\n", + "-------------------------------\n", + "Training loss: 2.150651\n", + "Validation loss: 2.153961 \n", + "\n", + "Epoch 41\n", + "-------------------------------\n", + "Training loss: 2.150427\n", + "Validation loss: 2.155334 \n", + "\n", + "Epoch 42\n", + "-------------------------------\n", + "Training loss: 2.150817\n", + "Validation loss: 2.156111 \n", + "\n", + "Epoch 43\n", + "-------------------------------\n", + "Training loss: 2.151369\n", + "Validation loss: 2.152672 \n", + "\n", + "Epoch 44\n", + "-------------------------------\n", + "Training loss: 2.151037\n", + "Validation loss: 2.156627 \n", + "\n", + "Epoch 45\n", + "-------------------------------\n", + "Training loss: 2.150778\n", + "Validation loss: 2.153859 \n", + "\n", + "Epoch 46\n", + "-------------------------------\n", + "Training loss: 2.150342\n", + "Validation loss: 2.153864 \n", + "\n", + "Epoch 47\n", + "-------------------------------\n", + "Training loss: 2.150505\n", + "Validation loss: 2.152920 \n", + "\n", + "Epoch 48\n", + "-------------------------------\n", + "Training loss: 2.150604\n", + "Validation loss: 2.153793 \n", + "\n", + "Epoch 49\n", + "-------------------------------\n", + "Training loss: 2.150761\n", + "Validation loss: 2.154117 \n", + "\n", + "Epoch 50\n", + "-------------------------------\n", + "Training loss: 2.150530\n", + "Validation loss: 2.154160 \n", + "\n", + "Epoch 51\n", + "-------------------------------\n", + "Training loss: 2.150618\n", + "Validation loss: 2.152859 \n", + "\n", + "Epoch 52\n", + "-------------------------------\n", + "Training loss: 2.150672\n", + "Validation loss: 2.153025 \n", + "\n", + "Epoch 53\n", + "-------------------------------\n", + "Training loss: 2.150077\n", + "Validation loss: 2.155118 \n", + "\n", + "Epoch 54\n", + "-------------------------------\n", + "Training loss: 2.150566\n", + "Validation loss: 2.153018 \n", + "\n", + "Epoch 55\n", + "-------------------------------\n", + "Training loss: 2.150889\n", + "Validation loss: 2.155723 \n", + "\n", + "Epoch 56\n", + "-------------------------------\n", + "Training loss: 2.151085\n", + "Validation loss: 2.153217 \n", + "\n", + "Epoch 57\n", + "-------------------------------\n", + "Training loss: 2.150738\n", + "Validation loss: 2.154677 \n", + "\n", + "Epoch 58\n", + "-------------------------------\n", + "Training loss: 2.150227\n", + "Validation loss: 2.153130 \n", + "\n", + "Epoch 59\n", + "-------------------------------\n", + "Training loss: 2.150185\n", + "Validation loss: 2.153054 \n", + "\n", + "Epoch 60\n", + "-------------------------------\n", + "Training loss: 2.150307\n", + "Validation loss: 2.153734 \n", + "\n", + "Done!\n" + ] + } + ], + "source": [ + "# Implement early stopping in training loop\n", + "# Stop if validation loss has not decreased for the last [patience] epochs\n", + "# The model with the lowest loss is stored\n", + "\n", + "\n", + "Training_losses = np.array([])\n", + "Validation_losses = np.array([])\n", + "\n", + "epochs = 60\n", + "for t in range(epochs):\n", + " print(f\"Epoch {t+1}\\n-------------------------------\")\n", + " \n", + " Training_losses = np.append(Training_losses, mytools.train_adv(train_adv_loader, final_classifier, adv, criterion_adv, opt_adv, scheduler_adv, device))\n", + " Validation_losses = np.append(Validation_losses, mytools.validate_adv(val_adv_loader, final_classifier, adv, criterion_adv, device))\n", + " \n", + " # Keep a running copy of the model with the lowest loss\n", + " if Validation_losses[-1] == np.min(Validation_losses):\n", + " final_adv = copy.deepcopy(adv)\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "db6ea33f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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HAXGfWbt+eefV/8e8f76+++/Td26dTOVLVtWjdDkvrJty5YtJl9fX9N7771nOnTokGnatGmmoKAg0y+//GK+j5Gf/QyAHOyLL75QByV/f381LH7Tpk2OfokiafXq1SrwyXoaNGiQeTjkG2+8YSpfvrwKIrt27WqKiooyeSpb+wqnKVOmmO+DD4gnn3xSDfXGh8o999yjgiRPNWTIEFP16tXVv71y5cqp95Ae/AD3V/4DIO6zTA888ICpYsWK6v1VuXJl9fPhw4e5r/KwcOFCU+PGjdXnev369U3fffed1e1GfvZ74X/OzzMRERERuQ/2ABEREZHHYQBEREREHocBEBEREXkcBkBERETkcRgAERERkcdhAEREREQehwEQEREReRwGQERERORxGAAREdnBy8tL5s2bx31FVEwwACIitzd48GAVgGQ93X777UZvGhEVUb5GbwARkT0Q7EyZMsXquoCAAO48IioQZoCIqEhAsFOhQgWrU+nSpdVtyAZNmjRJevToIYGBgVKzZk357bffrB6/e/du6dKli7q9TJkyMmzYMImNjbW6zw8//CCNGjVSr1WxYkUZOXKk1e0XL16Ue+65R4KCgqROnTqyYMECF/zmROQMDICIqFh44403pE+fPrJr1y556KGHpF+/frJ//351W1xcnNx2220qYNq6davMnj1bVqxYYRXgIIAaMWKECowQLCG4qV27ttVrvP3229K3b1/5999/5Y477lCvc/nyZZf/rkTkAC5Zc56IqBAGDRpk8vHxMQUHB1ud3nvvPXU7PsqGDx9u9ZjWrVubnnjiCXX5u+++M5UuXdoUGxtrvv2PP/4weXt7m6Kjo9XPlSpVMr322ms5bgNe4/XXXzf/jOfCdYsXL+bflqgIYg8QERUJnTt3VlkaS+Hh4ebLbdq0sboNP+/cuVNdRiaoWbNmEhwcbL69Xbt2kp6eLlFRUaqEdubMGenatWuu29C0aVPzZTxXyZIl5fz584X+3YjI9RgAEVGRgIAja0nKUdAXZA8/Pz+rnxE4IYgioqKHPUBEVCxs2rQp288NGjRQl3GO3iD0Aun+/vtv8fb2lnr16kloaKhERkbKypUrXb7dRGQMZoCIqEhISkqS6Ohoq+t8fX2lbNmy6jIam1u2bCnt27eXadOmyZYtW+R///ufug3NymPGjJFBgwbJW2+9JRcuXJCnnnpKHn74YSlfvry6D64fPny4REREqNFk169fV0ES7kdExQ8DICIqEpYsWaKGpltC9ubAgQPmEVozZ86UJ598Ut1vxowZ0rBhQ3Ubhq0vXbpUnnnmGWnVqpX6GSPGPvnkE/NzIThKTEyUTz/9VF544QUVWN13330u/i2JyFW80AntslcjInIC9OLMnTtXevfuzf1LRHZhDxARERF5HAZARERE5HHYA0RERR4r+USUX8wAERERkcdhAEREREQehwEQEREReRwGQERERORxGAARERGRx2EARERERB6HARARERF5HAZAREREJJ7m/5uIs5TMeELCAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot Training and Validation Loss\n", + "# Indicate where the final model stopped training\n", + "\n", + "best_epoch = np.argmin(Validation_losses)+1\n", + "\n", + "plt.plot(np.arange(len(Training_losses))+1,Training_losses,label=\"Training Loss\")\n", + "plt.plot(np.arange(len(Validation_losses))+1,Validation_losses,label=\"Validation Loss\")\n", + "plt.axvline(best_epoch,label=\"Final model stopped here\",color='k')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('BCE Loss')\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "id": "d367d398", + "metadata": {}, + "source": [ + "# Train full adverserial neural network" + ] + }, + { + "cell_type": "markdown", + "id": "7f439db8", + "metadata": {}, + "source": [ + "# Setup dataloaders" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "6609b5f6", + "metadata": {}, + "outputs": [], + "source": [ + "# the structure is: feature, classifier label, adversary label, weight, clas_mask\n", + "# weight: 0 for signal (masks adv loss), sample_weight for background.\n", + "# clas_mask: 1 within ±8 MeV of MASS, 0 outside — applied to classifier BCE loss only.\n", + "# The adversary sees ALL events regardless of clas_mask.\n", + "\n", + "train_full_dataset = TensorDataset(\n", + " torch.from_numpy(X_train.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_train.to_numpy().astype(np.float32)).unsqueeze(1),\n", + " torch.from_numpy(y_adv_train),\n", + " torch.from_numpy(adv_w_train),\n", + " torch.from_numpy(clas_mask_train).unsqueeze(1))\n", + "train_full_loader = DataLoader(train_full_dataset, batch_size=bsize, shuffle=True)\n", + "\n", + "val_full_dataset = TensorDataset(\n", + " torch.from_numpy(X_val.to_numpy().astype(np.float32)),\n", + " torch.from_numpy(y_val.to_numpy().astype(np.float32)).unsqueeze(1),\n", + " torch.from_numpy(y_adv_val),\n", + " torch.from_numpy(adv_w_val),\n", + " torch.from_numpy(clas_mask_val).unsqueeze(1))\n", + "val_full_loader = DataLoader(val_full_dataset, batch_size=bsize, shuffle=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "f435d980", + "metadata": {}, + "source": [ + "## Define loss functions" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "b259be14", + "metadata": {}, + "outputs": [], + "source": [ + "BCE_loss_unreduced = nn.BCEWithLogitsLoss(reduction='none')\n", + "CE_loss = nn.CrossEntropyLoss(reduction='none')\n", + "\n", + "def full_loss(output_clas, output_adv, target_clas, target_adv, w, lambda_, clas_mask):\n", + " \"\"\"\n", + " w : per-sample weight — 0 for signal, sample_weight for background.\n", + " clas_mask: 1 within ±8 MeV of MASS, 0 outside. Applied to classifier BCE only.\n", + " The adversary loss uses ALL background events regardless of clas_mask.\n", + "\n", + " Classifier loss: weighted mean over events inside the mass window.\n", + " Signal rows (w==0) get weight 1; background rows get sample_weight.\n", + " Both are then multiplied by clas_mask, so only in-window events contribute.\n", + " Adversary loss: weighted mean over ALL background rows (w > 0), full mass range.\n", + " \"\"\"\n", + " # ── Classifier loss (mass-windowed) ──\n", + " clas_w = torch.where(w > 0, w, torch.ones_like(w)) * clas_mask\n", + " sample_losses1 = BCE_loss_unreduced(output_clas, target_clas)\n", + " loss1 = (sample_losses1 * clas_w).sum() / clas_w.sum().clamp(min=1)\n", + "\n", + " # ── Adversary loss (full mass range, background only) ──\n", + " sample_losses2 = CE_loss(output_adv, target_adv)\n", + " nonzero = w > 0\n", + " loss2 = (sample_losses2[nonzero] * w[nonzero]).sum() / w[nonzero].sum()\n", + "\n", + " loss = loss1 - lambda_ * loss2\n", + " return loss\n" + ] + }, + { + "cell_type": "markdown", + "id": "1f658409", + "metadata": {}, + "source": [ + "## Setup optimizers" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "94e024ec", + "metadata": {}, + "outputs": [], + "source": [ + "optimizer_clas = optim.Adam(final_classifier.parameters(), lr=1e-2,betas=(0.9, 0.999))\n", + "optimizer_adv = optim.Adam(final_adv.parameters(), lr=1e-2,betas=(0.9, 0.999))\n", + "\n", + "scheduler_clas = torch.optim.lr_scheduler.ExponentialLR(optimizer_clas, gamma=0.999)\n", + "scheduler_adv = torch.optim.lr_scheduler.ExponentialLR(optimizer_adv, gamma=0.999)" + ] + }, + { + "cell_type": "markdown", + "id": "81c343c1", + "metadata": {}, + "source": [ + "## Adversarial training" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "73d7690b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1\n", + "-------------------------------\n", + "Diff score: 2.960755\n", + "Epoch 2\n", + "-------------------------------\n", + "Diff score: 4.514787\n", + "Epoch 3\n", + "-------------------------------\n", + "Diff score: 5.691445\n", + "Epoch 4\n", + "-------------------------------\n", + "Diff score: 8.223486\n", + "Epoch 5\n", + "-------------------------------\n", + "Diff score: 4.207736\n", + "Epoch 6\n", + "-------------------------------\n", + "Diff score: 5.331173\n", + "Epoch 7\n", + "-------------------------------\n", + "Diff score: 9.049319\n", + "Epoch 8\n", + "-------------------------------\n", + "Diff score: 2.873021\n", + "Epoch 9\n", + "-------------------------------\n", + "Diff score: 1.803429\n", + "Epoch 10\n", + "-------------------------------\n", + "Diff score: 2.421798\n", + "Epoch 11\n", + "-------------------------------\n", + "Diff score: 3.333674\n", + "Epoch 12\n", + "-------------------------------\n", + "Diff score: 3.115693\n", + "Epoch 13\n", + "-------------------------------\n", + "Diff score: 2.160503\n", + "Epoch 14\n", + "-------------------------------\n", + "Diff score: 2.422331\n", + "Epoch 15\n", + "-------------------------------\n", + "Diff score: 1.391144\n", + "Epoch 16\n", + "-------------------------------\n", + "Diff score: 3.117608\n", + "Epoch 17\n", + "-------------------------------\n", + "Diff score: 4.076741\n", + "Epoch 18\n", + "-------------------------------\n", + "Diff score: 4.319126\n", + "Epoch 19\n", + "-------------------------------\n", + "Diff score: 5.558614\n", + "Done!\n" + ] + } + ], + "source": [ + "Training_losses_clas = np.array([])\n", + "Training_losses_adv = np.array([])\n", + "Validation_losses_clas = np.array([])\n", + "Validation_losses_adv = np.array([])\n", + "diff_scores = np.array([])\n", + "\n", + "N_ADV_STEPS = 8 # adversary updates per classifier update\n", + "\n", + "def train_full_windowed(loader, classifier, adv, loss_fn, lambda_,\n", + " crit_adv, opt_clas, opt_adv, sched_clas, sched_adv, device):\n", + " classifier.train(); adv.train()\n", + " total_clas, total_adv = 0.0, 0.0\n", + " for X, y_c, y_a, w, cmask in loader:\n", + " X, y_c, y_a, w, cmask = X.to(device), y_c.to(device), y_a.to(device), w.to(device), cmask.to(device)\n", + " # ── Adversary steps ──\n", + " for _ in range(N_ADV_STEPS):\n", + " opt_adv.zero_grad()\n", + " with torch.no_grad(): out_c = classifier(X)\n", + " out_a = adv(out_c)\n", + " nonzero = w.squeeze() > 0\n", + " if nonzero.sum() > 0:\n", + " adv_losses = crit_adv(out_a[nonzero], y_a[nonzero])\n", + " adv_loss = (adv_losses * w.squeeze()[nonzero]).sum() / w.squeeze()[nonzero].sum()\n", + " adv_loss.backward()\n", + " opt_adv.step()\n", + " # ── Classifier step ──\n", + " opt_clas.zero_grad()\n", + " out_c = classifier(X)\n", + " out_a = adv(out_c)\n", + " loss = loss_fn(out_c, out_a, y_c, y_a, w.squeeze(), lambda_, cmask)\n", + " loss.backward()\n", + " opt_clas.step()\n", + " total_clas += loss.item()\n", + " with torch.no_grad():\n", + " nonzero = w.squeeze() > 0\n", + " if nonzero.sum() > 0:\n", + " al = crit_adv(out_a[nonzero], y_a[nonzero])\n", + " total_adv += ((al * w.squeeze()[nonzero]).sum() / w.squeeze()[nonzero].sum()).item()\n", + " sched_clas.step(); sched_adv.step()\n", + " return total_clas / len(loader), total_adv / len(loader)\n", + "\n", + "def validate_full_windowed(loader, classifier, adv, loss_fn, lambda_, crit_adv, device):\n", + " classifier.eval(); adv.eval()\n", + " total_clas, total_adv = 0.0, 0.0\n", + " with torch.no_grad():\n", + " for X, y_c, y_a, w, cmask in loader:\n", + " X, y_c, y_a, w, cmask = X.to(device), y_c.to(device), y_a.to(device), w.to(device), cmask.to(device)\n", + " out_c = classifier(X)\n", + " out_a = adv(out_c)\n", + " loss = loss_fn(out_c, out_a, y_c, y_a, w.squeeze(), lambda_, cmask)\n", + " total_clas += loss.item()\n", + " nonzero = w.squeeze() > 0\n", + " if nonzero.sum() > 0:\n", + " al = crit_adv(out_a[nonzero], y_a[nonzero])\n", + " total_adv += ((al * w.squeeze()[nonzero]).sum() / w.squeeze()[nonzero].sum()).item()\n", + " return total_clas / len(loader), total_adv / len(loader)\n", + "\n", + "epochs = 19\n", + "for t in range(epochs):\n", + " print(f\"Epoch {t+1}\\n-------------------------------\")\n", + "\n", + " E_clas_training_loss, E_adv_training_loss = train_full_windowed(\n", + " train_full_loader, final_classifier, final_adv, full_loss, lambda_,\n", + " criterion_adv, optimizer_clas, optimizer_adv, scheduler_clas, scheduler_adv, device)\n", + " Training_losses_clas = np.append(Training_losses_clas, E_clas_training_loss)\n", + " Training_losses_adv = np.append(Training_losses_adv, E_adv_training_loss)\n", + "\n", + " E_clas_val_loss, E_adv_val_loss = validate_full_windowed(\n", + " val_full_loader, final_classifier, final_adv, full_loss, lambda_, criterion_adv, device)\n", + " Validation_losses_clas = np.append(Validation_losses_clas, E_clas_val_loss)\n", + " Validation_losses_adv = np.append(Validation_losses_adv, E_adv_val_loss)\n", + "\n", + " # Get the diff_score (measure of how much the invariant mass distribution changes after selection)\n", + " df_test[\"Class_adv\"] = mytools.test_clas(test_loader, final_classifier, device)\n", + " df_bkg = df_test.loc[ (df_test.PhiKK == 0) ]\n", + " per = np.percentile(df_bkg['Class_adv'],90)\n", + " df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + " x1 = 1000*df_bkg.InvM.values\n", + " x2 = 1000*df_cut.InvM.values\n", + " diff_scores = np.append(diff_scores, mytools.get_diff_score(x1,x2))\n", + "\n", + " print(f\"Diff score: {diff_scores[-1]:>7f}\")\n", + "\n", + " # Keep a running copy of the model with the lowest diff_score\n", + " if diff_scores[-1] == np.min(diff_scores):\n", + " final_clas_adv = copy.deepcopy(final_classifier)\n", + " final_adv_adv = copy.deepcopy(final_adv)\n", + "\n", + "print(\"Done!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "ca259e41", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAqIAAAGwCAYAAABy9wf+AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAuBlJREFUeJztnQWYE1cXhg/ui7u7u0uBAsWtRQqlWCkULRTaHyhWxQsUKcWhxQrF3aW4u9Pi7q57/+e7dyebhGQ32U0yM8l5n2c22WQyM5mMfPdoFCGEIIZhGIZhGIbxMVF9vUKGYRiGYRiGASxEGYZhGIZhGF1gIcowDMMwDMPoAgtRhmEYhmEYRhdYiDIMwzAMwzC6wEKUYRiGYRiG0QUWogzDMAzDMIwuRNdntebnzZs3dPDgQUqZMiVFjcp6nmEYhmHMQHBwMN28eZMKFy5M0aOzDNIb/gUiCERoiRIlPPtrMAzDMAzjE/bs2UPFixfnva0zLEQjCCyh2oGcOnVqT/4mDMMwjA959uwZ5cyZUz4/ffo0xY0bl/e/H3P9+nVpSNLu44y+sBCNIJo7HiI0Xbp0nvxNGIZhGB/y9OlTy/O0adNSvHjxeP8HABxWZww4uJFhGIZhGIbRBRaiDMMwDMMwkeXCBaI2bYgyZyaKE4coa1aiAQOIXr0KnWfzZqJ69eBOJYLlvVAholmzwl/23r1ElSsTJUpElDgxUbVqRIcP2647SpR3p127bJczfz5RrlxEsWMT5c9PtHKl7r87C1GGYRiGYZjIcuoUUvKJJkwgOn6caORIot9/J/r229B5duwgKlCAaMECoiNHiFq3JmrRgmj5cufLffKEqHp1ogwZiHbvJtq2jShBAiVGX7+2nXf9egTBhk5Fi9quu2lTJZYPHiSqX19Nx47p+ttHEUIIXbfApFy5coXSp09Ply9f5hhRhmEYk8eIxo8fXz5/8uQJx4j6OT69fw8bRjR+PNG//zqfp1YtZEATTZ3q+P19+4iQ3X/pElH69Oq1o0eVoD17lihbNmURhSUWAhNWVkd8/DEOdlvRW6qUmh+CWSfYIsowDMMwTMDx+PFjevTokWV6+fKl51fy8CFRkiSRmydnTqKkSYmmTFFu/ufP1fPcuYkyZbKdt25dohQpiMqVI1q61Pa9nTuJqlSxfQ1WVbyuIyxEGYZhGIYJOPLkyUMJEya0TIMGDfLsCs6dIxozhuiLL5zPM2+eiv+Ei94ZcMMjtnTmTBV7Cuv96tVEq1YRaQX58dovv6gY0BUrlBCF291ajN64oSyv1uB/vK4jXL6JYRiGYZiA48SJE7Jcl0asWLEcz9irF9GQIWEv7ORJlQSkcfWqiuts1IiobVvHn9m0SQnQSZOI8uZ1vmxYQBHXWbYs0Zw5RG/fEg0frlz6ELEQp8mSEXXvHvoZuPKvXVOhAbCSGhgWogzDMAzDBBwJEiSgoKCg8Gfs0YOoVauw58mSJfQ5BOD77xOVKUM0caLj+bdsIapTRyU0IVkpLGbPVjGgcKFrLcXxGrLnlywhatLE8edKliRaty70/1SpiG7etJ0H/+P1QHXNwwyO9lo4GFKkSEH169eXXS3CYuHChVSsWDFKlCiRDCgvVKgQ/fnnnzbzIP+qf//+sth8nDhxqEqVKnQWAb12rFixgkqWLCnnSZw4sVw/wzAMwzCMheTJlbUzrClmzFBLaMWKKlt92rRQ4WgN3OywZsLK2q5d+Dv62TO1HJRj0tD+R5a+Mw4dUmWiNEqXJtqwwXYeCFW8HqhCdMuWLdSpUyfatWsXrVu3jl6/fk1Vq1a16XJhT5IkSahPnz60c+dOOnLkCLVu3VpOa9asscwzdOhQGj16NP3++++0e/duKVirVatGL168sMyzYMECat68ufzs4cOHafv27fTJJ594/TszDMMwDOOHaCIUZZbgOr99W8VfWsdgwh0PEfrll0QNGoS+f+9e6DyLFtm6+T/4gOj+faJOnVQIAEpDwaWP+FBYXsGMGcptjxJSmAYOVFn4XbqELqdrVxVbilhSzPPddyojv3Nn0hVhIG7duoVSUmLLli1ufa5w4cKib9++8nlwcLBIlSqVGDZsmOX9Bw8eiFixYok5c+bI/1+/fi3Spk0rJk+eHOFtvXz5stxWPDIMwzDm5cmTJ/J6jgnPGf/Ga/fvadNQD9PxpNGypeP3K1R4dznWrF0rRNmyQiRMKETixEJUqiTEzp2h70+fLkTu3ELEjStEUJAQJUoIMX/+u9s4b54QOXIIETOmEHnzCrFihdAbQ2XNP0QJgxCrpyvABb9hwwbpzi9fvrx87b///qMbN25Id7wGsuHggocVFRw4cICuXr0q+8wWLlxYuvBr1KhBx8Io6oqyDtZlHlD2gWEYhmEYRoI4UmdSVGP6dMfvb9787nKsgVUUhewfPFDWU7jYUQNUo2VLZF+pOqHQUih837Dhuz8MkqcQAolSVdA8NWvq/uMZRogGBwdTt27dqGzZspQvX75wBSuKD8eMGZNq1apFY8aMoQ/wI8nqBMoEntKuRAH+1977N6Sw7HfffUd9+/al5cuXyxjRihUr0j1r87hdPKt1mQeUffAKODiwfTDpMwzDMAzD+DGGEaKIFYVFcu7cueHOi+SmQ4cO0d69e+nnn3+m7t2702br0YQLohcg1rRBgwZUtGhRmjZtGkWJEoXmowaXA3r37i0FsDah7INXQNwH+tP+8Yd3ls8Ymh9//JHmzZtn8Q4wDMMwjD9jiPJNnTt3llbJrVu3utRuCy71bGhpRehMVYhOnjwpLZawaKYKKUNw8+ZN6XLXwP+YF2ivW1s1UT8sS5YsdAkttByA961rjME97xW073/lineWzxiWu3fvSis9BkrRo0eX4Sa1a9eWU/bs2fXePIZhGIbxL4soYjwhQhctWkQbN26kzOiTGgFw49Zac2EZEKOIHbUWjcieLx1SogAWUIhK61JRyNi/cOECZcyYkXRFE6LIvmP8HpQVGzFihLSA4hj86quvKGfOnPTmzRt5TsDanyNHDvnaDGRFMgzDMIwfEVVvd/zMmTNp9uzZ0t2OGE5Mz9FFIIQWLVpIt7gGLJ8o9YQ4T1hCf/nlF1lH9NNPP5Xvw72OWNOffvqJli5dSkePHpXLSJMmjaVOKArYtm/fngYMGEBr166VgrRDhw7yvUYI5NUTtogGFCNHjqQePXpQ27Zt5QBq+PDhdOrUKTpz5ox8r3LlytI6iv8hTjWuXLlCs2bNklZUhnEGvEwok8cwDGNY9EzZ18pl2E/TULoghAoVKoiWKHcQQp8+fUS2bNlE7NixReLEiUXp0qXF3LlzbZaLEk79+vUTKVOmlGWbKleuLE6fPm0zz6tXr0SPHj1EihQpRIIECUSVKlXEsWPH9C//sGuXyqFLn96zy2UMx507d0ScOHHkcbRp0yan86H82Lx588TNmzctr40aNUp+LmrUqKJcuXJiyJAh4vjx4/LYZxhw5swZES1aNHmMHDlyhHdKGHD5psCCyy8aiyj4o7cYNiOwSKVPn54uX77sUlyry8Alj+VFi6Yy6PHI+CUDBw6UCXMoIbZ//35pzXeV6dOnS4spmjpYg9AUxJSiGgS6lTGBS6tWrSzhHPv27ZMhSYxj0EQFlVjAkydPZBMUxn/x2v2bMXfWPBMCkq0gPt++Jbp1i3eLn/Lq1SsaO3asfI44UHdEqCYy0BEMcc3jxo2TdXAR94w6uhMmTKC4ceNa5j148KBM1mMCh/Pnz8uwJ7Bjxw4WoQzDGBYWokYDIlTL9ufMeb/lr7/+ouvXr8sKDo0bN47wcpBc17FjR1q5cqWMF12yZAkNGTLEYt0Bbdq0kfGnaOqABCjG/0FZu7dv31L16tUtSZoMwzBGxBDlmxg70qZVIhRT8eK8e/wMRMMgUx506dJFNmbwBHAn1q1b1+Y1JP5FCwnv2LNnj0zGg9UUCXuMf4JEzj9C6hAjIRM8e/ZMJnaisUe7du103kKGYZhQ2CJqRDhz3u/j0VCOCWLwiy++8Oq64sSJIxs/oKUt1onOYVpIAOOfjBo1SlpDq1WrRqVCWgCiSUL//v2pV69eTrvHMQzD6AELUSPCtUT9GrjN0UEMgfJJkiTxyTpRvgxCBMAy5rWGDIzuoMQdyoD98MMPlteaN29OBQoUoPv379P333+v6/YxDMNYw0LUiLBFNCDwtXv8448/ZqtoAIAQDdSmLVGihOU1hGdo4SBIbkOtWoZhGCPAQtSIsBD1W2AJRXF6PYAYgVUUBfIfPHigyzYw3g35QJc5Z6A5Qp06daTb/uuvv+afgmEYQ8BC1KjJSoCz5v0KZLV/9tln0iqJ0kt6AKvouXPnaOjQobqsn/EesIIWKlQozE5KcNljILJixQrZVY5hGEZvWIga3SLK/Qb8BtT3RBY7xALi9fSyiqLkE+NfXLp0iaZOnSpbGmtVEhyRI0cO6ty5s3yO1sncz4RhGL1hIWpE0qRRj+isxBmufkFkC9h7A3RlWrhwod6bwXgoQen169dUqVIlKleuXJjzIjyjRYsWMpPeCMchwzCBDQtRIxIrFpHWntGk7nnEqsFF+BJimrEpYA/3uN5s3bqVChYsSJ9//jln0JscVF+YMmWKTd3QsEicOLFs/Zk1a1YfbB3DMEzYsBA1KiaPE+3Xrx9VrFiR/ve//1Gg460C9pGhbNmylCtXLlnOZ8yYMXpvDhMJBg8eLK2hON/Kly/v9ucxQGIYhtELFqJGxcSZ8+jcM3DgQPl89OjR9PDhQwpkYBk+dOiQ7P/u7QL27mbQA64ral6uXLlCkydPdtkaag28FS1btqTMmTPLBDaGYRg9YCFqVExc1B5JEBrogx7o7nl0NULh+latWvmsgL0roMc9W0XNzcyZM2X8MSyhsIi6Q6xYsejmzZvy/GTPBcMwehFFcNpkhC0R6dOnl/FZ6TTR6ElgUezTh6h1a6KpU8ks7Ny5k8qUKSOTIA4cOCAzxBnV6xsZ80mTJjXU7pgzZw598sknMm7wwoUL3IPeZODyvXLlSkqWLBmVLFnS7c8fP35cxgqjtuimTZvcFrP+VIMVHc/AkydPZFMAxn/x+v2bcQu2iBoVE8aI4qb41Vdfyeeol8kiNBS45Y0mQgFbRc0NBny1atWKkAgFefPmtYSL4NyFIGUYhvElLESNigljRHETa9KkiRxp/vjjjxZxumPHDkuyTqAVsF+9erWhazVqsaIpUqSQVjXGHCDJ7NGjRx5ZFnrPJ0yYUMYxI5ueYVxh37599Ntvv/HOYiINC1GjYsIYUXRs6datG50/f16WKQJ4jgztb775RhbdDrQC9jVq1JDi3MjAKooEM6MkUjGuicdMmTLJGNHIggEIqlyAb7/9lh4/fsw/ARMmSG5DvVo0R0BoCMNEBhaiRnfNw+rhIcuHr4gRI4blebZs2WSRbdQV/f333ykQC9jXrl2bjAysoggdYMwByi1hkAOraMqUKT2yTJQVw7mK8/TkyZMeWSbjv+BYad26tfT2IMb8zJkzem8SY2JYiBoVBM4nSmQKqygyb0uUKEHLli1z6IbWWgpOmjSJXrx4QYGA0QrYuwJEyN9//y1bRTLGZdiwYfI8Kl26NFWpUsUjy0RtW3TZOnv2rDyXGcYZMCgg5AjHIbxdKM9Xv359bozBRBgWokbGJAlLqF+4d+9e+uGHHxwK0Tp16si40Tt37tD8+fMpkArYQ4QboYC9KyxfvpwaNWpEPXr0CPjar0Ye9GmeBZx3nmzRmT9/fhkryjBhVf/A9QEhR7CCYuCaNm1aaUVH21gMZhnGXViIGhkTJCwdO3ZMWjrByJEjKWrUqA5jR7X4w3HjxlGgFLCPEyeOqeIukX2dO3duevDggWxEwBgPWKFQBgxZ8lWrVvXaQAoW/d27d3tl+Yx5Wbt2rRSjGTNmpMKFC1OqVKlo0aJFsibtkiVLLEmqDOMOLESNjAkSlr7++ms5Cm7QoIEMXncGepojdhQ3N2Rb+jOaNRQF7I1YssmVbkv4DoHeEcuI1lAtS9nT1lBrhgwZIhPsEDfKFi7GGoRvgI8++shy/BUvXtxipUftaD5mGHdhIWpkDG4RRZzQmjVrpMDEzSsskFSB7OwsWbJIF72/AmvVxYsX5fOuXbuS2YBrPk+ePAFtFUVxc8ReNmvWzFAxzRs2bJBdkHDjr169utfWgwEUirsj3Gb27NleWw9jLl6/fi3zAMCHH374zjGzYsUKaR115BVjmDBBZyXGfS5fvoxgSPnoNSZORMSlELVqCaPx+vVrkSdPHrkPevTo4dJn7t+/L96+fSv8neDgYHHgwAFhVubOnSt/10SJEsnfLNAYOnSo/P6YqlatKp49eyaMwqlTp8S+ffu8vp6BAwfK7582bVrx5MkT4e/gO2q/eSB834iwdu1auX9SpEgh3rx5E+418Pnz5yKg79+My/DQxcgY2CKKxJYTJ05I13Pfvn1d+kyiRIkCYrQMlxXip8xKw4YNA9YqihqamnUfvyNi4urVqyct3UYgZ86cVLRoUa+vB12WEAd49epVGj58uNfXxxgfWDsBzgeE8TgD5wospHDfc6cuxhX8XxWYGQPHiOJiBDGKWpkQmO4A9yKy5/3tIoX4KLh1zY4WK1qsWDEqVaoUBRJouoDBVfbs2Wn9+vWy5/i6detky1o9O3T5uk5j7NixaejQofI5HtGbmwlskJgKIDDDK3Y/b948WrVqlSXmnGHCxHXjKeNz0/69e8o1j8nAbg53gMsmV65cct8tW7ZM+AsvX74UadKkke5sM7vlNRBCgd8qEEHYyb///iufb926VWTKlEkcPnxYt+3p1auXiBo1qvjhhx98ul78/mXLlpXnavPmzYU/w655146HI0eOyGtdeMycOdMS6jBv3jxhNNg1byzYImpkYGmME8dQVlEUaYeFJqLA3YkyQf5WygkWgGvXrsmSTXnz5iWzgxAKb2VlGx2UG8ucObN8/t5779Hp06epQIECumwLzjV4HZCJXLBgQZ+uG78/SrIVKlRIulqZwAbHA2rNulIXGYl+qDcKcOwcPXrUB1vImBUWokYGQsBgcaLoJY/2bloZj4jQoUMHeVFD1j06uZgd1F3EDdtsBexd4dGjR/Tzzz9bvp+/gnhYlEZC2Ig91r/nP//8Q3Xr1vVZCAbKaD158kTGHKMxhK9Bhj5CTtCmlwlMcH1Dy2J3GTx4sKw+gbqj6Lx07949r2wfY35YiBodA8WJ7tixQ1r+UF8SYjSiZM2aVXbmAOPHjyezs3XrVnmzNlsBe1dAeS4ko33//fdSrPkrENqdOnWSN0xnoJQT2rWihE3t2rW9LkZx4x4zZox8jlg7vSzU1uv1t7huJnxgzUyePDm1adPGYee8sDwLc+fOld6Ff//9lz799FO3Pm9KLlwgatOGCB4VeDOzZkXRXyJrIb95M5IsiFKnJooXj6hQIaJZs8Jf9t69RJUrK09p4sRE1aoRHT4c+v533ynjlf2EdWhMn/7u+7Fjk96wEDU6BrGIwjWITFqAC1JkXZW46YNp06aZPsHHrAXsXQGNCpBBj8HHr7/+Sv4I3N+axReNF8JK4FmwYAElSJCANm/eTDVr1pTWSm+BbUIWP1zySA7UE4jwgQMHSje9kWqrMr7JlodnBPWf3R0M4Xq4ePFiypAhg3TV+324z6lTuFkSTZhAdPw4TmIiFPv/9tvQeXbsIML9c8ECoiNHiFq3JmrRAqVonC8X1xnUDs6QgQgdz7ZtI0qQQInR16/VPF9/jdg52ylPHhSHtl1WUJDtPCF1r3VF7yBVs+KzYOfevVWyUpcuQk9mz54tv2+8ePHE9evXPZIMkyVLFrnMiaiXalLOnDkjokSJIr8Hajz6I3/99Zf8fgkTJvTLuqK9e/eW369QoUIu1bnduXOnCAoKkp957733xOPHjz2+Tffu3bOsY8GCBUJvnj59KtKlSye3Z9CgQcLf4GQl5xQoUED+7tOnT4/w/nUlwclvk5WGDhUic+aw56lZU4jWrZ2/v3ev0gGXLoW+duSIeu3sWcefOXRIvb91a+hr06YJkTChMBpsETU6adPqbhFFXbhevXrJ571795b9hT2RDNOxY0f5fP/+/WRWNm3aJB/hqkWNR39Eqyvqj1bR27dvW2qlIvzAlTq3KGmF+qJBQUEyZhRhJrBcepIjR47IMlpIDgkrXMBXxI0blwYNGiSfI2b4xo0bem8S4wPOnz9vORYjE6NsHWeN5L/D1i5lHcF5C2uvNjmKEY80aJWcJEnk5smZE+ZloilTlJsfdY3xPHduokyZHH9m8mSiHDmQcfmudTVjRqL06VWIACy3eqO3EjYrPhtRLV6sRjUlSgi90LqspE+f3qNdZmBdO3r0qDA7Z8+eFSdOnBD+jL9aRb/55hv5vYoWLep2uardu3fL/YHP9+3b1+Pb9vDhQ3H8+HFhFGAtLlGihPy+n3/+ufBXi+iVK1f03hzDMGzYMLlPKlWq5JHlbd++XVr6M2TIIG7duiX0vn/bTwMGDPDsimCtDApSXRKd8ddfQsSMKcSxY2EvC/fKrFmFiBpVTTlzCnHhguN5Ue4xcWIhhgyxfX3HDiFmzBDi4EEhNm8WonZttX06d5hiIWp0IYp2fhCiadIIPW/WqGOI2nBMYAIRorV0/e6774Q/cOPGDREnThz5nVasWBGhZezdu1fW2DSa69FbQEhgfyEc5SBuZn4oRL/++mu9N8cwlClTRu6TsWPHeizkJFu2bHKZ77//vnj16pXQ8/4NAwIGfNr04sULxx/o2TO0prez6eRJ289gQAPh2KaN8w3ZuFGIuHGVOAwLGIBgjGrRQog9exAfJESDBkLkzaves2f2bCGiR8dFLuzlYv9jG70wkDaNEIWlrVixYiJ+/PgiefLkol69euHG2SFeCtYLWCLixo0rChYsKP744w+beWDZ6Nevn0iVKpWIHTu2qFy5sozlcwQOPCwDB6U7F1afCVEcSDjIo0RRB41O4HfxZp/4mzdvyouUWYDw+O+//0QgMX/+fPHpp5/6TSwsitbXqlVLlCxZ0mPF+7EcxFNGFFibIYqN3Ezg448/lte+ihUrGno7w+LSpUuiTZs2lrhHayGK+wquR4HOtWvXLPHvnrQSHzt2TN7zsdyuXbsKPXD7/g3rLYRmWJP1YPTqVSGyZxcCjSCc3TdhkYwXT4gJE8Jf/+TJQqRIYbssrA8ids6cd+eHBbt+fde+W8OGQjRpIgJWiFarVk1MmzZNHpiHDh0SNWvWlCZ7XBScsWnTJrFw4UI5kjl37pwYNWqUiBYtmli9erVlnsGDB0uhunjxYtkRpW7duiJz5sziuYPuRF9++aWoUaOGcYUoDrwYMd4NVPYjfv75ZxEzZkyfd46JDH/++ae0Enfq1EnvTWEiSVjXG3eAKOvcubMoVaqUePDgQYSW8f3338vrCgS/Ublw4YKIFSuWiBEjhrx2mwkIzG7dusnrDfYz7jewylkLUUyYJ9CB6xzX5JYtW3p82biHa/s6MklQEcWr92+IdohQiLs3bxzPs2mTEqGuWppHjxYiVSpcZEJfe/1aLWPWLNt50RUOhitXOhdi++Di/+oroSdktAMfB8eWLVvc+lzhwoUtMVq4GcASitgWDdwUcOGcYzdyWLlypWw3iTgswwpRkDGjEqKI7/AR2I/9+/f3SYzarFmz5L5Ei0y9XDXu7hscc9hmiOhAxazWMG9a2ZIkSSKPC1hZ3RWjmB8tYvF5+2uV0UAVDcRGmwXsW3jJNEucZtHdEXJNtReiuF/45NoewOD+ou3rPXA3+xCv3b8hQrNlE6JyZfUcFWa0yd4dj4o41u/fvRs6z8KFSiBqwOIaK5YQHToIgXwEDAAxWEUG/LVrttsALYRQPkci+PvvhVizRojz54XYv1+J5dixhdA5Ft1QQhQXNhwcriaw4Ea4fv166UpZu3atfO38+fMORWX58uWl9dM6Pixt2rQyxgsu1vCEKFz41rEksMj6TIiWLauE6Pz5wldApGtuKm+7zLFvU6RIIdcH96/R2bx5s9xWxBfeuXNHBBoIc4HFzkwWbHvB2KVLF68kpeAaoolRJPa4k9j1448/ys/lzp1bvHFmSWHcBr3Otd8EE8LBcL+wHkhZC9Fy5crJx3bt2vHe9iII9apTp47c1wj38AshivJIzmJINWBhdvR+hQrvLscaaBxoAYhPJCLB/Y5YUXsParp0Qnz7rePtg6U/QwaVHJUypSobdeCA0Bsy0kGJeK2y2NEujG5RzzJ69OhyNDVlypR3gukR32JNo0aNROPGjeVzXICqV68uL/zAFSGKbDpHWXY+EaI4SXFQjhzp/XVJi/9reTP0ZeB+nz59LFYKo4NQD2xrB4xOA/TGbuYM+vbt28vtr1q1qleWjzCjpEmTynUUL17cpX2EwW3ixInlZ2BtNBP79u0Tt2/fFkZFuyfgmoYcA0eWfGshCpGKR9xfEEcciGzdulWe596okWt/3CP50dfJfj71aDLmEaK4OWTMmNGlAwOiFdZTCMfhw4fLGyJiR10Vor/++qsUvJrVwfAW0e7dlRDt0cP76xJCjBs3Tn433Ex9JTRgpUKsrzsWcT0IhAL2rpx/+fLl8065Ey+Dcx2xjREJAXIHxKYnS5bMYoELz6uAEA/Mi1AhM1lDsd04HxBHaATvAPYd4rdHjBhh8zrEZVj71b6gfceOHWV4hDcTNI3Mhx9+KPcFwhn8ERaixsIQQhQJH+jaEdHRJ7IfNeuGK655ZOcj0QTCR5vwGTy2QHkEox3IuKhCiPrAfQHhqd1APVWyw1U++ugjw1sacaxiG2uj/loAgxAK7AfUBDRTtQPUv8R2V6lSxevrOnLkiDyXcK0JqzzUo0ePLK5js5VI08JUtClHjhyiVatWYsKECT4dUMLKieRUbYCEsJmryFx2Ee6sFAqqPmhlzQ740G0LT1z37t190mmPhaix0FWI4uKBGzuSVJyVV3KF1q1biwoh8RVashIspRqwYFonK128eFFeJLVpzZo18qT7+++/XRaWPj2Q581TQrRcOZ8V+IYbCxcGX7Jhwwa5bty8jViXEYlUOFaxjRsRcB7AWFtFkXRgBlBlQxt0akkq3gbXF7g4wwLeFexLiDgzWUM1ULkEllz7sKW8qHFo1xrVG4MWXDeQHKatFwlfaEPqTjWEsISoGRIovZHRnilTJp8mJKIMI9YLjwU8m96Ehaix0FWIwvIFtzpG1ehfrk3W3XtQLLpXr142tUfhZoHlExdwCE7E8kyaNMmmfBMuRkuWLJFWCVhAnZVvctU1r+uBjJsmhGimTF5dDSzSWlmTiBb4jgy46P3222+GjjeD9Qolxzhj3HxWUVjqsL0o16YXSJBy5MKGsEd4ipm5e/euvG6ggglqN1uXQIKYQ01nbZALL9bkyZNlVY6Iur9hvMB6rOt/fvvttxE6Fp0J0fHjx8vB535kGAcISETEfvjKxyV9cE1t0KCBXDeMSe5YtN2Fhaix0FWIOkr+wYQbvQYsndZ1zJDUgs4MuKghuL906dJi7ty5Dgvap0yZUlpCcbE6ffq00+0wvBDFDQpCFPVEvRizhDjYX375RcbTstBiwgMCIn/+/PI8QP1LIwPRAhc5ttXXpWKsRSiuXWigYeTBljfANVbrqGM/wWgQkVhj7E+tnimqIKASSkRxJkSbNWum++DFl8ATpZUQQ8KSr0FylOZpQT1ep52OIgkLUWNhiBhRM+LTAxmuIRSohRiNxMXWbDizYOsBhAOL83dZtmyZ+Omnn2T4i5HB74cKEL4uE2PNyZMn5eAY140CBQrIwvBInIxMJyazgVrRS5culV4uGBlgxcT+gKdLA5ZhDHBQPglGCRgRcO7BC4YwAGv++usvj3Q4cyZEkRSrhXNs27ZN+DtamBrK6ekVJoIQGk0Mw3LujesuC1FjwULULAdy6tRKiHrBRQTLlpHioFDbFbX86rvaosxHBewLFSoks6EZ86L3YAJiFG5HXDu04uqulKzzV3DdgdvbOiwBHi57qykqeGjVDnbt2uXx7QgrRrRt27bydQhnvY8fb6PVssV31hN0SoQHA0nI3jBIsBA1FlGJMQfp0qnHK1c8vui5c+dSgQIFaPXq1WQE4saNS9u2baOlS5fSpUuX9N4c2rp1Kx08eJBOnz5NadKk0XtzDAsGtq9evSIjEyVKFF3XnytXLtq0aROlSpWKnjx5Il/7/PPPKVCJESMGFSlShNKnT2957YMPPqBFixbRN998Q+XKlaPYsWPT3bt36fXr11StWjWKHz++T7exX79+FDNmTNqyZQutX7+e/Jm+ffvSf//9R71799Z1O/A7b9y4kVauXCl/f8a/YSEa4EL0+fPn1KtXLzp16hTt37+fjECePHno/fffp+DgYJowYYLem0MjRoyQjy1btqRkyZLpvTmGBGK9RIkS9NNPP5GROHr0qBQ2O3bsIKMAMbp582bKlCkTFS1alJo1a6b3JhmKJEmSUP369Wno0KH0zz//0MOHD2nPnj10+PBhOVjOmzevT7cHIrlDhw4WoabSG/wXHJeZM2fWezOoQoUKFC1aNL03g/EBLETNQtq0XhGiI0eOpMuXL8uLbffu3ckodO7cWT5OmjSJXr58qdt2wBK6bNky+bxbt266bYfRuXPnDu3bt49+/fVXunfvHhmF7777TlqxsF1GImfOnHTu3Dnau3evtAoyzoE1snjx4tJroxewEMJTA0GM38wfefv2rd6bwAQoLEQD2CJ648YNGjRokHw+ePBgihMnDhmFunXrUrp06ej27ds0f/58XbZh8eLFVL58eWkBwfZAPDCOgQULQuHRo0c0ZMgQQ+wmDCIWLlwo3fEDBgwgowFrj96hAoxrpEyZkn7//XfpNYLl39/ANQ7nb+3atenixYt6bw4TYLAQNZsQvXrVY4vs37+/jFPDhbVJkyZkJKJHj05ffPGFfD5u3Difrx+u0w8//FDun4oVK9KUKVN8vg1mImrUqBaxB5cqLEgIrdDbGgqaNm0qwz0YJjI0b95cxrP6IwhhOXHiBG3YsIGSJk2q9+YwAQYL0QC1iB45csQirhADCSFhNNq2bSvdlrt27fJ5/CosofXq1aOuXbvS2rVrOTbUBSDc+/TpY7GwN2zYkJ4+fUp6APcpkt1wXGPAxTCeBEmUb9688ZudCs8B0CMZjGGMpz6Y8GNEPRAsP2fOHGmxatSoEZUtW9aw7jBYtZDVnz9/fq+vDzF7SN4CEDB///03jRo1imP4XARuZiQr/fnnnzKuD5nPWqKXr9Gss59++imHVDAeP7ayZcsmj3N/AeeqNphkGF8TBTWcfL5WP+DKlSsywQeJPohl9DoQSHHjqudIBkmcOFKLw8++fPlyypcvnyEyJPVmxYoV9Mknn1CdOnXkDYZj9yIHstSRCDdr1iwpSn3J7t27qVSpUjIGE9UgIBoYJixgudcsgQjHiRcvntN5f/nlF/r6668pY8aMsqRbrFixTL1zz58/L88RnC+3bt2SVQv8HZ/fv5kwYYuoWUAikRa7E8E4UbiSroZ8FkILoivQRSgE+cCBA+W+QKLNhQsXdHMn+xNlypSRSWaaCEVGLupn+gLE8aHaAsqSsQhlPE3Hjh1lPWEk9UyePNlvrKGIhQ8EEcoYDxaiARIniuxzxP+gPifq8pkJWCiQiV25cmWPJsBguY0bN5ZxjRCk7du3l0WUOUbK8/zvf/+jSpUqyVALbzthEFeMIvFGq2nK+AeoLoJ6ogDH2LNnz8gf4kM/+ugjvTeFCVBYiAaAEEXtO1iJILKuXbsmC0ObCcRrQohi+z3V/QnuqNKlS8s4UAgXFM4fP368z93IgQCEp5YM9/3338ssdi0W19P4UwIJY1zatGkjC7+jBJ4eVT08eW6iYoqWnMkwesBC1I+L2uMiM3HiRHrvvfdkTEyOHDmkKMVFx0ygkHTr1q3l87Fjx0Z6eXAT16pVi44dOyZbLaJUU7t27TywpYwjEAYybNgw6cZEWa6//vpLdk25fv26R3cYXP84xmfOnMk/BONVMGDVyoOhQgTCesx6bn755ZeyfWla7f7CMD6Ghaif1hKFxQnuSdTiRP9vZEOipI1Z6ylqLfZgEYU1MzIgKB/FqVEtAN2AEM/I+MaKhC5HiEPDsYhuOSg67wkw6EI2M/pko9wXw3gbtGZFkwt0fsN1hGGYiMFC1E9d8z179qSpU6dKlyhG7AsWLKCgoCAyK0g6qVGjhhQccKG7C+K4rFvzITAffazZCuBbYAmFVR791pE4h7hlTySHQeDi90QGM4rpM4y3gXV/9uzZ9O+//8r4Z7Nx//59WUsa+QMMoycsRP1UiCKYvnDhwrRmzRopSv2hHFGnTp3kIy6e7iQIILu1XLlyMtkJ5Xw0/GGfmJGsWbPSzp07qXr16tIyHVapHFfA4EQrWo+EMx5cML4CsfcpUqQw5Q5H+T54zapUqaL3pjABDgtRP4kRRTb5qlWrLP/j4ohuRP50kYFwQbmpBw8eyIL8roD4z2LFikkXMKxld+/e9fp2MuGTKFEiWrlypU2mLmoyws3pLgjXgDse2cwo2cQweoA4SyQvmS1bnpOUGL1hIWpGi+iDB6jAbHkZwqx+/fpUs2ZN+uOPP/zW4ofYzm7dusluOUWLFg3XSjZmzBgpxO/cuSOtwxDmRu0iFYhYH58oLA23PVycKKodEWso6jsi+YxhfA0K3CPc5+effzbFzkc4DLxlgMs2MXrDQtRMIMYzQQKbhCVkfiPpY9myZdLi5++NspDhic5HhQoVcjrPixcv6LPPPpPzIkMeSQXbtm2jDBky+HRbGXIrfAK/GzoylShRgo4ePerS5zA/EkXg3ketUobRAxgBAMrA4Vg2OhChSGhFCaqCBQvqvTlMgMNC1GxYxYnCPV2yZEnZIx0iC2KrZcuWFOjAEjp9+nSZqIV2fBCuKAHFGBfE8MK9jqQ03MhRyQBtV8MDFu6tW7fS6NGjTRurx5gfWPIxvX79mn744QcySzclWEP9zXPGmA8WoiaNE501ZIjsjY6knQ8++EC6nRELGSjAEgxX7M2bN995D+57lKvCqL979+58oTUJyKRHn3h0/0LXq7p168p+9eFZ+VEnFxZwhtETrZPXjBkz6MyZM4b9MVDOD4lKANdJhtEbFqImtYgeX7tWPn777bcySSlZsmQUSLRt21aWcUJPcbB48WJLVx10SkIgvj8lagUKqDGKAQR+XyTgYSCBpgz24D0uO8MYCXRqQ6MMhAOhpq1ROXTokCzAnzJlSrnNDKM3LERNKkQblColxReC45HEE2hopZxQ/gdF+zGy/+abb/TeLMYDaC1XYQ1FUhpifO1Ba1bEt3E/ecZIaMfj3Llz6ciRI2REEION7H6cQ4F472CMBwtRE6AVcZdB8CFCtGiKFAFddqNRo0aUPHlyWRQdFjPEOSFG0N+TtQIF/J4IsUC90fjx48vX8Nvi94bFCe0VEZYCyyjDGAUkUeLalCVLFkOXisO1E3HZDGMEWIiaoMwGyhUhHrJhw4b0OmVKt/rN+yuoEKD1h0+YMKFMbEFHHQ689z/rqAY6hOXNm1cK1JMnT1LixImpa9euum4fw9gDowEaZyDW2WjwQJ0xIixEDQyy4RHDgzZycKHARRk9Y0b1ZoALUYD6keiyhGL1aP/J+C+I/0Us9MOHD2ns2LGW2o0YhDCMkUiaNKnNAMpIoOED6vVqNUQZxgiwEDUoqAuKLHjUU0RQ+caNG6UlKEr69GoGFP1+9YoCmZgxY8psaXRbYvy/r/e6deuoRYsWFtdily5d9N4shnEKSjkhbAi1bo1iDUVcKMqdoSoFwxgFFqIGA/Fv/fr1k6VrYP1BPcUDBw5Q+fLl1QxJk8IvrZ5fu6brtjKMr8MxUB927dq1tH37dkqgNXdgGAOCOGYkUvbs2dMQLnEkT/37778UO3Zs2S6ZYYwCC1EDjqK1Qt6dO3emTZs2UZo0aUJnQPFhq6L2DBNIIAYYdXOzZ8+u96YwTJggrh+iD41GjOAK14rYV6tWTXYiYxijwELUYODCtWDBApo1a5bsEAT3s7Oi9ixEGYZhjEnatGmlGAV9+/bV3SqqCVEuYs8YDRaiBgQxj+ia5BTNIhrSb55hGIYxZnIQyo+h850mBPXg/Pnz0jWPpNc6deroth0M4wgWomaEXfMMwzCGB0l1SDIFiP1HDoAeaCK4YsWKsnsZ4yUuXCBq0wbWJKI4cYiyZiVCly3rxOLNm4lQAzx1aiKESBQqRDRrVvjL3rCBqEwZIsTGp0pF1LMnyonYzoMmCu+9B9cqERKbhw59dznz56Ofsponf36ilStJb1iImhEWogzDMKagR48elChRIjpx4gTNmTNHl23InTu3bD/apEkTXdYfMJw6hf7DRBMmEB0/TjRyJNr/oRd36DyoolCgANGCBUo4tm5NhGogy5c7X+7hw0Q1axIhyezgQaK//iJauhQm99B5Hj0iqlqVCCUe9+8nGjYMGXNE1i2Sse6mTZVYxnLq11fTsWOkJ1GE3oErJuXKlSuUPn16unz5MqXThKGvWLiQqEEDolKliHbu9O26GYZh/LBxiNbBC6WNPJ3MM3DgQNq8eTMNGjRItq1lAuj+DUE4fjzRv/86n6dWLSI0q5k61fH7ELLr1hHt3Rv62rJlRI0bq1KOsJJiHX36EN24gdqGah4I1cWLlUAGH3+Mg91W9EJHwCoLwawTbBE1I2wRZRiGMVWsKMqOsQg1Fo8fP6ZHjx5ZppcvX3p+JQ8fEoUXDvEwnHmwXXClWwPX/4sXyvoJYJRCmUfrBOdq1YhOnya6fz90nipVbJeDeXQ2aLEQNbMQvX4dhUf13hqGYRgmDKJGDb3V/vfff3Qa4sBHoIj9xYsXfbY+M5EnTx7ZnU2bYLH2KOfOEY0ZQ/TFF87nmTdPWTrhoncGxCLc6gjtwD0fico//BCqAwAsoVoLcA3tf7wX1jza+zrBQtSM4MCJFk0dkDdv6r01DMMwjIv06dNHxmw2btyYDh065NX9dv/+fWratCllypSJxagDELeLxjHa1Lt3b8c7Ei5u1PAOa9Lc3xoQi4jpbNSIqG1bx8vdtEkJ0EmTiPLmdf5DIvYTLv727VVDmxw5VMwosBrkmBVdvwFGH8WLF5cdUlKkSEH169cPd6S4cOFC2foSwd+I4ylUqBD9+eefNvMg7BV9yFOnTk1x4sShKlWq0NmzZy3vX7hwgdq0aSPLJOH9rFmz0oABA+iVWVpmQoRqRe65qD3DMIwpQNb88+fP5T1q/vz5VLhwYapduzbt9JJrdPny5fTmzRvKly8fZUQSC2MDtEdQUJBlQvc2h/ToQXTyZNhTliyh86Pr4fvvqyx362Qha7ZsIUIprZEjVbJSeHTvTvTgAdGlS0R37qjMe6CtF5n09oYp7X+8F9Y82vuBKES3bNlCnTp1ol27dsk+0ugqVLVqVRk47gyUnsCIEicu6qK1bt1aTtadK4YOHUqjR4+m33//nXbv3i0FK7pJvEA8hUxsO0XBwcE0YcIEOn78OI0cOVLO+611ZpvR4aL2DMMwpgJ1PFFK6fDhwzKDHS57dNJDK+f3339f9oH3JDDcgI8++sijyw04kidXJY/CmrTYTFhCK1YkQlLatGmOLZYo4YQEpSFDiNq1c307YHmFEQrxoXDTo0RTkSLqvdKliXD8vH4dOj8SnHLmJEqcOHQelIGyBvPgdT0RBuLWrVvI4Bdbtmxx63OFCxcWffv2lc+Dg4NFqlSpxLBhwyzvP3jwQMSKFUvMmTPH6TKGDh0qMmfO7PT9Fy9eiIcPH1qmEydOyG29fPmy0IWGDVHuQIjRo/VZP8MwjJ/w5MkTeT3HhOe+4syZM+Kzzz4T0aNHl+sePHiwx5aN7xE7dmy53EOHDnlsuf4A7tteuX9fuSJEtmxCVK6snl+/HjppbNwoRNy4QvTubfv+3buh8yxcKETOnLbLHjpUiCNHhDh2TIgffhAiRgwhFi0Kff/BAyFSphSieXM1z9y5aj0TJoTOs327ENGjCzF8uBAnTwoxYIBaztGjQk8MFVyAGA3gasFduDc2bNgg3fnlkS0WEgh+48YN6Y7XQBByyZIlw3R/YN1hrRdhBNZBzQhy1hXOnGcYhjE12bNnpylTpsjOR6g32qFDB8t7uLfNnTs3wkXw4SWEFxAhaAVQt5LxPrAuIkEJVkfco1G0Xps0ZswgevYMosL2/Y+srNbQQvZhiqtWqWL1xYoRrVhBtGSJqgGqkTAh0dq1EEHKGotwgv79bS2uCBWYPVuFCxQsiEw2Vd4pXz7SFWEQ3r59K2rVqiXKli0b7rywcMaLF0+OImHpnDJliuW97du3y5HOtWvXbD7TqFEj0bhxY4fLO3v2rAgKChITJ040j0UUIxr8fJ98os/6GYZh/AS9LKLOgGevaNGicnuyZ88u73EvX750axnNmjWTn+/Ro4fXttOseM0iypjbIopY0WPHjskRoCsBxsg23Lt3L/3888/UvXt3WSw4Ily9epWqV69OjRo1orbOMtsIiWqxbIKasQ26wjGiDMMwfgkSjOrVqye9dEi0RXJttmzZaOzYsTLZKTyQA7F+/Xr5/MMPP/TBFjNMxDGEEO3cubPM7tu0aZNLXQ4Q4I2TEhnzcGc0bNjQUv8rVUj21027zDD8r72nce3aNRkgjkDxic4y24yKtp8QGM0wDMP4DTFixJC96VH/c/jw4fLehS5AXbp0kaWYZoXTmxz3SAhY1BAtrXciCsMYWYgixhMiFFmEGzdulLEsEQGjP60jApaBkxbxNRromIDseesTEpbQihUryk4X06ZNsyk4bAqsY0S5SyvDMIzfgbajMLYg9+G3336TJZhu3brlUgtSeO0aNGhgvnsbE3BE19sdP3v2bFqyZIk8aZBkBJAMhPqeoEWLFpQ2bVqLxROPqCOK2p8QnytXrpR1RMejz6qsbhCFunXrRj/99JMMBIcwxcgyTZo0sk6ptQjFSY3R5u3bty3bZG81NSxaHVEI8Lt3iZIl03uLGIZhGC8QO3Zsmcj0+eef0+LFi6lu3bqW93799VdpLYVgRe1shjEbugpRTTxCFFoDC2WrVq3k80uXLtmM6FBjtGPHjnTlyhUpVnPlykUzZ86kjz/+2DLP//73Pzlfu3bt6MGDB1SuXDlavXq1PJkBapaeO3dOTvahALDSmgLULEuRgujWLWUVZSHKMAzj9y575DNoPHv2TOZJwJiC+NHPPvuM3nvvPRo2bBg1b96cvvrqK123l2FcIQoyllyak7EBQjh9+vRyJOpKXKtXQImGAweIli0jql2bfyGGYZgIAMMF3ODgyZMnLrm+jQBu36tWrZJidAd6kVvRrFkzaaRhDHr/Zixw8IiZ4YQlhmGYgAWhaDVr1qRt27bJZF/r+tnoZc8wZkBX1zwTSbioPcMwTMADQYoQN0z79u2TeRB10MecYUwAC1Ezw0KUYRiGsQLJvJgYxiywa97McFF7hmEYhmFMDAtRM8MxogzDMAzDmBgWomaGXfMMwzAMw5gYFqL+4Jp//Bjto/TeGoZhGIZhGLdgIWpmUOsuUSL1HEXtGYZhGIZhTAQLUbPDcaIMwzAMw5gUFqJmh+NEGYZhGIYxKSxEzQ4LUYZhGIZhTAoLUbPDtUQZhmEYhjEpLETNDseIMgzDMAxjUliImh12zTMMwzAMY1JYiJodFqIMwzAMw5gUFqL+IkTv3iV6/lzvrWEYhmEYhnEZFqJmJ2FCorhx1fOrV/XeGoZhGIZhGN8J0UePHtHixYvp5MmTkV0UExGiROGEJYZhGIZhAkOINm7cmMaOHSufP3/+nIoVKyZfK1CgAC1YsMAb28iEB8eJMgzDMAwTCEJ069at9N5778nnixYtIiEEPXjwgEaPHk0//fSTN7aRCQ8WokwgMWcO0e7dem8FwzAMo4cQffjwISVJkkQ+X716NTVo0IDixo1LtWrVorNnz3pimxh34aL2TKCwfz/RJ58Q1a9PFBys99YwDMMwvhai6dOnp507d9LTp0+lEK1atap8/f79+xQ7duzIbg8TEbioPRMobN6sHm/cIDp1Su+tYRiGCRyePyd69iz0/4sXiUaNIlq71rdCtFu3btSsWTNKly4dpUmThipWrGhx2efPnz9SG8NEEHbNM4HCjh2hz//5R88tYRiGCSzq1SP64w/1/MEDopIliX75Rb0+frzvhGjHjh2lRXTq1Km0bds2ihpVLSJLliwcI6oXLESZQEAIou3bQ/9nIcowDOM7DhwgCskRor//JkqZUllFIU5Hj47wYqNH5EPIlMcE3r59S0ePHqUyZcpQ4sSJI7whjAdiROGufP2aKEYM3p2M//Hff0Q3b4b+z0KUYRjGd8AtnyCBeg53/EcfEcEYWaqUEqS+dM1PmTLFIkIrVKhARYoUkbGjm7X4Lca3JE+uxCcsRhCjDOOPaNZQhABFi0Z06VKkLn4MwzCMG2TLRrR4MdHly0Rr1hCF5AjRrVtEQUHkMyH6999/U8GCBeXzZcuW0X///UenTp2ir776ivr06RPhDWEiAUYknDnPBEp86AcfEBUpop6zVZRhGMY39O9P9PXXRJkyqfjQ0qVDraOFC/tOiN65c4dSpUoln69cuZIaNWpEOXLkoM8++0y66Bmd4DhRJlAsomXLhsYpsRBlGIbxDQ0bKk/Uvn2o3xn6euXKRCNH+k6IpkyZkk6cOCHd8ijf9AGsEzJ04BlFg7uM0QcWoow/8/Ah0bFj6nmZMixEGYZh9ACGSFg/4Yl99Ei56hE3miuX74Ro69atZUvPfPnyUZQoUahKlSry9d27d1OuSGwIE0nYNc/4M7t2qRjoLFnUhbBcOfX6yZNw0+i9dQzDMP5P48ZEIS3eZU1RJK3jtQIFiCLR4t1tIfrdd9/R5MmTqV27drR9+3aKFSuWfB3W0F69ekV4Q5hIwkXtmUCID4U1FCRLRpQ7t3q+bZt+28UwDBMobN0a6o1atEgZB1BPFKWbItHiPULlmxoiTsCOli1bRngjGA/ArnkmEIQo4kM1cEGERRRxomj5yTAMw3g3RCqkxbuMEW3QgChuXKJatYi++cZ3FlGwZcsWqlOnDmXLlk1OdevWpX84aUBfWIgy/sqbN8o1b20RBeXLq0e+9jAMYwQuXCBq04Yoc2aiOHGIsmYlGjCA6NWr0HlQ5hKdiFKnJooXj6hQIaJZs8Jf9oYN6vqHeEyEJ/Xsqa6N7ix3+nSiKFFsJ3das6dPT7RzJ9HTp0qIauWb7t93bzmRFaIzZ86UcaFx48alL7/8Uk5x4sShypUr0+zZsyO8IYyHYkSvXiUKDubdyfgPSFJ68kTVqcubN/R1zUWEbh94n2EYRk9OnVL33wkTiI4fV5nkv/9O9O23tt4dLabyyBEk3hC1aEG0fLnz5R4+TFSzJlH16kQHDxL99RfR0qVE1uGQri4X19Hr10Mnd2oxd+tG1KyZMnylSUMU0uJduuwj0eI9ihBw8rtO7ty5ZXwo6oZaM2LECJo0aRKdhKssALhy5Yos4n/58mVKp1kj9QQdlTAiwUmAovZovcUw/sC4cUSdO6vRN4ooW5Mxoyonsm4dUUjiJMO4y9OnTyl+/Pjy+ZMnTygeLEqM39+/UQEorWbEIZI5L1rei8cYNkz1Yf/3X+fz1Kql7tlTpzp+H0IW17i9e0NfW7ZMJQqhmLzW7Si85cIiCjGJuM6IgtJNKGiPikkh5wytWEGUKJFt6JQ3LaL//vuvdMvbA/c8itszOoHOSiH1XenKFf4ZGP+OD9XgeqIMw0SQPHnyUMKECS3ToEGDvBtXGdF5Xr581/UN1/+LF0T797u3XHiPMICHmx2ufFhu3QGZ8h9+qNz/mh0TgjeCIjRCQhSjiA2IVbBj/fr18j13wI9evHhxSpAgAaVIkYLq169Pp0+fDvMzCxculH3uEyVKJEethQoVoj///NNmHhh5+/fvT6lTp5ZhAwglOHv2rM089+7do2bNmlFQUJBcVps2beRI2NQEUJwoLBg7d+6kP/74I2Cs8BToheyt40M1WIgyDBNBYBF9+PChZerdu7dn9+W5c0RjxhB98YXzeebNU5ZOuNKdUa2aGpDPmYPe6ioE74cf1Htwr7u63Jw5lXV0yRLEWSoPKq6r7miGP/5QbngIYUwIB7DTYG4j3OS3334TMWPGFO3btxd//PGHnL744gsRK1Ys8fvvv7u1rGrVqolp06aJY8eOiUOHDomaNWuKDBkyiCdPnjj9zKZNm8TChQvFiRMnxLlz58SoUaNEtGjRxOrVqy3zDB48WCRMmFAsXrxYHD58WNStW1dkzpxZPH/+3DJP9erVRcGCBcWuXbvEP//8I7JlyyaaNm3q8rZfvnwZQwH5aBg+/BDjEyHGjhX+xL1798SGDRvEsGHDxCeffCJy5colokaNKve/NhUpUkSMGDFCXLt2Te/NZTzJlSvqmI4aVYhHj959//hx9X7s2EK8fMn7nokQuOdo15Kw7j+Mf+D2/btnT3WdCWs6efLda1fWrEK0aeN8uRs3ChE3rhAzZoS/Db/8IkRQkBDRoqnPDBqk1jt3bsSX++qV2sa+fcNfv7YNWO7//ifEkiVq+uYb9dqIESKiuC1EAYRg2bJlRZIkSeSE5xB9keXWrVvy4NiyZYtbnytcuLDoG7Ijg4ODRapUqaRo0Xjw4IEUynPmzJH/Q8RiPXv37rXMs2rVKhElShRx9epVh+t48eKFePjwoWXSlmEoIdqlizowe/cWZgS/Hfb/8uXLxQ8//CA+/PBDkTFjRhvBaT3hdy5VqpSIHj265TUI1KpVq8oB0iNHwoUxF/PmqWO6UCHH7wcHC5E0qZpnxw5fbx3jYd68eSMuXLggB54TJ04UPXv2FA0bNpTX+JIlS4off/xRGi5wrTCbEMV327Fjh5g9e7bYunWruHjxonj9+rVX1sV4WIjeuqWEZliT9UAYOiJ7diGaNxfi7VvHy9y8WYh48YSYMMH1nwvHPZb97BmEjLru7dkTueU2bChEkyauzZspk2NxO326ei+CRKiO6Icffignax48eCCz5j/55JMIW2dhGgdJwounCAFCeuPGjdKdP2TIEPka4lRv3Lhh6fgEEPtRsmRJ6cpt0qSJfIQ7Hi5+DcwfNWpU2SHK/rtpYQTff/89GRoTuebx2yHe+MCBA3Tw4EE54fktBF47IHPmzFSkSBEqXLiwZULoBbh9+zbNmzdPVnTYtWsXrV27Vk4Iy0C4x6effipb0cZAHC1j7kL29qD8CLoswdWEMk6lS/t08xj3ef78ubxOnz9//p3pwoUL9Mq61I0duD7369ePsmfPbrkPlShRQl67jQjuabgWLV++nFauXEl37LqAoREMkmUyZMhAGTNmlJP2XHvkxCkDkDy5mlwBbvP33ycqWpRo2jTVCtMelFqqXZsIuqVdO9e3A9c7ZKsDuOkRDlmkSMSXCzf/0aMqI98VEAbg6FqM15yFCLhAhISoIy5evEjNmzePsBANDg6mbt26UdmyZWX70PBObpy8L1++lCfyb7/9Zul5DxEKUtpljeN/7T08IibVmujRo0sBrM1jD2JHunfvbvn/6tWrMtDZUBhUiL5584ZOnTplEZua8HyEPrV24IaCVrGa6MQj4oAxcHBG8uTJqVOnTnI6d+6cHBBBlCIueM6cOXLCPB9//LEUpbhxoT0tY6L40LAC4REnqgnR//0v0qvEtejnn3+m69evy2MRlULwiOoYfNy4BmLwHQlNTLh2hgUGjJkyZaKsWbPaTBhwLlq0iNatWyfP7aFDh8opTZo0VK9ePSlKK1asqPuAE9cgCM9ly5bR1q1b5fXP2iiSP39+unbtmqy48vr1a7p06ZKctjnpEJY0adJ3xKn1c1zb/PG4hLECvzlKRWoVDQwPjm2UNEIy0PDhsJKEvqclE2/apMRi166qIPyNEM0RM2ZoYhG6FiFeFeWgrLPvUb4JwnbhQqLBg1UcaLRori8XcaWlShFly6Yy57FMlG/6/HPXvh8+h3Val6MCKCeVPTvpLkQjC0TEsWPHnJ6M1iC56dChQzK5CIlTEIhZsmSRFyFvYV/WwZGIClQhihs3Rvq4ceMCqz3i4nr48GE6cuQIvUB2nx0xY8aUF2Vr0Yn/ceGJKGiwgEQ1WE327t0rBencuXPlBW3s2LFywjwQpEhWw3PGoDx7pmrmhWURtU5YgmhF8H0krWNIiMQxZA9uhjlz5rQIU+0RxxCO5UAERoE1a9bIgaUmNOHpgIcsLJAkai80tQmCHwYGRyCp9PHjx7Rq1SopSlesWCGvNePHj5cTBqy1a9eWorRatWo+sSZCTKLdNcQnJvuEWxwz2CZMMLRoQhnXTRg+YMTBtdL6UXuO/Xv37l05YR87Inbs2O8IVSQOw/iiTTC86C3Q7Xn27JkU45oQx2T9P57jvgHDQtOmTckUoMQSEpQw2Zd11DLMZ8xQ1zZk6Ftn6VeooCyaAN5h+8TtVauIfv5ZZdAXLKgG3zVqhL7vynJReL5tWyVSEydWVlt4nVw1qsEr/PHHqm6oZhzAdRcJ7BCovqoj6gwIDgiJtzD1uknnzp1pyZIlcvQIF6y7fP755/KgxQURF0FczHDSwpKmUaFCBfn/r7/+SlOnTqUePXrQffwoIWDUihN6/vz5Dl3zhq8jCs6fVyMWZLKh80EkR8mawLQWl44eMVmP+h2Bm7i1Wx3HCm7kvrg44kaBqg4Qpbh5wTWogZANiFJYS2FZ8GdwqpvKcoKLHS6icEVhcOVs21FDFxdVHPMo5ByJwso45gsWLCgHxfCyQMjAmg8rl7NjHKIJ1xx7gYoJFjB/IyyLnzWwVMJA4EhswsrniWMRXjGEZ+G8xj3EOrQHoTlVq1aV13OUHAwr5MvdOqK4Lq5evVruBzxqYWWadw33GwjPWrVqyTCCiILlhiVUce119RaO728tTsOaIltLEzrg5s2bNiLTXnDahyk4A/dsNM7xJIa8f5sFlItCoX6tWk3u3EQ9ehAVLmxOIYpVd+nSRV5ENm/eHOET9rPPPpMCFMvAMnEB/Prrr6XY1KyXGBFOnz5dxoii3A/c6vv27aOiGBEQyTie6tWrywMUnzflgQyrI0QouHdP3ZzDcJ3hghCWyMRoPTyBaQ2EHPYdYjfxiAlhFjgucPMxQhwXbjKLFy+WohQuPggPTUzg94coRU3cyFhlvQ2OcVgTNEuJ/YTf1tHrsFLB5fnee+9RuXLl5COsNYYVpxjVwwXUsCHR/Plhz4vQnPXrVfH7jh0jvMoFCxZQw4YNpcUOcYyaeMFgBtY+XDsgTK0fwyr7hnPBXqDiEeeGYfe7HbgGIK4ewhMTvrc1+E7vv/++vH5rQhMGBV+fQ7j3YDtxP8FkXdca5zfEIUQp4sbtr9nhCVGcc8ePH7dYPbEe7doBkiVLJkUnxCcGML4agECII9TBWpziEfcnCEFM8Aa5ayDC9ocnVvH7Yj32Vk1MeN2Vewf2uWbBhTXXfkIInje8DYa8f5sZDAAnT37XZe8iLgvR0aNHh/k+Tobhw4e7dcB37NhRmt0xksUN0fokwGgWtGjRQh6MWqFZPCLJCBc7nIQIAO/Vq5d0y8AyCpC4NHjwYJoxY4a8IMJNC/cwaobB6glq1KghT9Lff/9d3mRat24tl+tqm1LDHsiw6mGkGYZlCK5quKWtL6RhARGviUv7R+05Lkxmc09CaP/1119SlGJQYn1x/Oijj6QorVSpklM3YUTA6YYLNI5duJ0wac9hoXdFUOJ1fMYT4AaqiVJM8BoYxoWHxhloT4fRN7qBhAVin9DTuUkTFcQfAXA+4PsfPXpUXjN+0Or0hfN7YtDmSKBiQBdWeBEEXN68eaWHAOvFBAFsBDBogYcJwhNucBxz1ha/8uXLS9EFS6MRw1vwu+Car4lSPLcG9au1ZCf8Do6EKM5JGDc08QmBZw0s55rLHcvz5HXCk+C4xnVDE6ZhTbAo437oCawTsbTJXnDiXq/HgMyw92+zghakSJqKgEfcLSHqqsvcne5Kzg7AadOmUatWreRzxH3CigNrJujbt68UDziQIFZxEenatat0rWrgKw0YMIAmTpwoL6i40SKhKUeOHJZ5cGFFSAAutLDUNWjQQIptV4OiDXsgwzx+6BDRypW28SNWYN9CpOMigN/VmbjEIwSmYYSJF4F4mDVrlhSlyNzVwH7AsYVHTTA6EpHuPLo6AAgP/C5wcTqbYM2z/h8iB4Oxf/75R07IQLaP3cUNuFSpUhZxiue6ZO1iH2FQBQG0ezdRiRJhz49A/UqViNCuD+3nInBz06yhEIk4Blyt3hGWW9VenOIRllVnA3a4srXwFQhTrTqEL27WSADSXO44PqwtWtgXNWvWlKILsZdhJQ8aEexzeEIQ/wtrpvVtD/cQWDN/+eUX+T/iyOEtwQTPgwaMGJUrV7a43N1t4GIGsF8wIHZFtGLf2AtNa8GJ4xaDFiNi2Pu3WfGVEGVMciAjaw59XydOVEHJDoA1AzcaUwWB+wicDrhRQZBiwGNtCfIGuFDjBoeYLNzcwxOT1hMEYmQECsrk7N+/Xx4LSBLEZB03rVk1EFqhWUyRbOGTWFq4fxF7BM8I4u/CGwxBMEAcwZqDns5uxppjcADRB8sZBrs//vgjeQsMRjQ3P9aHeHYkX+Ja4swjoYlS7RFWyMha4CA0d+zYYXG52yfZIIQAFk8Ir9KlSxtWVETEEwIvHCyliC8NywIIoaVZPeEdMXLIDuMH92+zwkJUHwx7ILdvTzRhgnJTfvedw1mw3dh+CC5YvBjnQg2JCLhJ42alCUZPPWIykjsPYgwWUwhSzWrqSBzBgmTtzofHwuMWuylTVEmR8uWJtmxx7TOoIbprFxG8Jy1burU6WMrgFfGUNTQiIHkDsfZaeTOIU1hQHVnQMQgpUKCAjThFPLYWeuQMeIi0Yxoud+uBh5Zko4lPhD/5O7BaI/MeSaqwmAKEaKEcFPYB3O9mieVl/OD+bVZYiOqDYQ/kn34i6tcPdU5U8LAdcMViVA/LH9wr9vVUGcYaJB5oohQCFQkb9iCEQxOlEKgowRXpxDQcv+iJ3KuXbSmSsEANUdTFc3LsOwNCD1ZfiMA+ffrQTziHDALcn8jg14QpHmFFta78oIFBDayY1uIUjxC4mtUTv6N1WAAs63C5Q3wiw9wfs/xdwd2secbcGPb+bVSsaqg7BPVSkV/DrnnfYtgDGdag1q2JqlUjWr36nbfhfoNFCxda1OPj0T7jDkh6gDtXE6dI8rLPjoVlERamSB1buXKpOnrLlqlwE1fAvHXrEiEW3L4GXxjARYvkNFhDEeMOcWZkICTPnDljI04x4bdxBVQMgfDEBI+IkazyesFCNLAw7P3bqKBTlCsgVj8C+EfQDxMKkjXCKGqvJZMhSYlFKOMuEGmaiNEsdnv27LFYTDdt2iSTftBy1TqB0C1Q9UETku607NQKLJ85Q3TzJtqpuWQN1Vr3olah0UWoteUTk9bJDh4OVC6xFqd4xPmOpDZrlzsSohiGYVwmggLTVViI+hva6M5JGz3UWwV8M2I8AcI8UNlC62qGJB90JUK3M7h8YWV0m507Q62i7ghDxHWiPfCxY0To0IY2d+GwdOlS6ZKHW/arr74is4JBJSw7mLRBghYDidhPdjUzDGNU3A7kwsj6jz/+cBijxBhIiKLFnoNC29YWUYbxNN98841MckFtTVfqcDoELefCa+vpDCQ3AfSdDwdYEc1mDXUXxHyyCGUYxq+EKALg0bUoVapU1LZtW9qFLFXGOMACpRXFdmAVZYso402QtT1mzBj5fNSoUQ6Tm1wWopqr3R20vvMuCFGU8IH7GtZQWHAZhmEYEwhR3Fxg7UDReXRhQE1KBL+jqxKysBkDWUUdxIlqFlF2zTPeAl3L0EYRSUxoGuFWqeJXr4j27Im4RVQTomjq8OiRS9ZQtBn2R2sowzCMGYhQjRXEHCHLFBYFZJ8hYB4t8ZCFhhsQigQzBkhYsrOI4uaLQtqAXfOMNxk5cqS0jqJFIlrKugwEJLo9Id7Tqu2vW8c+wk5Qe1OLNXUSG8rWUIZhGBdBdRSEWzlJhI4MkSr2h2xZtNJEazTUo+zdu7fsXY3MTLjvGZ1wYhFF8epHIVYiFqKMN0GRe9TkBD169LAcd+GyfXuoNTSi5Z/Ccc9bW0NhscU1i2EYhgkDdFZDnWa7cn26CFG44yE80cUDBaxv375Nc+bMkd1IcHGfPHkyrV27ln7//XePbywTOSGqueXRP55b1THeBoNRtKK8fv26Rfh5NT7URSGKwu4ob4QkHohkhmEYxgUqVXK90503yzehPAiyYj/77DNq1aqVw77TaD1XvHhxT20j4yEhyolKjB6JS4gZ/fXXX6l169ZyAOsUxJJaW0QjK0R370Zjd6JYsaxWIei7kNa3iA1layjDMIyL1Kihut0dPUpUtCh6Ddu+j4Yi3haiuIhv2LBB9uKNEyeO0/mCgoJkYWvGWEXtuXQT42uqV69OH374oexeBDc4rgtOGylcvEh0/bpyAUVmIIvOSmhde+sW0d69ROXKWd5avnw5W0MZhmEiQseO6nHEiHffw3U9gi0+o7orRCtXriwTlBjzFbVniyijV+ISBq5btmyRYTxO0ayhRYoQhTHQDRdcEDXxaeWet7aGcmwowzCMmyAJ1NkUQRHqthCNGjUqZc+e3eWexozOQhQWIbgmQ2AhyuhBxowZqW/fvuEnLkWmkL0LcaKwhh44cIBjQxmGYSILqpvolaw0ePBg2T3lGNroMcYEpW9ix1bPr12zvMyueUYvIEAxiL1x44bFKunUIhqZRCX7DktY5tu3NpnynTp1chjbzjAMw4QBrJ4//qjC/+LHh3VLvd6vH9GUKeQzIdqiRQtZtqlgwYLS3ZYkSRKbiTEAcE3axYm+ffuWLiIGj4vZMzoQK1YsS8el0aNH01EEu1sDK6n2micsogULqi5jIctdsWIF7d+/X1aL4NJyDMMwEeDnn4mmTycaOpQoZszQ15GEOnky+SxrHp2VGJO451G8PiRO9OrVq/T69WuKESMGpdVEKsP4kGrVqlGDBg1owYIF0iqJmFFL4hK6KSHOKFMmojRpIr+yaNGUoF2zhsTWrfTdH39YYkPZGsowDBMBcB2dOJGocmWi9u1tB/6nTpHPhGjLli0jvDJGvxJOWnwo4vWi4SbNMDowYsQIWrVqFf3zzz80a9Ys+vTTT9Ubnijb5ChOdM0auj5/PltDGYZhIgsMW9myvfs6jAivX+vTWenFixcy8cB6YowpRLnHPGMEMmTIINsBA7jIHz586LlC9k4SlmKinijHhjIMw0SOPHkcNwr5+2+iwoV9J0SfPn0q3Vto6YnOJIkTJ7aZGGNbRLm1J6M33bt3p5w5c9LNmzdli2AZAK/1hfekRbRECXobPTole/2a8sWOzbGhDMMwkaF/f8Q3EQ0ZoqygCxcStW2rYkfxnq+E6P/+9z/auHEjjR8/XiYgoKUnslHTpElDf4TEYTEGQIsDDYkR5dJNjFGIGTOmJXEJj6dxMXv8WGVh5s/vsfWIWLHoaEj1iH4VKsjBM8MwDBNB6tVDj2Si9etVVyWIz5Mn1WsffOC7GFH0aYbgrFixomzZh37z6CeN2EPEfDVr1izCG8N43zXPFlHGCHzwwQfUqFEjmj9/Pi3r1Yty4sVSpVSSkYdALOrhJ0+oEBHVYW8NwzCMZ0Ke1q0jT+K2RfTevXuUJUsWSytP/A/KlStHW7du9ejGMR4QomiZ+OYNW0QZQyYuIbwnpVaLzoPxoVoXJS2aKQ5afTIMwzCGw20hChGqWddy5cpF8+bNs1hKEyVK5PktZCIG3JDo2f32LT2/cEHG4wFtEMEwepMuXTrq378/afLzSYECHlv26tWrae/evXQwdmwSKBGFUmYYlDEMwzCug/rwd+6o5/As4X9nk69c83DHHz58mCpUqEC9evWiOnXq0NixY2WNSlg4GIMAF2fq1ESXL9P1EGtQwoQJOaGMMRTdPv6YYvbsScFE9MPatTT0o48ivUzrnvLNOnWiKBs2EB06pLI9Gzf2wFYzDMMECCNHquYgwEt15N0Wol999ZXleZUqVejUqVOyRh/iRAt40KLBeMg9f/ky3QvpWMPWUMZoxNy/Xz7iCP1l0iT6pH17KlQIUZ0RZ82aNbL7Gzq/oR0xvXrFQpRhGCYiHD5M1LAh2uMhyURVNoG31YNEqo4oQJJSpUqVWIQaOE706enT8pETlRjDEVLI/mbWrBQcHCw7LuHRE9bQDh06UMqUKS31RB3Wv2MYhmGcgwonT56o5++/j0Qh8jRuC9EhQ4bQX3/9Zfm/cePGlDRpUtk2Ei57xnhC9A33mGeMSkgh+2JffikTl3bs2BGpMnCwhu7evTvUGgo0IXrkCNGDBx7ZbIZhmIAgUyai0aOJtmzBSF/VfEZiuqPJV0L0999/p/Tp08vn69atkxPKpNSoUSP0ws8YqpZo9Bs35CNbRBlD8fw5UYhrPknt2qq4fUit4gcREIywhqKmMWjfvj2lSpVKvYFHtKXDRVTr4MQwDMOEz7BhRFOmKGsoEj8//JCoYsV3J7zvKyF648YNixBdvny5tIhWrVpV3jyQpcoYzyIa7/59+cgxooyhgAhFf2IIxcyZqWvXrpQ7d266ffu2pQ2oO6xdu5Z27dpFsWPHltcjG9g9zzAM4z7160P4EaGFOwbzCPWDprCfIuGyd1uIoo3n5cuXLSVSkLCkWSPeolUfYzghmgSWJxaijEHjQ2Xwe5QosuMSKnCA3377jQ4ePBjh2FCLNVSDhSjDMEzEQee7TZtUwlLChI4nXwnRjz76iD755BPZGeXu3bvSJQ9w00DmPGM8IZpWCIoSkljGMIZBc5NbFbJH4mOTJk3cTlxCiJBTa6i1EN2zR4UEMAzDMOEDS6hG4cJEz56p1xxNvhKiI0eOpC5dulCePHnkxT8+VLJs4HOdOnbsGOENYbxA6tSymHcsIsqfOjXFQvkFhjEC1vGasIhaMXz4cHld2blzJ82YMcOFRYVaQ21iQ63JmlWFACAUAGKUYRjG01y4QNSmjbIaxomjrjuIfUcJOY3Nm1XPdtT5Rr/2QoWIZs0Kf9moh4xrJWp64lrWs6fsmmizbsRw2k+7dtkuZ/58dCMiih2bKH9+opUrw14vitjfuqWeo2kR/reftNcjiFvFoFC0/osvvpDxW/aJL9b1RRmDEDMmvQgKojgPH1JRRzdnhtGLs2dVtw4MjooUsXkLFTggLL/++mtp3axfv36YjRgwIIZodWoNBbggwyqKizDKOFWo4OlvxDBMoHPqFBG8OBMmqATJY8eI2rYlevoUI2w1DwbgqLkOIYnycsuXE7VooVzbtWs7Xi4qEtWsSdSnDxGqily9ilG37JxoWa7G+vVEefOG/p80aehzrLtpU6JBg9S6Zs9WMaAHDhDly+d43Rs3hnZNgmveGwg3CQoKEv/++68IdC5fviyw+/BoZK6lSQPbkxhRqZLem8IwoUydKo9LUa6cw73y6tUrkSdPHnmOdezY0emeCw4OFmXKlJHzde3aNew9PGaMWmfVqvxLMDY8efJEHkOY8Jzxb3x6/x46VIjMmcOep2ZNIVq3dv5+795CFCtm+9rSpULEji3Eo0fq///+U9e3gwedL6dxYyFq1bJ9rWRJIb74QuiJ2655WCcWL17sERE8aNAgKl68OCVIkIBSpEghl306pPi6MyZNmkTvvfeetJBgQrIUuqhYg77qrVq1ojRp0lDcuHGpevXqdBYWGLvs/+bNm0s3HuoXFilShBYsWED+xo2QDgg54sbVe1MYJsz4UGtixIhB48aNk8/Hjx8vu7c5Yv369bL2KKyhPWFhCAstThTrtnZpMQwTkDx+/JgePXpkmV6+fOn5lTx8GH4f9ofhzIPtgivdGrj+X7ywlMCzULcuUYoUROXKES1davseaoCGJJhbqFZNve4Kq1cTbdsW+j+u0Qgt+OQTlTkfQdwWotmzZ6cffviBGjZsKIXk6NGjbSZ32LJli0xIQJIB3Gtw/aMU1FOYsZ2wefNmatq0KW3atEm641BKCp+5ClN1SLwYBO2///5LS5YskUlUSNKBYLVebosWLaToXbp0KR09elQmYaEUlTuZumbgYkglgwxRI91Ei2E8h5P4UGsqVqwoEyNxTjtKXLKuG4qQodSIuQoLuJ7g/kKXEG6+wTABD3JdEiZMaJmgaTzKuXOqM9EXXzifZ948IpS+bN3a+TwQi7hmzpmj3PHQOz/8oN67fl09Il/nl19U+NGKFUqIwu1uLUZRhgnhANbg/5Ba4+GCWvFaUhJah3fvrkIG/vtPPY8o7ppQM2XK5HTKHJ75ORxu3bolzeVbtmxx+TNv3rwRCRIkEDNmzJD/nz59Wi7j2LFjlnnevn0rkidPLiZNmmR5LV68eOKPP/6wWVaSJEls5vEH1/yQRImkuf5GjRp6bwrDKO7eVS4kTLduhblXrl69Ks9vnGuTJ0+2eW/dunXy9VixYsn5XAIuMKx35Ej+NRgL7JoPLLT794kTJ8TDhw8t04sXLxx/oGfP0GuWs+nkSdvPXLkiRNasQrRp43xDNm4UIm5cIUL0S5j88gtiI4WIFk19ZtAgtd65c51/pnlz2/CnGDGEmD3bdp5x44RIkUK4RLx4KgQADBggRIMG6vn+/UKkTCkiittmsv/++8/pBCtkZHgI8zTqXoZnxrbi2bNn0pKqfUYzrcNVpxE1alSZMb7NyqRcpkwZ2ar03r170tIyd+5cevHihbTCOALLtTbhw6RvdLBfjofs00Rar1iG0RstizNHDqLkycOcFeE1mtUTrnecr/aZ8rCGYj6X4HqiDMOEgLDAoKAgy+S0skyPHkQnT4Y9ZckSOv+1a6rTEDw+Eyc6XiZaZtapg1JEKlkpPGBxRMe5S5dUoicy74H1eu0pWVJZZTWQtHzzpu08+N/VZOaYMVX5Ji0pqmpV9Rz6KxLlm9y2iGq8fPlSnDp1Srx+/Vp4Algta9WqJcqWLevW5zp06CCyZMkinj9/bklyyJAhg2jUqJG4d++e3M7BgwfL0U9VqySF+/fvy//xevTo0WUS1po1a5yuZ8CAAZZgduvJyBbRc+fOiYoho7XgXLn03hyGUXz7rRrJt2rl0h7BOZ0vXz55vrVv316+tn79evetoWDbNrXu5MmR6cS/CCNhi2hg4VWPJiyh2bML0aQJXLaO59m0SVkXx46N+Hr69RMifXrn6wCffy5E4cK2yUq1a9vOU7q068lKdeoIUa2aED/8oKyr+K4A2gnfOYK4LUSfPn0qPvvsMxEtWjQ5nT9/Xr7euXNnMQim4giCG0zGjBndOjCwvsSJE4vDhw/bvL5v3z5RsGBBeaBhG6tVqyZq1KghqlevbpkH21uiRAl5Qzt06JD47rvvRMKECcWRI0ccrgsme2sTPkz6RheicF2GdNhWBz3feBkjULGiOiZdDIMBCNfB+RYlShSxd+9eUa5cOfl/ly5d3Fs3XG+xYqn1nzrl/rYzfgkL0cDCa0IUwixbNiEqV1bPr18Pnezd8ciEt37/7t3QeRYuFCJnznez76FPEHaoCcFFi0Lfnz5dud0RIoDp55+FiBpVVSjR2L5diOjRhRg+XM0D9zqWc/Soa9/v4kWVdV+ggBDWoVLdugnh7rU4MkL0yy+/FEWLFhX//POPjLPUhOjixYtFoUKFIrQRnTp1EunSpXOrLNSwYcOkcMRNyRkPHjyQcacAolMrAwNLoX0cKahcubL4wsWRgRliRCdMmCDiWMewPHig9yYxgc6rV0LEiaOOx+PH3frop59+Ks+5tGnTWqyhV7QRuTuUL++2EGb8GxaigYXX7t/TpjmPIdVo2dLx+xUqvLsca95/X4iECVXJJpRcWrnS9n0I0dy5lchFLGmJEkLMn//uNs6bJ0SOHELEjClE3rxCrFgh9MZtIQq3986dO+Xz+PHjW4To2bNnZVKBO6AGIERomjRpxJkzZ1z+3JAhQ6QrXduO8MCyo0aNanG9w+qpBSpbA1d927Zt/UaI9urVS9XF0yxAbt74GcbjYOCIYzFxYsTjuPXR69evy/NeC4uBVyNC9OmjtgGB/AzDQjTgMMP925AgKcnaa7x4sRD16inr7suXvktWun37tqz5aQ9KI0VB9xI3QEmWmTNn0uzZs2XQMGp7Ynpu1QsaZZZ69+5t+X/IkCGys9PUqVMpU6ZMls88sUrGmT9/vizzpJVw+uCDD2RJJ5R5Arly5aJs2bLJJAfUID1//jz98ssvsoQU5vMXtOSxZ1ry15Ur+m4Qw2zfrvZB6dLIInRrf6Dmr5a4FDNmTOrVq1fE9icnLDEMw7gPylCdOaOeQ180aUKEGuUoGeWsq50LuC1EixUrRitQoyoETXxOnjyZSuPm4gYoVI1MeWSqowagNiGbXePSpUuyj731Z169eiXrmFp/Bv2pNTA/itVDcH755Zfy+RzU37Iqlr1y5UpKnjw51alThwoUKEB//PGH7GtdEzWx/EyIvtXqK7IQZQxeyD48OnfuLLPlZ82aJVuBRghkskIEozcznxMMwzCuARGKAvYA4rN8edUmdPp0okg0BHKr1zwYOHAg1ahRg06cOEFv3ryhX3/9VT5HdxMUqHcHhAaEByyb1lzAzSMcID4xhVeY3x87KVmDklogRqZMqpcs33QZPcH5rllEwyhkHxbRo0enAQMGRG47EiQgKlxYdSRB33n0XmYYhmHCv4ZrjUVQvgn96kH69KqklK8souXKlaNDhw5JEZo/f35au3atdNWjy1HRokUjvCGMZ0Gt07t378rn8XLmVC+GdJ9iGF24fFkdg9GiEZUooe+PwO55hmEY9yhWjOinn4j+/FPVQa1VS70Oo5d9xyZvWkRB1qxZZc93xvjW0GTJklHsrFnVi2wRZYzgloc1EnFFegvRUaOURZRhGIYJH1wzmzUjWryYqE8fomzZ1Ot//x1hL1eEhCh6tn/66aeyNzs6ETDGjg/NnDkzUbp06kUWooyeaG75CMaHehT0YQbHjhGhW5Mb3dwYhmECkgIFVI95e4YNU54uX7nm8+bNK7PYkcHaqFEjmZWOVpKMMS2iWdD+i4UoYySLaCRGzh4DlT+0kBVNIDMMwzDug5bqMWKQz4QokpOuXr1Kixcvpnjx4snySilTpqR27dq5nazE+MgiqmUXw/JjVRqLYXwGyqsdPmwcIQo4TpRhGMZ13r4lQoUixPijPz08SdaTr4So/FDUqLIm5/Tp0+nmzZs0YcIEWY+zUqVKEd4QxjtCVFpEEyYkihdPvcEJS4we7NmjLmIZMoRa6PWGhSjDMIzroI7ziBFEH39M9PAhUffuRB99pMrhffcd+VSIaqCQ/O+//y6LzB85coSKFy8emcUx3nLNo9Yru+cZPYlk2SavCtF9+9CRQ++tYRiGMTazZhEhUb1HD9TSU6XvJk8m6t+faNcu3wlRlAWaNm2a7FaUPn16WWC+bt26dPbsWdoViQ1hPEdwcLBFiErXPGAhypi4kL1XQH1dhK28eUO0e7feW8MwDGNsbtwgyp9fPY8fX1lFAeqJWjU68roQRTxonz59KF++fLJ26OnTp6l///6ypBNjDGCpfvnyJUWLFk0OFiRanChnzjO+BgWQd+40nkUUngJ0BgFcxolhGCZsYNDSOl1C861dq57v3UsUKxb5rHzT0qVLqXLlyjJOlDF2fChEKNqZ2lhEOUaU8TUnTqiRM+KUUf7DSMA9j/a/LEQZhmHC5sMPiTZsICpZkqhLF6JPPyWaMgW92Im++op8JkThkmdMFB+qwa55Ru/4UFy8EFdkJLQ4UVhsUYYuEiVIGIZh/JrBg0OfI2EJyae4dmbPTlSnToQX69JdoXDhwhQFbiwXOICe5oxxMuY1WIgyemHE+FCNPHmIEicmun+f6OBB/VuPMgzDmIXSpdUUSVwSovXr17c8f/HiBf3222+UJ08eKh2yAUhSOn78OHXs2DHSG8R4uIaoBgtRRi+MVMjeHoQYocvSsmXKPc9ClGEYJpSlS8ll6tYlrwnRAQMGWJ5//vnn9OWXX9KPP/74zjyXL1+O0EYwPnDNa8lKN2+yC5LxHTjezp1TiUGlShlzz8M9rwlRlCVhGIZhFFaGyDDBNR61oiOA2xlH8+fPl92U7EH/+QULFkRoIxgfWESTJSOKGZNIiNCsN4bxNlq2fN68RIkSGXN/a3Gi27apDH+GYRhGgWuiK1MERWiEhGicOHFou4PezHgtNvqNMrqC0Ilr1669axGFC5JLODG+RrtWGDE+VKNIEVzYiO7eJTp1Su+tYRiGCSjcFqLdunWjDh06SPf8zJkz5dSlSxfq1KkTfRWJ9H3GM1y8eJGEEBQvXjxKBiuoNRwnyvgaI8eHasBToIUNcBknhmEYWzZuVImdjx7ZvUGqNB88Xlu3UkRxu5ZKr169pKXt119/lSIU5M6dW3Zbaty4cYQ3hPF8xvw7lQ40iyjXEmV8wYsXqn2m0YWo5p7ftEldTL/4Qu+tYRiGMQ6jRhG1bUsUFPTuewkTqmvmyJGhDULcJEJF/SA4HYnOY8eOyY5LjMESlTTYIsr4EpRye/WKKEUK1YXDyHCHJYZhGMccPkw0ZIiTN4moalWi4cMpokS6PdLjx49p4sSJVKJECSpYsGBkF8d4I1FJg4Uoo0d8KKyhLtYh1g245lFsH5U/Ll7Ue2sYhmGMVf0krGYfuHbevu17Ibp161aZPZ86dWoaPnw4VapUSdYTZfSFLaKMYTByIXt70H4USUuA40QZhmFsw/qOHSOnHDlClDo1+USI3rhxgwYPHkzZs2enRo0aUcKECenly5e0ePFi+Xrx4sUjvCGMDyyinDXP+AqUCbO2iJoBrYwTC1GGYZhQatYk6tdPxf3b8/w5CskT1a5NXheiderUoZw5c9KRI0do1KhRskTQmDFjIrxixvMgW95he0971zzKO3G9RMabnD+vXDXISC9a1Bz7moUowzDMu/TtS3TvHlGOHERDhxItWaImxI3mzKne69MnwnvO5WSlVatWyZJNKN0EiyhjPO7fv0+PQsorZMqU6d0ZUqVS9UTfvCG6dUv9zzDedMsXK0YUK5Y59jFafYKTJ4nu3FFNIBiGYQKdlCnVNb1DB6LevZXHCyD2v1o1onHj1Dzetohu27ZNJiYVLVqUSpYsSWPHjqU7uFgzhkGzhqZKlYrixo3rOKBYi+O4csXHW8cEFGYoZG9P0qSqVp7WZYlhGIZRZMxItHKlGqTv3k2EnCA8x2uOQgG9IURLlSpFkyZNouvXr9MXX3xBc+fOpTRp0lBwcDCtW7dOilTGwIlKGpw5z/gCMxSydwS75xmGYZyTODER8oFKlFDPPYDbWfPo2PPZZ59JC+nRo0epR48eMlEpRYoUVLduXY9sFOOFRCUNLmrPeJsHD4iOH1fPWYgyDMMw3qojiuSloUOH0pUrV2jOnDmRWRTjAdgiyhgCuGwQQ5Qtmypmb0aLKIrxP3mi99YwDMP4PZEuaA+iRYtG9evXp6VLl3picYw3LaLsmme8jdnKNlmTIYOKhXr7lmjnTr23hmEYxu/xiBBljAFbRBlDYKZC9o7gOFGGYRifwULUT3j79i1duHAh/GQljhFlvAlKgyGj0qwWUWshumwZ0aFDoaVKGIZhGI/DQtRPQJzumzdvKEaMGLKagUuueb7BMp4Grd6ePiVKmDC0FJLZqFhRPUKEFi6sSp41b040c6bqucwwDMN4DBaifuaWRyF7xOw6RROpaMt1/76Pto4JuPjQ0qVV8wQzgu4hEJ1oWYce9BCf+B9iFE0gChUi6tmTaMMGxy3vGIZhGJcx6Z2CiVCiEogdmyh5cvWci9oznsbs8aEazZop1/zdu0SbNqluIkWKqPcOH1Zt7qpUIUqSRPVhHjWK6MQJY3kZXr8mOndOtd9jGIYxKCxEAylRyd49f/Wql7eKCTjMWsjeGWhPClf9wIFE+/er1rizZxO1bKlc9vAsrFpF9NVXRHnzqqz7Nm2I5s1TItbbwCILAYyKJSNGEHXsqFruZc1KFCcOEdoxwwvStSvRjRve3x6GCWSQp4HzHwYhnH84DwcMIHr1KnSezZuJ6tVT1w94XAoVIpo1K/xlwwOD62qCBMozA68MYvI1vvtOtdy0n7AOjenT330fximdcbnXPGMOi6hLQhQJSwcPskWU8Sx79hBduoR6bqrrhj8Cb0LTpmqC9ROF+9esIVq7lmjrVnVOTZ2qJlzkixVTwrBqVbSnI4oRw/11omvd+fPKuqk9ahMGk2FZYWPGJHr5kmj0aKJJk4g6dSL63/9CvSIMw3iOU6eIgoOJJkxQdZSPHSNq21bFzQ8fHjpYL1BACUn0Z1++nKhFCxVXj3AgR8ALA89Lnz5Ef/yhzvv27VWZOW25X3+tXrOmcmXVBcmaoCCi06dD/8d1Sm+EjgwcOFAUK1ZMxI8fXyRPnlzUq1dPnDp1KszPTJw4UZQrV04kSpRITpUrVxa7d++2mefGjRuiZcuWInXq1CJOnDiiWrVq4syZM+8sa8eOHeL9998XcePGFQkSJBDvvfeeePbsmUvbfvnyZVz95aMRKFWqlNye+fPnhz9z+/a4dQmRIYMQP/8sxJUrvthExl95+1aIESOEiBlTHVdVqoiABNeONWuE6NFDiPz51b6wnhIkEKJePSHGjRPi7Fnbz967J8SePULMni3EDz8I0aKFEGXLCpEy5bvLsZ+CgoQoUkSIxo2F+PZbIaZOFWLLFiGuXhUiOFiI9euFKF06dP548YTo3VuIu3f12lOG48mTJ/L6iQnPGf/Gp/fvoUOFyJw57Hlq1hSidWvn7+N8LVbM9rWlS4WIHVuIR48cf+bQIXW+b90a+tq0aUIkTCiMhq4W0S1btlCnTp2oePHiMuP722+/papVq9KJEydkK1FHbN68mZo2bUplypSh2LFj05AhQ+Rnjh8/TmnTpoWwlsX1kT2+ZMkSCgoKohEjRlCVKlVslrtz506qXr069e7dm8aMGUPRo0enw4cPU1STJli45Zpv1Ei5F2G9wgirXz+iGjWIPvtMjchgRWEYV4C7t1UrZRUEaPM7eXJg7ju44mD5xASuXSNat07tGzzeuUO0ZImaAM7VpEmVZTO8xMFkyZSFRZvg8tOeYxlhWTVgFalUiWj1aqL+/Yn27SMaNIho7FgVUoApUSIP7giGMQePHz+mR48eWf6PFSuWnDzKw4cqljy8eXLndv4+vBr2LnRcbxCag5AhrdKHNbgOI/FSK0engY5xaNoByy3i3hF2hLAiPREG4tatW3KUsgWjeRd58+aNtGbOmDFD/n/69Gm5jGPHjlnmefv2rbS4Tpo0yfJayZIlRd++fSO8rUayiFqP5u/BsuLah4SYPl2I996zta4kT64sOidOeHuzGbOzbJk6XnDcxIkjxPjxygLHOLYa798PN5AQFSsKESPGu5bNNGmEKF9eiM8+U56Kv/5Sn3nwwHN7FL/P4sVCFCgQut5EiYT46SfnlpUAgC2igYV2/7afBgwY4NkVwfMBj8XEic7n+esv5U2y0izvAE9L1KjKY/LmjfJiavduvGbP8+dCJE4sxJAhtq/v2CEEtNLBg0Js3ixE7dpq+3TWMYYSomfPnpUHw9GjR13+zKNHj0Ts2LHFMtwUhRBHjhyRyzh37pzNfOnSpZPuenDz5k05z+jRo0Xp0qVFihQpRPny5cU///zjdD0vXrwQDx8+tEwnTpwwjBCF6Ma2IFQhQpw+LUTPnkKkSmV7Y4Q7b/LkgL5BMU5c0J06hR4nBQvywMVdHj8WYuVKIRYuFALXu6dPfS+MEcaTJ0/o75g0qbpxBaBrmoVoYApR3Met7+u4zzsE98fwQmROnrT9DMRi1qxCtGnjfEM2bhQiblwlDsPjl1+UaIwWTX1m0CC13rlz350X4jR6dMQphr3MV6/UNkbCKOdXQhRWy1q1aomyiItygw4dOogsWbKI5xgByP36SmTIkEE0atRIWgdfvnwpBg8eLA+6qlWrynl27twp/0+SJImYOnWqOHDggOjWrZuIGTOmw1hSgJGSoxGUEYTo0qVL5bYUQZxYZMBBuWSJEHXrqoPdOqYMVprt29niFegcPixE3ryhx8ZXX2GUpvdWMREF1hXctHLkCP1NU6QQYuRINeAIEFiIBhZuezRv3VJCM6zp5cvQ+RGfnT27EM2bq0GfI2CRxL11wgT3PBpYNs5NeC1xviK23J5KlYSoX9+1ZTZsKESTJkJPDCNE27dvLzJmzOiWsBs0aJBInDixOIyboxX79u0TBQsWlAdatGjRZLJSjRo1RPXq1eX727dvl+/1RgCwFfnz5xe9evUynUX0119/ldvSoEEDzy302jUhBg+2vUFhypVLiGHDwh9p+dONGslw//0X2CIc333UKCFixVLHAazncBcx/sHr1ypUJ0sW21CBsWMDYqDBQjSw8GpoHSyhEKEQd7h/OGLTJiVCcX5FlH79hEif/t11/PuvEFGiqNCp8MBnc+ZUBoVAF6KdOnWSrvN/sQNdZNiwYSJhwoRi7969Tud58OCBjDsFJUqUEB07dpTPsR4chH/++afN/I0bNxaffPKJ6WJEu3btKrflm2++8Y4AQdYdwhrgDtBuUjD7Y8SFgx03MX8Do9h582xdl+nSCdG0qRC//YYYEOcjXX8Dgw4M4rT9gLiikPOK8TPgFUE8G25w2u+N6hp4De/5KSxEAwuv3b8hQrNlE6JyZfX8+vXQyd4dD0OY9ft3rapYIGQHAtE++x73HcSSorIG4swXLXp3G+BmxyDSkQj+/ntlQDh/XsWfQywj8/74cRGwQjQ4OFiK0DRp0jh1iTtiyJAhIigoSLrYXQHLjho1qlgTYsHBerFO+2SlQoUKvWMlNYMQrVOnjtyW3yCQvMnDh8qNUKLEu0kW2G/2JWnMCMTlggW25XcwcoXwto8JQjA4RBni6hAEbu2a8ReWLw9NSMIFC6WHAtkyHCjACorfGue2dryjBA3Kv/jhwJOFaGDhtfs3zg9nMaQaMOo4er9ChXeXY83776vSS7gOlyypYswd3b9gMEEZN0d066YGlkiOQmk4lI06cEDoja5CFPGdsGpu3rxZXL9+3TJZ1/Js3ry5jbsc8Z6I5fz7779tPvMYwf8hzJs3T2zatEmcP39eLF68WLr8P/roI5t1jxw5UopZ1N1EkhREKZKe7JOczCBE8+XLJ7dl9erVvlspEixgzk+W7N2T6Y8/fJ984alsYiTeWNdn/O47Ie7fVwkcGzao/zHatbYOaxMuEPj+GOBg0GPmJC/7hCRkWYeV1cn4J4i9R0iGdT1ThOvMmuXc7WhCWIgGFka6fzM6C1FHyT+YpmE0EEKFChUs2e4AojK8sguImYSrP0aMGDJxCSITSUuOYkwxHwraI3s+rKx5ox7IsO5i+7EtKF3lc7BfkX0L1y3iUqxFHArnw1JoZAsatg3hBUj0si48jvibsEphwU2J2NHhw1WIgr0gx4RyG1hu165C/P23eeJq4f6xTkjCKDokGZAJUDAQg2sQmfXacYGwFYSv+EGICgvRwMIo929GEQV/9K1kak6uXLlC6dOnp8uXL1M6rXe7Dty6dYtSpkxJUaJEoefPn3u+GK87XL6setmivSF67mqg7+4nnxA1axZ20V5fgsMeBb7RB3jvXvUamh2gJ3f37qpIuLvLQ3u3f/5R07ZttvtAA72/UWBYm1DU3Agt1rTvMGaMagGJAspoP4ffs3p1vbeMMQpoN4p2oWgr+OCBeq1gQdVe8P33Vftgs3H1Kj1du5bio6EH6n1PnEjxPvxQNRFg/BKj3L8ZBQtRkx/Iu3btotKlS8ttuYROSUYAHRs2bVIiZtEi1WdXo3BhJUrRq1uPmxbEFrrcoMPM7t3qtbhxibp0UTdTT9580HdcE6aY0HfYntSpicqVU6K0TBnVgzgi/cgjy82bRK1bE61apf6vVUsNKFKk8P22MMYHnWBGjlSTVWcaypSJqGzZ0AkdW6JFI8OA8x8DxK1b0dpPPZ4/T7hCxQ+Z5QnGpOiwV7o0UZ06asIA2igDRsZv7t+MgoWoyQ/kOXPm0CeffELly5eXLVMNB0To0qWqpSgskG/eqNdxUUdbMlhJGzTwfotB3IA2blQCdMeO0BZpnToRffONbwQX2jhu3x4qTNFq8fVr23nQxq1oUaKSJdVUqhRR+vTevQmuXKlE6K1bav2wdnXsyDdeJnzu3SMaNYpo2TKiI0fUINSahAmVoNOEaYkSyvPgK3DenzkTKjoxwXNjTdSo9LRAAYp/6JD890m+fBTPftAIzwXaH0OUli/PbZBNjlHu34yChajJD+Sff/6Z+vbtS61ataJp06aRoUGv7fnziWbNUoJMA73tYYGDKMWjfU/dyLJ5s3LB4yYEsPwOHZQLOlUq0o3nz4n27Al15cNCq7k7rcE2aqIUj8WKESVIEPn1o08x9gHc8SB/foxs9O87zJgTWEZ37VLnNiY8t/aGgOjRlVfE2moKr4CngBCGiLS2eGKAZb8NxYsTVaigRGWZMvQ0enSKH1/ZRJ88eULx7t4lWr5cCWwMYF+9Cv08zr1q1ZQorVmTXfgmxCj3b0bBQtTkB3KbNm1o6tSp9P3331N/WPvMAtxjED0QpcePh74eFKQspBClsJhGxq0HgQcBijABgPjZL74g6tXLszc/T95Ez55VN3CIUkyHDxO9fWs7H9yGEIvWVlO4Dt3ZV7hZIzxCs/wgNnbwYM8PApjABd4PHL+aMMV09eq788HaqIlShKngWMYx7uo6YMnURCfOeXgerMExjXMEohMTLLQIx7Hi6dOntkLU2mr75AnR+vVKlK5YocJYNDQXvmYtzZOHPQkmwCj3b0bBQtTkB3KlSpVo06ZN9Oeff9Knn35KpgOus6NHlSCF+x5xlRoQi02aKFFapIjrF3i43iFAcfPQLK5t2xL17m2+ZIpnz4gOHAgVphCp9q5FgJsorDya1RSTI2sv9vfYsSocgROSGF+CYw9x7LD+a8IU5759vizCdBAvbe3ORxgNgGUSyYWaxRPLgFC0BiISn4PohNUT50U4SZxhClH7wSJCaiBKMUFoW4PETAhSCFOsG9ces4PrBLxZ9hM8OrieInQIU5o0+sS3m/j+zShYiJr8QM6UKRNdvHiRtm/fTmVw8TYzuMjjJgVRChe+tWUjZ87QzPusWR1/HkINAnTNGvU/LorIhP32W6IMGchvuH49VJTiETdmexcoyJjR1mqK4xQhCYgJBXArIpyDE5IYPZOecBxr4hTHMwZf1uA8xkAUYhTzIqTEXrgi2U+zeML176YgclmI2gNhDSup5sKHaHPkwq9Rgyh5ctIdeFcQ12stKG/fdiw0tQmVElwB1mEYDzRhimuu/XPsA1et3QFw/2YULERNfCC/fv2aYseOTcHBwXTt2jVKbUR3c0TBBR2CEqIUyU7WNx8IK4jSjz9WJYZgoYAA1QQWYsBatSLq00dl8fo7uLmcOGHr0ke4g7PKbLAOISEJiVqcCcwYCSTvWbvzIVAx8LIGYkYTnbA65ssX6cz8CAtRa8Jy4eM807LwEX4AMYawApy7kZmcLQPXS8S52otMDO4jUrER+xcl7VBVBPsfj7iOINQCYhyeLPvES0fAQoz7pSORqk1IcPPydckI928mFBaiJj6Qz58/T9myZZNi9NmzZ7KWqN8mQaAMFFz3uNBrmbm4OCJWEtm62v8tWyoBirizQAb7DALd2nKKGyMSkrAfcfNmGKOjlVuCKIW4gss9Vy6PCxWPCFF3XPh6kzixEpPapIlLZxPEYViWTHxfJIVBlCJ0SJus/8eAwhURDEuytUiF0QH5An52/2ZCYSFq4gN53bp1VLVqVcqdOzedgEUsELhxg+ivv5SYQsY5wAWyeXOivn2JsmXTewuNCW4AcMklScJWUIbxthC1B0JMy8JH4wtcszBwdjbBqxPW++F9FpZHZwIT1wDM42sQ33vtmmORqj3HNcqeCROI2rXzu/s3E4oORyPjKf777z/5mCWQrH9IwEGGNyZkmMN1BytJjhx6b5mxgQXJ3W5RDMN4Blj3EJ+NydvAcoxEIlg9jQTEMUKlwgqXQqw73PzWIhUhDYxfw0LUxPz777+BJ0Tt22ViYhiGYRQYmKMs286dKsnLTMASjcRUTEzAoH/6GhNpi2hmlAxhGIZhAgckJaFGcufOttn6cIFjQpk2hjEBLERNTMBbRBmGYQIt1huWToQmIbaxUiWiceNCS9aBkSPVI2LpkbTIMAaHXfMmhi2iDMMwAQBiJWHhhLi8eDH0dcSBohOdddxl5cqqO9XJk6p7HbrJMYyBYYuoSXn48CHdRZ04ds0zDMP4H0g4sq5ROnSoEqHI7kcXPWTho4rIpElEBQrYJiZ+/rl6Pnmy77ebYdyEhajJraHJkiWjBKi7xjAMw5ib8+eJBg5UwhJNOTRg4ezenejvv1W9zj//JKpVy3kL0RYtVHcp1DI9dMhnm88wEYFd8yYlIEs3MQzD+BsoVzRvHtHcuapdrwZKGCHpSBObv/zi+jJRL/TDD9Vy0S65UCHPbzfDeAgWoiaFE5UYhmFMDuI3J04M/R+F7hHj2aSJEpLOLJ6u0Lu3ctFjeQxjYFiImhROVGIYhjEZW7YQFS5MFBSk/s+aVT2WL0/08cdEDRsSpUjhmXWxFZQxCRwjalLYIhqCK72LGYZh9ObIEaIaNYiWLg19rU0blREPgdqxo+dEqD1v3nhnuQzjAViImhS2iBLR1q1EiRIR/fij3j8HwzCMcx48IProI5UJjyL02gAabXe92esc64GLHus4c4Z/IcaQsBA1IcHBwZysBH7+WRVs7t+faNo0vX8WhmGYdwkOJmrZUmXEZ8yoyjChxJIvwHqOHiW6eZNo6lT+dRhDwkLUhFy/fp1evnxJ0aJFo/Tp01NAgqoBa9faBv3DvcUERmtDTAxjBoYMUe54JB6h/BKsoL5Eqyk6fTrR69e+XTfDuAALUSMC982zZ+G65TNkyEDRowdovhmKOIMqVYgaN1ZWh3//1XurGG+DuogpUxLVrcvxwYzxWb+eqG9f9RytOIsV8/02oN4ozhlYRVes8P36GSYcWIgaEbRyQ33QUaNsu2uEwIlKRJQqFVHatETt26uR/ubNRK1b6/FrMb4kTx4idBRbuZJo2TLe94xxwXHatKkaJH/2Wahl0tegsL1WHJ87LTEGhIWo0UBwOdw3GL1+9ZUq7wFh+vKlZRZOVCKiL78kunCBqH59ojhxiMqVC92H9++rQtCM/5wTuJmDuHFVdjFAbLD2OsMYjSRJiH76iahUKXUN1xNk54NVq1QBfYYxECxEjQaCy7dtU0WOM2RAQChRly5E2bIR/f67FFhsEQ0BYQnRotnuP2SGlixJ1KEDu279AQwokOjRp0/oa6iSgLa2hw8TLVqk59YxTNjXcsSub9+uBst6kj07UYUKauDGiZ2MwWAhakTgSmnbVomq335TLmiMYiGuunYNbCF68aISH87q4mHfIDsVGaLDhvl66xhP8vAhUc2aqq82fstTp0ItTfAWgAED2CrKGC8uFF4Z625JRqBrVzU1aKD3ljCMDQY5QxiHxIqlxOe5c0SjRytB2rmzxTWfLXnywCtUDGGOenywkjmienUVWwt69WKLmVnBwOu994g2bCCKF0/Fg+bKFfo+hChqyB4/rnppM4wROHSIqE4doqJFia5eJUOBlqG4NiLOmmEMBAtRMxA7tnLPX7hAL7JmpashF7i8U6YQ5c5NNHNmYJSzgZtWcys1auR8PuyrTp2Ua75ZM6L9+322iYyHOtAgrg71D5GUhsYF6EhjDURojx7q+erVvNsZ/YEVFNbGFy/UdTl1ar23iGFMAQtRMxE9Ol2Ea5qIUsSLR7E2blTW0ubNifLlI/rrL/92Uy5eTHT7trrAoyRJWGDkD+soqg7AQsEB+uZxayLxDIMt3Mx37SIqUsR5wtqaNVyom9EfXHdbtFChQZkyqXASo7jkrcHgHBVGcM/AtZRhDIABzxQmLLT40FRZs1IUxJAOHEiUOLGKn2vShKhAAaIFC/xTkE6YEJoBijja8BKZIMwh0JHwBdHCGJ9bt4geP1aJFUjyQCcaZwQFEVWt6rsuNQzjjEGDiJYvV+FUuP4ijtmI4Fz53/+UFw1imWEMAAtRk2FTugmZw+gjjNd++IEoYUIVM9ewIdGYMeRXnD1LBAswLqSu1uODUMHNAbFRmohljM0nnxAtWaIsnRhgucq9exyCwegDOrz16xcaw+7Mgm8UtOsnaopqPe8ZRkdYiJoMhxnzEKC4EEKQ4hFtP+Emsr5Jm/2Cg3JWALGCYVnJ7MG8CxcSIbGLMR5oOYjB1LVroa+haxIsS66ycydGZmoAxi0MGV+C6+q336pHVDpB4XqjA88Z6vGePKnOHYbRGRaiJiPM0k2wIMEyivJFmjUJF0jESCL5A1YmswrSEyfUI+ryRQaUdeLuIsYALniIzsGD1WNEE+4KFlR1GtHgAF22GMZXwEOD6yqqOKCyiRmAp+jjj21bJTOMjrAQNRkudVWyjp+EKD14kGjPHpW8g0QQlMQxmyBFj2R8D9SVjChw7SO+FG1BkRTD6AfidhEHiox3iEh0SbJvTuAqsO7AqgrQycaqCxnDeJ2kSYlGjFDVTcyC5p6fN0/V62U8AwbDuMfg/ozrGjojotaxdae/06eJ3n+fKGVKdcxkyULUt2/43pxLl1SSLq53KVIQffPNu+UbkYiG0BB4lNAEx9HAfNw4lVCHdaP5C7SBzrAQNRFCCPeL2eNghHjFiB0H3o4dRFWqqBMhJAPfNBQqpJKQIgq+86efKssb3LhwTTH6WLdhocfAAiETuHjCIhoZ2rUjSpNGXaxR1oxhvAkSfczsWSldWlWlePaMaO5cvbfGf0DSMBKFkZOAfI2RI1VHRIRvWBuKEDqH2GKI0lGjlGUagtUZuGdBhELQ4h4+Y4YSmRjAa+A+j3lwn0M9227d1IADFnsNJPB2767WdeCA8iZVq6aSRPVE6MjAgQNFsWLFRPz48UXy5MlFvXr1xKlTp8L8zMSJE0W5cuVEokSJ5FS5cmWxe/dum3lu3LghWrZsKVKnTi3ixIkjqlWrJs6cOeNwecHBwaJ69eowD4pFixa5vO2XL1+Wn8Gjr7h7965cJ6Znz565v4CrV4Xo0kWImDFhDxWicmXsAGFobt0S4v59zy3vxQshypZV3z9LFiFu3/bcspnw2bRJiIQJ1f7Pnl2Ic+c8t9fGjlXLTZNGiOfP+ddgXObJkyeWayueh8mBA0LEjq2OtaVLzbuXf/lFiGzZhPjzTxFo+PT+PXSoEJkzhz3PV18JUa6c8/dXrhQialSIm9DXxo8XIihIiJcv1f//+58QefPafu7jj4WoVi30/xIlhOjUKfT/t2/V9XLQIKEnulpEt2zZQp06daJdu3bRunXr6PXr11S1alV6+vSp089s3ryZmjZtSps2baKdO3dS+vTp5We0Iu+wGtavX19aDpcsWUIHDx6kjBkzUpUqVRwud9SoURTFJOVfNGto6tSpKU5EehfDYoQ4JvTojhmT6MED47tlUJ4K2z12rGeWB5cFWoTCooz9Wb8+u3J9BSwFKEKPY65MGTWyh+vKU2D0j0Q9JD5pyW0M462i9bVrh1/P2Mh07qzaSMNLFKA8fvyYHj16ZJleeiOsB9e7sMp5nTunQpQQquQMJJXlz6/c+RqwZD56pCyv2jzwdlqDebSENFhT0dzFeh7UusX/eietCQNx69YtOUrZsmWLy5958+aNSJAggZgxY4b8//Tp03IZx44ds8zz9u1baXGdNGmSzWcPHjwo0qZNK65fvx6uRfTFixfi4cOHlunEiRM+t4j+9ddfcp1lYdGLLBjVYzRkZGDVSpxYWR5WrPDssk+cCLXMffqp8S3D/sLFi0K0by9ERCz6rjBhghDRognRq5d3ls8ErkUU18uaNUO9Kffu+XozGQ9bRO2nAQMGeHYfnz2rrJYTJ777XunSQsSKpY6ndu3Cvh+3bStE1aq2rz19qj4LaymAh2ngQNt5cN/EPLjewiOK5zt22M7zzTfKUqojhooRfRhinUviRjHgZ8+eSUuq9hltRBPbKnA8atSoFCtWLNq2bZvN5z755BMaN24cpUIbwXAYNGgQJUyY0DLl0aFfr0uJSq5SuLAxO39Y8/ffygKRIYMa2XkSxEdh+Yg5zZHDs8tmQkEw/aZNof/jtxw/XgXye4NWrVTcFQqMM4wnQSLcypUq1h5F692pc2tkYN2dM0d1oQswTpw4IXWHNvXWkh7t6dVLVUgIa0J8qDXw0iJBGO2oUdrLHsRrIk5z9myVjDt8OAUqhlEiwcHB1K1bNypbtizlQzccF+nZsyelSZNGut5Brly5KEOGDPKAun//Pr169YqGDBlCV65coevI1A3hq6++ojJlylC9evVcWg+WZ33A4gD2NW4nKrnCkydEw4api5HR0IrQw+Ua0YzqsMAxA9cUaq+aJDzDVCAU5qOPiCpXVrVcfQFCTjzp7mcYANfpd9+pfYHkEyRO+gtly6pGEhDXAUaCBAkoKCjIMsFg5RCEFCG5NazJ+r6M8CAkDSEEyVmYEMKIYNBq2lSVsMPx5ayEHYxlN2/avqb9rxnSnM2Dcl0Y+CdLpu6jjuZxwRjnTSKRguxZECt67NgxG6tleAwePJjmzp0r40Y1C2iMGDFo4cKF1KZNG2kljRYtmhSpNWrUkPGjYOnSpbRx40YZP+oqOECtD1LEk5jaIgqwPypVItq7V/2PchBGAXEvOBZw4qAchrew3pcQTii/kTev99YXKODihvq1OLb0Kmtz7JgqX+biYJNhXKpj3LKlf+0oxMnDMocqAAEcLxomqO7halMUWEIhQosWJZo2zTXPY3CwKt+ER0dGF1Q5+Plnld2O0k1g3TolMjXvLOaBxd4azIPXtUE6tgnlG/Gba+vF/4gX1hNhADp16iTSpUsn/v33X5c/M2zYMJEwYUKxd+9ep/M8ePBAxp2CEiVKiI4dO8rnXbt2FVGiRBHRokWzTNgVUaNGFRUqVDBs1nzWrFndjqENl+nTVdwI4iXv3BGG4csv1XbVr++b9SEbsUgRIZInF+K//3yzTn8FlS+QJYrfL2lSIbZv9/024ByJEkWIJEmEePjQ9+tn/C9GFMcUqm74G7iHISMb5+vp0yIQ8Nr9+8oVVYkAFWnw/Pr10Elj5kwkfKg8hfPn1XNkrjdrFjrPwoVC5MwZ+v+bN0Lky6fiRA8dEmL1anWv6t07dB7op7hxVcznyZNCjBun4uUxr8bcuSouFfd9rB+xqYkS2Wbj64CuQhSlkyBC06RJ47S8kiOGDBkigoKCxM6dO12aH8uGyFyzZo38H8lJR48etZlwUP76668ui2FfC1EkZUWPHt3z68QBXqCAugh17y4MwevX6iTDNq1a5Zt1Pn4sRKFCap0ogfHggW/W629s26bEn5bQodeNDcc1LuTYjh9/1GcbGPMLUa00jr9Tq5Y6V1ACKADw2v172jS1Hx1N1mIQRo/48YWIF0+IPHlUkpF1yTltOdZcuCBEjRpCxIkjRLJkQvTooe6V9uXxcB9DiUZcf7Ece8aMESJDBjUPkpR27RJ6o6sQ7dChg7Rqbt68WYpDbbKukdm8eXPRyyoDdvDgwSJmzJji77//tvnMYwiJEObNmyc2bdokzp8/LxYvXiwyZswoPvroozC3xeh1RC9cuCDXh++OKgAeBWIPBz0OTKNYA5Hhhzp3vszsx2+ZOrXaF6i9Zn+SM2Fz5EhofUVc4G7e1HePzZmjtgUjfk/WomV8C8Tg4sVevWE6FKKTJ6ubOqxW/g7ufThXUqQQ4tUrfbcFFUxQn9WLlUz08GgyztE1WWn8+PEy8adixYqyNqY2/YVsshAuXbpkk2SEzyABqWHDhjafGW6VcYb5mzdvLhOXvvzyS/l8DrICTYyWqJQpUyZZBcCjICMdCSWoM4ZWY0YAtUPRAcKXmf3p0hEtW6ZaqKEbBTpTMK6D6gOIM0YbVrRT1WKZ9ALZqoj3Rb1cdC9hzAXqHeMcTJtWxbRZd4JDW0IkMSJxaN8+z9cCRr3FTp1Uh5pASOJBPVTUqEQM4vLl+m7Lr7+qTmuIVzVbK2omYoQhUhkDjaimTJki14cuUV5h//5QFwKe64URapsiPgfxhdgXo0frvTXmw0g1WefPV78javndvav31jDhgTh1nHOFC9u6NeGpsA7fGjLE9v0YMYQoWlSIL75QNRvdtMbbWERR6zZTJrXcOnWMcU3yBT176u+e1zoIYTvgEfMSbBE1FoYp38ToULrJmiJF1AgUpSSSJtXv5/jqK6IPPlBdd/Tiww9VOQ2AR5S4YpwDjwUyPjWMVAoL5aPQTxlVLn75Re+tYcLi8mXlCfnySyJUNEFP7oYNVY3FS5eIsmcPnRddaNC/u2pV1bUGxx+smCj51q6dWpYGqm8ge/nIEVXXNjxQpQPVM1AG7I8/jF9v2VNgv6MW5pAh+qwfJZCaNFGZ3J99pu4FTEBgmPJNjI9LNzli+nTv1Ot0lWfPiGbMUC3R9C4lhfUjVKF5c6L48fXdFiODGzvKNOHmMXeu8ZoDQER8/71y40LkMMYBNXwhOD/+OLSuImpI41hq3VrVtkTtQ0eULKkmAPsZhCOEKNz0KEVkXYsaYnLSJPUc9RRRA7RYsdApVy7bZa9fr+ZD7dtEiShg0PP8uHtXXUcwYHzvPdX0wkgDWsarsBA1CV63iAI9RShAbDBEKL6jfc9cX4OLoFHiZY0MmiFAAKDLTIIEZEgQb4aBHA8o9AdCY948ZaGE1wM1ZtF9JmFC9T5qGror/nCuYoCOCRZUeyBKK1ZUx+njx6qvtnVv7Xv3VI1Fa2BZLVCAApbbt5Wl2Rf3BAz48buh5m+mTCom1/73YPyaAPE5+I9F1KtCNHRlyhrx55+kSycltEMzmjtsyRKijh05eN6+yLfWbQYJBqlTkyGBUGERqh+wcKLNa4sWqoMLzm+IUIgcDDhhDdPwhgUSLmesH0lrcD3PnKmSoMqVU9ZR+1ad2D54QgIVFO1HghgGBb4AiWfbt6tzFMmirhaOZ/wGtoiagKdPn9LNkLZcXnXNa8yfr3oPI7YKI1Vv9QW3z5DdvVv1fodbzkggWxf7AW5oxNLCzRvoYF/gd4I1Axm3ZujIAhfu4sUq3rBrV723JnAYO9Z2f8MVjmMHYs+XgxcMbnPmVFOzZuo1R1nZesVIGgVYIxFzi05LiMH1NhgQQPQiFt+N9t6M/2AwsxMTljU0ceLElMgXMUtduqh4LQT8jxnjW2soEoVQRsRIZMyo2qtp+watIwOdkSOVJQMuVfx2Zojn+ucflbzUq5fqBc14J8571ixlgdTAIA5WRyQRwSUOS/r//mcMC7qj4zbQ3cLaQBuDNrjovYX1IABxoTVqeG9djKFhIWoCfJKoZA0soD/9pJ4PHGjrOvMGGAnDXaa5hYzI11+reqsvXqjkCvSlD1Tg3uzXTz0fMUK58cwAbnZly6rfUKuKwHhGUOzapc5diEtYxwcNsk2CgUcHA5ZSpcwxaAlkUGUCSVywiiLRy1vXEPQ950E9w0LUHPgkUckeuK4QrI/kIYhRb4IyLaNHEzVuTPT++2RI4NbDRRk3Wlh0EHcWqCB8AjcRCHOjhVGEBQTQDz+o5xBF1iV+fAkGMQhp8JcQDSQblS5NNHGiSkZCwglEv7XFC+c4Yz6rKNzzni4qj+QwZMijYgIs40zAwxZRE+DTRCUNJBIMHRoa44XyKN4iViyiVq1U1rzRkpSsQaeg2bPVNk6dGmrFDTSyZSPaulWVazKbdQsDHdSghBD09gDLEQhnKFw4VBDjJr95s3mT4FCbde1alf2OmE901EL2MyzmZjs2mFBQTxod5mC59GRNZ1hZ0fHs3DkV8oSSgUzAY+C7PmNvEfWZa14Dgepa689AD+DXQBmY/v1DE6wCCeti4BiomLHGorVVdMoU7w6w7Pcd6pmWKUN09qwaxMAyihs+xDEGOGYDSV9a1QS02oTHAN/FyINJxjWCgkLru+I88QQYbMGThMGKliGvdxtgxhDwFcME6OKa127asIoOGBBqHfU0iCVDH/D798k0oL7ounWqhmYgleBBqZ0ePVRCipkpX159F1hntFhobwLhicxgiLa3b1X3GLgl48ULrVWJckJ37pCpQELjb78pwYLSTIx/0bmzigH31LV/3Dg1YMF9BQOv/Pk9s1zG9ERBn0+9N8KMXLlyhdKnT0+XL1+mdOnSeW09+Hnix49Pz549ozNnzlB26zZ3ZgfFpZHIgGQlZNnC2mhGcAr5uxsSMbwowQPxhAQDxAGaGbgbUQEB1lGUn/LWcYGOPmhVCPGOCgMQbqjRqwExjHjbo0eVa9tbySFMuCXycJ0FT548oXg4zhnPges7WjdjIAbvms6xob66fzOuwRZRg3Pr1i0pQqNEiUIZEVOjt1Xsxg3PLQ+1SiFCUdcPcXtm7bMO6xpqr/oriPnr3Vs9h3XE7CIUwEWOdpDeEqEAJaK6d1ciFC5riE1rEaol8SAhBAMZNJBYs4YMz61bKomRYdzJxMc1HpZzvds3M4aDhahJEpUweoupZ32706dVb2dkSmNU68naoagvaFaLIkQEYp6QZRoSQuFXYPCB7wYxBYt1+/bkN3j7mENZK9ThRUIP+pfDle2IEiVCqzBg/xq5NBisvOg8lDcv0ZYtem8N4wtQFxahJQgniShoF7p6taqsYNZrPeM1WIgaHN0SlexB2zVkOh454plscVijDhxQGfMtW5JpgaUQtSlRtgaxcv5SlkcDMV3I6kYGLZIW/C0RBRZ5xCkjHs4ToSYQadhfGihvBatoePsNsaoZMqjkKS0ZzojA8r90qbKKJk2q99YwvgC/N1pwupu0hLATFMW3tv7jes8wdvjZXcX/0KV0k7MR7bffqucozfL8uWesoei6YuYbGmpqIsQA+wfiGl17/AWIIi2WCwXg9T4GvZVIhOMasZuoDxtR0DEIfcthIddan7oD4hMh+rGPjdphBo0tEFerDcC4HWNg1RSFAcKd6z4S8NApj2uFMuHAQtTgGMYi6snWn4gv08rVGLWTkjtgn2j18ND6EhYEf+DkSVWmCcXJO3UivwQ1PXGzhMsZ5ZXcBVYfDMzgusS5CqvmtGkRaxMJAYp9jphjI4KKCbCE5skTOihl/B+U8EN+Aq7bCxa49hkM7DDBDQ+PEcOEAQtRg6Nb6SZHoGi1J1p/4oJWs6YKYMcN3B9ApxBkRwMU50eNRbMDYXT8uMrk9jeXvDVaLcx581RCkTtx00h6wjmBWFq0tkToSmSqP1gLWLQiNQooWj9jhhIWsPqyizVwwLnfpo16jioQ4YF4aC3mGfeJevW8u32M6fHju4t/4PM+8660/oSAjEzrT1iNEGu2d69/Ba7DfY0ezSjSbOSEE3dAaRN/yJIPC9TyRLcXgJq5rnDmjLKmIhwjcWLVFQxZ7yjRFFkgatHNDFYoxGUbIY5W81zAK4J2nkxggXATCFJ0VMOx7wy8h3MJCa0oR9azpy+3kjEpLEQNzOvXr2WdM8NYRAFctVqXJcTFRSaD3t/6T8OaheB8iJPcucmUwEUN68eqVRRQwCqKQdGiRSqJLjxQzxcWY7jRYUVt3Nhz24LtwHEENzgEoN6lnhF+AMsvhPHPP+u7LYx+A1ItdtlZ0hKaksAz9OABUalSnCHPuAwLUQNz6dIlCg4Opjhx4lDKlCnJMKD1J1x127YpYeoOsBzBpemvoGRPSGFsidm6ECHWdepUoo8+8mzNWKODuEe02wTOYkWXLCG6dy9ULCJkAXU/8Zt7EiwbyXxx4qjSYHr344bFFyV8INCtj20msEBFCAy8nIVTwSWP5D/EzGMghVAuhnEBFqImSVRCQXvDgG1Blwx34wYxUoaLJ1cuFUvnz8C9ivqROXKoovdm4OrV0DhXdBxKlYoCCpRNQvtNhFhYg9JcOG7r11d1PjULJbrveCt2NmvWUEGMJKGbN0mXY9gaVIZgAhfEeqK1MayejoBLHgIUyZpGMpwwhoeFqIExTOmm8OLHYLFxxX2olf9AMWx/7zOM8j2wmEHcIa7WU00AvAV+P7iBEfuLAuuofRlooMMXSnFZh1X884+KicYxjgFYtmzvCjRvgUEB4lDh8kQpHF+DJCxYxs0ykGL0wfp8qFtXlTFjGDdgIWpgDFW6yREvX6pagrAWhdeaEEJHqx0KwWMkC683gFsKWdiwmqHPstFj6zBIWLFCxbmi/JC7IRf+OJBA/UO0JUQ9VSRsIVEDCXq+2jeoUYsMdaxv7lz1+/gK1FSFEEXMLHdQYuxb1+I8QP4Crm3Fi6tzhGEiCAtRA2Oo0k2OQAkXFKQHuGmHZfVDYtOxYyruDdmUgWJhQ5FyADerdccdIwGLV9euoUk7iJcMZK5cUVbQYcPUAArluA4f1qfUWJEiyjIKMXrqlG/WifMYRcyRpFS7tuoYxjAauH736aNCWRo0ULHDOFcYJoKwEDUwpnDNo7B1okQqczis1p+aNRQ3NcwfKKC2JCzGcF998gnR7dtkOJBABvdv0aJE33yj99boz5s3KiYTHb9QwBsW4qAg/bYHg5j9+1WsqC9AIXIMHBMkIBo/3v+9F4x7fPaZekS4Cq4bCOUZPpz3IhNhWIgaGMO75u1bf/bt67gFHC5WcFP7Sycld0EXKsQdwvIIa4KvYgxdBfGHCxcqwQV3cKADNzzqd8L1iBhJvYkbV1lofcHFi6p9J0CZNpTtYRhrcE5oxgQcH0hQgqeLYSIIC1GD8vDhQ7oXUirG0EJUK3KNIvVwaY4e/e77mkseCUolS1LAgThRCHEUO69e3ZgWJrS59PcEMncHWEa8ucLzgFABd3vZu5OwhmYMaOsaiINGJnxwXqCqBhpBIEM+dWrea0ykYCFqcLd88uTJKb7Ra/dZt/4cNOjd1p+4qSHA/e+/jSnCfAGSuhDQD+ujEfYBRMeIEfqUBWIiBsQnBjJotemNmDzUjUUcKmK/0crRn9u6MpE3PiBuGlUdGCaS8JXGoBg+UckelChC2Q5033BkrYFYRU3NQMY6NvbxY1VXVS/QYhUxh/jN/KUdqb+DigaaAP3xR883hoBlC96L5ctVoh3DMIwPYCFqUAzXYz48YD3Zvl3VYbR21Zw8qX+LQqOBYv7oSQ8Xqx77BglTnTqp5+3aqdABxhyg+xOsoiidht/O0/HG8L6gew7DMIyPYCFqUExnEdWSKqyBix6uG1hXjJgtrmdWNtz0aBmJRCZf07kz0Z07KiYUZVgY84CwDmSy41xDXVNnfb/dYdUqtUyjJdExDBMQsBA1KKYo3eQMbDvKFqH+ISw3sLglS6b3VhkH1IZE+0/w9ddE+/b5bt3IjkfiFOpSIkse7l7GfFn9WoMElNuKTOcjtC+FZbVjR1W2iWEYxsewEDUopijd5AxkVM6aRfTnn4HTScld4BpHpjqKhqO2KlprehtYqDt0UM979lR1QxnzJougow2Om19/jfhyevVS1S7Q216rD8kwDONDWIgakODgYLoQ0jLNlBZRdOfRLG2whqKQO2MLhDncqhkzYtShrFIRjRd98UIJErjbUZ0AtSBRBxPxuSj3Yz1AuHVLdU5CVxTGvMCijcx2VD6IaPvYf/5RLnmAZdmH1jAMw/gArl5tQK5fv04vX76kaNGiUTozFpSGuEKZoqFDiVq21LcrjZFJnFj1EEd5K9Tjg4CE2xW0aaO626ACAaym1lOMGLYxtygwjTg/ZyD2D8IXJbYQn4rfBCV6GHODIvcRLXSPwQvaeAI8vv++RzeNYRjGFBbRQYMGUfHixSlBggSUIkUKql+/Pp0OpyTJpEmT6L333qPEiRPLqUqVKrRnzx6beW7evEmtWrWiNGnSUNy4cal69ep09uxZy/soFN+lSxfKmTMnxYkThzJkyEBffvmlLCJvJLd8xowZKbpZO90MHEi0enVoLCTjmFKlVO1VlLfSRCiAKIVF8/x5okuXVBwgLJ44Ru3LPkGYWlvKsCy0Z0RR9pQplfgEeG3cONWSj/Ev0NEMyW+ugvJPZ86oChfcJ5xhPAM8mTAiIKQOhf8R8jJggG1JQ2gcDPxwbca1OksW1ZUQRoawwH2gVi3luUiRQsWHa9d2Lf7/gw9QfFwZf0qXJlqz5l1vJYwS1lOuXLr/+rqqnC1btlCnTp2kGH3z5g19++23VLVqVTpx4gTFc1JSZvPmzdS0aVMqU6YMxY4dm4YMGSI/c/z4cUqbNi0JIaSgjREjBi1ZsoSCgoJoxIgRUrBqy7127Zqchg8fTnny5KGLFy9S+/bt5Wt/o+i6zpg6UclaEFWrpvdWmAPU87SP14Q4gOhEiAOEpv1k3yseFxS8zkXIA48nT1QCHAbbcLeXKxf2/AjP0AaIGJhY17dlGCbioCEEPFATJhBly6bq8rZtq2o1Dx+u5sF1ukULdc7i3Dt8WM2Dz8GA44i3b5UITZWKaMcOZZjAMrAs7TOoogEhiv+xXCSj1qlDtHu3beOBvHmJ1q8P/d8Ixi5hIG7duoUgObFlyxaXP/PmzRuRIEECMWPGDPn/6dOn5TKOHTtmmeft27ciefLkYtKkSU6XM2/ePBEzZkzx+vVrh++/ePFCPHz40DKdOHFCrufy5cvC0wwYMEAuu23bth5fNsMwfkibNogwFiJXLlyswp9/714hevXyxZaZgidPnshrLiY8Z/wb3LfxW+M+bn1fx33e4wwdKkTmzGHP89VXQpQr5/z9lSuFiBpViBs3Ql8bP16IoCAhXr50/rk8eYT4/vvQ/wcMEKJgQWE0DJWspLnGk8Cl6CLPnj2j169fWz6D2EoAa6lG1KhRKVasWLRt27Yw1w3rqTNXOMIIEiZMaJlgSfUWfmERZRjGd8CCDlcfLDLOrCrWoKECQkIYJoDBfdz6vo77vMeBrglL05w7p8LYKlRwPg/yBVD3Gee4BjyOKL92/Ljjz8DCig5+9uuG5yRNGhUSgI6IcPnrTFQjZYp369aNypYtS/nQl9tFevbsKWNB4XoHuXLlkjGfvXv3pvv379OrV6+k+/7KlSsyCcgRd+7coR9//JHaIXPZCVgexKo2wc3vLUxduolhGH0S30aPVs9xM3V0c8IA14vXLYYxG7iPW9/XcZ/3KBCZaFqCEob2lCmjYkSzZ1cJq6hq4owbN2xFKND+x3uOQCgAwnYaNw59rWRJounTlfBFxQxcE7BuCFYdMYwQRazosWPHaC6yiF1k8ODBcv5FixZZLKCIDV24cCGdOXNGWkmRrLRp0yaqUaOGtIza8+jRI6pVq5YcGX2HQF4nwKIKi6k2IcHKW7BFlGEYt2nUSMWEIelBiznTgOMeSRSFChHNns07l2Fk/mYCm/s67vNO6+3aJ/nYT/BGWHP1qmrHi/MS56M9iO0/cECdjytWhMaQegIs8/vvVfMSJDZp1KihtqdAAWVRXblSJb9iPh0xQJQqOg52puXLl9PWrVtdLleERCMI0fXr11MB7FQrihYtSocOHZIjHFhEkydPTiVLlqRicEdZ8fjxY5lRj4MRYhYiVm9evHhBV3EAs2ueYRh3wM0QyUebNilXHiweaJwApk5VryOTF5UaGIZxL6G0Vauw57EOpUM9Z2TGw+o5caLj+dOnV4958qhkJHhksR4k+tqDJCW76kB082boe9bAmIeSbPPnE4V4ip2CpKYcOZTlNlCFKDLcUUYJIhDZ8K66oocOHUo///wzrVmz5h1xaQ1iPgBKN+3bt0+6360todWqVZMjoKVLl9rElOqJVsge4tidWFmGYRh5cxs8WFlYatdWOwQhSbjBAVwDOfacYdwDJZEwuQIMSRChqISCzHVXKpkEBytPBh4dCVGUYkLjClS80Cyc69apMk3W+Spz5qgOaRCjyLIPD7juUSKweXMKWCEKd/zs2bNlmSUIrxshsQ4QkKjvCVq0aCHLMmlBxIj37N+/v/xcpkyZLJ+JHz++nMD8+fOlFRSxokePHqWuXbvKkk4o86SJUDxHotPMmTPl/5gAPodC8nph7ZaPwm0xGYZxF7RxRe947frRubNKmMCgvWtX3p8M4y0gQitWVE1d4Gq3bjyiWS7R/hreVyQfIRRg3z4koahWz5pXdtEi9Zrm7od2geCEYESjGOge1B6Fx0MLJ4A7Hs1K0PIXsaBa7Ci0VIhRjr7+WoXvYPtgtUWNU+idpk31PSb0TNnXymXYT9OmTbPMU6FCBdGyZUvL/xkzZnT4GZQ80vj1119FunTpRIwYMUSGDBlE3759xUurEgebNm1yuu7//vvPrfIPni7fNHbsWLnc+vXre3S5DMMEIAsWqLJO0aMLcfiw3ltjWLh8U2Dhrfu3gHZREdnvThpz5wpRpIgQ8eMLES+eKrE0cKAQz5+/uxxrLlwQokYNIeLEESJZMiF69BDCutxkhQqO12uln8THHwuROrUQMWMKkTat+v/cOaE3UfBHXylsTpCFnz59erp8+bJH23B+/fXX9Msvv1D37t3lI8MwTIRAJiysKFeuEPXpo1q8Mg55+vSpxaP25MkTpw1VGP/AW/dvxsTJSkwoz58/p5gxY3LpJoZhIge6cqF9X86cyo3HMAxjQFiIGoxx48bRmDFjZMtThmGYCIPYMSQ0MAzDGBjD1BFlQkG9U1hFGYZhGIZh/BkWogzDMAzDMIwusBBlGIZhGIZhdIGFKMMwDMMwDKMLLEQZhmEYhmEYXWAhyjAMwzAMw+gCC1GGYRiGYRhGF1iIMgzDMAzDMLrAQpRhGIZhGIbRBRaiDMMwDMMwjC6wEGUYhmEYhmF0gYUowzAMwzAMowssRBmGYRiGYRhdYCHKMAzDMAzD6EJ0fVZrfoKDg+Xj9evX9d4UhmEYJhI8e/bM8vzq1asUN25c3p9+jHbf1u7jjL6wEI0gN2/elI8lSpTw5O/BMAzD6EjOnDl5/wfQfTxDhgx6b0bAE0UIIQJ+L0SAN2/e0MGDByllypQUNar/Rzg8fvyY8uTJQydOnKAECRJQIBGo3z1Qvzfg7x54vzv/5oHzm8MSChFauHBhih6d7XF6w0KUcYlHjx5RwoQJ6eHDhxQUFBRQey1Qv3ugfm/A3z3wfnf+zQPvN2eMgf+b8hiGYRiGYRhDwkKUYRiGYRiG0QUWooxLxIoViwYMGCAfA41A/e6B+r0Bf/fA+935Nw+835wxBhwjyjAMwzAMw+gCW0QZhmEYhmEYXWAhyjAMwzAMw+gCC1GGYRiGYRhGF1iIMgzDMAzDMLrAQpShQYMGUfHixWVHjRQpUlD9+vXp9OnTYe6Z6dOnU5QoUWym2LFjm25vfvfdd+98j1y5coX5mfnz58t58H3z589PK1euJLORKVOmd743pk6dOvnd771161aqU6cOpUmTRm734sWLbd5Hc7n+/ftT6tSpKU6cOFSlShU6e/ZsuMsdN26c3I/YDyVLlqQ9e/aQmb7769evqWfPnvIYjhcvnpynRYsWdO3aNY+fM0b7zVu1avXOd6hevbrf/+bA0XmPadiwYab+zRnzwkKUoS1btkgBsmvXLlq3bp28QVWtWpWePn0a5t5BB47r169bposXL5pyb+bNm9fme2zbts3pvDt27KCmTZtSmzZtZItXiHZMx44dIzOxd+9em++M3x00atTI735vHMcFCxaUIsIRQ4cOpdGjR9Pvv/9Ou3fvlqKsWrVq9OLFC6fL/Ouvv6h79+6yvNWBAwfk8vGZW7dukVm++7Nnz+S29+vXTz4uXLhQDkDr1q3r0XPGiL85gPC0/g5z5swJc5n+8JsD6++MaerUqVJYNmjQwNS/OWNi0GueYay5deuWwKGxZcsWpztm2rRpImHChKbfcQMGDBAFCxZ0ef7GjRuLWrVq2bxWsmRJ8cUXXwgz07VrV5E1a1YRHBzs1783jutFixZZ/sf3TZUqlRg2bJjltQcPHohYsWKJOXPmOF1OiRIlRKdOnSz/v337VqRJk0YMGjRImOW7O2LPnj1yvosXL3rsnDHi927ZsqWoV6+eW8vx198c+6FSpUphzmO235wxF2wRZd4B/YZBkiRJwtw7T548oYwZM1L69OmpXr16dPz4cVPuTbhh4cbKkiULNWvWjC5duuR03p07d0rXrTWwiuB1s/Lq1SuaOXMmffbZZ9Iy4u+/tzX//fcf3bhxw+Y3TZgwoXS7OvtNsb/2799v85moUaPK/818HGjnPo6BRIkSeeycMSqbN2+WoUg5c+akDh060N27d53O66+/+c2bN2nFihXSwxMe/vCbM8aEhShjQ3BwMHXr1o3Kli1L+fLlc7p3cPGGS2fJkiVSxOBzZcqUoStXrphqj0JwIP5x9erVNH78eClM3nvvPXr8+LHD+SFaUqZMafMa/sfrZgUxZA8ePJBxc/7+e9uj/W7u/KZ37tyht2/f+t1xgFAExIwi9ARhGJ46Z4wI3PJ//PEHbdiwgYYMGSLDk2rUqCF/10D6zWfMmCFzAz766KMw5/OH35wxLtH13gDGWCBWFPGO4cX/lC5dWk4aECW5c+emCRMm0I8//khmATcfjQIFCsgLLqx+8+bNc8lK4A9MmTJF7gdYO/z992Ycg7jwxo0by8QtCA1/P2eaNGlieY5kLXyPrFmzSitp5cqVKVDA4BLWzfASD/3hN2eMC1tEGQudO3em5cuX06ZNmyhdunRu7ZkYMWJQ4cKF6dy5c6beo3BJ5siRw+n3SJUqlXRnWYP/8boZQcLR+vXr6fPPPw/I31v73dz5TZMlS0bRokXzm+NAE6E4FpC0FpY1NCLnjBmAuxm/q7Pv4G+/Ofjnn39kcpq7576//OaMcWAhykgrCETookWLaOPGjZQ5c2a39wrcVkePHpUlcMwM4iDPnz/v9HvAKgh3njW4eVtbC83EtGnTZJxcrVq1AvL3xrEOIWH9mz569Ehmzzv7TWPGjElFixa1+QxCFfC/2Y4DTYQi/g8DkqRJk3r8nDEDCDFBjKiz7+BPv7m1JwTfCRn2gfibMwZC72wpRn86dOggM6I3b94srl+/bpmePXtmmad58+aiV69elv+///57sWbNGnH+/Hmxf/9+0aRJExE7dmxx/PhxYSZ69Oghv/d///0ntm/fLqpUqSKSJUsmKwc4+t6YJ3r06GL48OHi5MmTMps0RowY4ujRo8JsIOs3Q4YMomfPnu+850+/9+PHj8XBgwflhEveiBEj5HMtM3zw4MEiUaJEYsmSJeLIkSMyizhz5szi+fPnlmUgq3jMmDGW/+fOnSsz66dPny5OnDgh2rVrJ5dx48YNYZbv/urVK1G3bl2RLl06cejQIZtz/+XLl06/e3jnjNG/N977+uuvxc6dO+V3WL9+vShSpIjInj27ePHihV//5hoPHz4UcePGFePHj3e4DDP+5ox5YSHKyIuVowklezQqVKggS55odOvWTYqYmDFjipQpU4qaNWuKAwcOmG5vfvzxxyJ16tTye6RNm1b+f+7cOaffG8ybN0/kyJFDfiZv3rxixYoVwoxAWOJ3Pn369Dvv+dPvvWnTJofHt/b9UMKpX79+8ntBaFSuXPmdfZIxY0Y56LAGN2ptn6C0z65du4SZvjtEhbNzH59z9t3DO2eM/r0xwK5atapInjy5HETi+7Vt2/YdQemPv7nGhAkTRJw4cWSpMkeY8TdnzEsU/NHbKsswDMMwDMMEHhwjyjAMwzAMw+gCC1GGYRiGYRhGF1iIMgzDMAzDMLrAQpRhGIZhGIbRBRaiDMMwDMMwjC6wEGUYhmEYhmF0gYUowzAMwzAMowssRBmGYRiGYRhdYCHKMAzjIaJEiUKLFy/m/ckwDOMiLEQZhvELWrVqJYWg/VS9enW9N41hGIZxQnRnbzAMw5gNiM5p06bZvBYrVizdtodhGIYJG7aIMgzjN0B0pkqVymZKnDixfA/W0fHjx1ONGjUoTpw4lCVLFvr7779tPn/06FGqVKmSfD9p0qTUrl07evLkic08U6dOpbx588p1pU6dmjp37mzz/p07d+jDDz+kuHHjUvbs2Wnp0qU++OYMwzDmhIUowzABQ79+/ahBgwZ0+PBhatasGTVp0oROnjwp33v69ClVq1ZNCte9e/fS/Pnzaf369TZCE0K2U6dOUqBCtEJkZsuWzWYd33//PTVu3JiOHDlCNWvWlOu5d++ez78rwzCMGYgihBB6bwTDMIwnYkRnzpxJsWPHtnn922+/lRMsou3bt5diUqNUqVJUpEgR+u2332jSpEnUs2dPunz5MsWLF0++v3LlSqpTpw5du3aNUqZMSWnTpqXWrVvTTz/95HAbsI6+ffvSjz/+aBG38ePHp1WrVnGsKsMwjAM4RpRhGL/h/ffftxGaIEmSJJbnpUuXtnkP/x86dEg+h2W0YMGCFhEKypYtS8HBwXT69GkpMiFIK1euHOY2FChQwPIcywoKCqJbt25F+rsxDMP4IyxEGYbxGyD87F3lngJxo64QI0YMm/8hYCFmGYZhmHfhGFGGYQKGXbt2vfN/7ty55XM8InYU7nSN7du3U9SoUSlnzpyUIEECypQpE23YsMHn280wDOOvsEWUYRi/4eXLl3Tjxg2b16JHj07JkiWTz5GAVKxYMSpXrhzNmjWL9uzZQ1OmTJHvIalowIAB1LJlS/ruu+/o9u3b1KVLF2revLmMDwV4HXGmKVKkkNn3jx8/lmIV8zEMwzDuw0KUYRi/YfXq1bKkkjWwZp46dcqS0T537lzq2LGjnG/OnDmUJ08e+R7KLa1Zs4a6du1KxYsXl/8jw37EiBGWZUGkvnjxgkaOHElff/21FLgNGzb08bdkGIbxHzhrnmGYgACxmosWLaL69evrvSkMwzBMCBwjyjAMwzAMw+gCC1GGYRiGYRhGFzhGlGGYgIB7dzAMwxgPtogyDMMwDMMwusBClGEYhmEYhtEFFqIMwzAMwzCMLrAQZRiGYRiGYXSBhSjDMAzDMAyjCyxEGYZhGIZhGF1gIcowDMMwDMPoAgtRhmEYhmEYhvTg/7DcCSCFgglHAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the training and Validation Losses\n", + "\n", + "fig, ax1 = plt.subplots()\n", + "\n", + "best_epoch = np.argmin(diff_scores)+1\n", + "\n", + "\n", + "\n", + "ax1.plot(np.arange(len(Training_losses_adv))+1, Training_losses_adv, 'k-', label='Training Adversary')\n", + "ax1.plot(np.arange(len(Validation_losses_adv))+1, Validation_losses_adv, 'k--', label='Validation Adversary')\n", + "\n", + "ax1.set_xlabel('Epoch')\n", + "ax1.set_ylabel('Adversary Loss', color='k')\n", + "ax1.tick_params(axis='y', labelcolor='k')\n", + "\n", + "ax2 = ax1.twinx()\n", + "\n", + "ax2.plot(np.arange(len(Training_losses_clas))+1, Training_losses_clas, 'r-', label='Training Classifier')\n", + "ax2.plot(np.arange(len(Validation_losses_clas))+1, Validation_losses_clas, 'r--', label='Validation Classifier')\n", + "ax2.axvline(best_epoch,label=\"Final model stopped here\",color='k')\n", + "\n", + "ax2.set_ylabel('Classifier Loss', color='r')\n", + "ax2.tick_params(axis='y', labelcolor='r')\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "44f7e36b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'Epoch')" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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wunZs6flvvvua3efkd7vmpoay5YtW8SFv/KeeeYZcV1Wav3v//6vpVu3bpZPPvnEsn37dlHV07t3b0t9fb39ObjK54UXXrDfXrx4sah0e/PNNy27d++23HLLLeI5ioqKLHrZ96amJsv06dMtOTk5lq1bt7b57Dc2Nrrd984+M1rfb77vT3/6k2XNmjViH7799lvL6NGjLf3797c0NDQY+j2XVFVVWeLi4iz//ve/XT6HHt/zQACBBMSHyNWFS7slZ599tiiNldx9993ixzUqKsqSnp5uueiiiyybN2/W3dG8+uqrLZmZmWI/srOzxe0DBw643W/mv//9r2XAgAHiMUOHDrV88cUXFj3Cgoff53379rW7z0jv9/fff+/y71vuH5f6P/TQQ2K/+AdwypQp7Y5JXl6eEMOO8A+IPCZcAr527VqLnvadf+zcffb5ce72vbPPjNb3m0/8zj//fEv37t3FyQ3v380339xO6BjxPZe88sorltjYWNHSwhV6fM8DQRf+x7NYEwAAAABAaAAPEgAAAACAExBIAAAAAABOQCABAAAAADgBgQQAAAAA4AQEEgAAAACAExBIAAAAAABOQCABAAAAADgBgQQAAArRpUsXWrp0KY4nAAYAAgkAYAhuuOEGIVCcLxdccIHamwYA0CERam8AAAAoBYshHsDrSHR0NA4wAMBrEEECABgGFkMZGRltLsnJyeI+jib9+9//pgsvvJBiY2OpT58+9MEHH7R5/I4dO+jcc88V9/Nk+1tuuUVMLnfk9ddfp6FDh4rX4mnmd955Z5v7y8rK6LLLLqO4uDjq378/ffrpp0HYcwCA0kAgAQBChoceeoiuuOIK2rZtG1133XV0zTXX0J49e8R9tbW1NHXqVCGoNmzYQEuWLKFvv/22jQBigXXHHXcI4cRiisVPv3792rzGo48+SjNnzqTt27fTRRddJF6noqIi6PsKAPCTgIzABQCAIMPTysPDwy3x8fFtLo8//ri4n7/ubr311jaPGT9+vOW2224T1xcsWGBJTk62nD592n7/F198YQkLC7NPes/KyrL89a9/dbsN/BoPPvig/TY/Fy/78ssvFd9fAEBggQcJAGAYzjnnHBHlcSQlJcV+feLEiW3u49tbt24V1zmSlJ+fT/Hx8fb7J0+eTGazmfbt2ydSdCdOnKApU6Z0uA0jRoywX+fnSkxMpJKSEr/3DQAQXCCQAACGgQWJc8pLKdiX5AmRkZFtbrOwYpEFANAX8CABAEKGtWvXtrs9ePBgcZ3/Z28Se5Ekq1atorCwMBo4cCAlJCRQr169aMWKFUHfbgBA8EEECQBgGBobG6moqKjNsoiICEpLSxPX2Xg9duxYOvPMM+ndd9+l9evX08KFC8V9bKaeP38+zZkzhx555BEqLS2lu+66i66//npKT08X6/DyW2+9lXr06CGq4WpqaoSI4vUAAMYCAgkAYBiWL18uSu8d4ejP3r177RVmixcvpttvv12s995779GQIUPEfVyW/9VXX9G8efPojDPOELe54u2ZZ56xPxeLp4aGBnr22WfpT3/6kxBeV155ZZD3EgAQDLqwUzsorwQAACrCXqCPP/6YZsyYgfcBANAp8CABAAAAADgBgQQAAAAA4AQ8SACAkABuAgCANyCCBAAAAADgBAQSAAAAAIATEEgAAAAAAE5AIAEAAAAAOAGBBAAAAADgBAQSAAAAAIATEEgAAAAAAE5AIAEAAAAAOAGBBAAAAABAbfn/DtbGQcG0UDcAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# This shows the BCE loss of the classifier\n", + "Validation_losses_clas+lambda_*Validation_losses_adv\n", + "\n", + "plt.title(\"Classifier Loss\")\n", + "plt.plot(np.arange(len(Training_losses_adv))+1, Validation_losses_clas+lambda_*Validation_losses_adv)\n", + "plt.ylabel(\"BCE loss\")\n", + "plt.xlabel('Epoch')" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "5076500e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'Epoch')" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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EBERYfbv9OzZ03DhhRc2u44ZM2ZYeZAkN998syElJcXQ0NBgGDhwoPAR2XqS+PvY39MW/L39+vVz6D59/vnn4nr/9a9/mT05ISEhhurqaqvL3X777eJylvDHM2fOtHvd119/vaFv377CP8V+qr///e8O3aa2PEj/93//J372/v37rT6fl5cnPm/r52rt/trCj/+DDz4oLnP48GGHbi8AWgcVJACAXaqqqoQPiMfyeXLK9sQVivLyclGJYK6++mratGmTmP6S8NSZvSoTt9NycnLEZNyuXbus2msMT1j5+fmJdpFtJYc/LioqavPZYz+R7fcOGjRI/C/bTvwzuPJi6afiya8lS5Y4/dvB7TSeFvvDH/4grperSq6AH1vmjTfesPo851IxctLOkftr+7hx5Y6nDy0vA4DewZg/AMAubJrmN3gWCi1l/HALjD0w7N1hkcStOM7T4c+3NuZvmRHE7R4Z6shiSX4seeWVV+jJJ58Uo/Q8zs65PXw9PNY+ffp0cxwAj/mzgXvv3r1W38+Cgttm119/vcgPYkH3r3/9S9xWbjOxn4jbiDzyzqZsbquxt+ntt98W941vt6XgYCE1c+ZM+sc//tHiY8YRASkpKUKEcNtKmqvbgh8XbvHdeeedzb42ePBgIYDkmL+MPGAhyh9bjvk7cn/5a2wg57wq9iCxf4mjE/g2sA+MBRMAusfbJSwAgHK59tprDcHBwYaKigq7l5k2bZohICDAUFhYKD7evXu3aHfx96WmphpeeOEFw/vvv99ii4254447xNfGjRtn92d88803hosvvtgQFhYmTr179xZtLsvWm70W2/bt20UrsEuXLiJagNuF11xzjYgusIRvY2ZmprgMX/+HH35omDt3rtMtNsuW4hdffGFwFG6x8fe0dLrvvvvEZerq6gzPPfecIT09XTzmaWlphieffNKqNejI/f36668N48ePF18LDAwUl+UWW25ursO3FwCtgwoSAAC4mN///vf0/vvvi9BINqIDANQH6qgAAOBCuG3I02s8ag9xBIB68ff2DQAAAC3AvqXVq1eLEXv2H7EHCwCgXiCQAADABfDkGidms8n8zTffNE+OAQDUCTxIAAAAAAA2wIMEAAAAAGADBBIAAAAAgA3wILUTDsLjUDsOrevolm4AAAAAeAaOM+MAVQ50bS0UFQKpnbA4SktLa++3AwAA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# This shows the CCE loss of the Adversary\n", + "plt.title(\"Adversary Loss\")\n", + "plt.plot(np.arange(len(Training_losses_adv))+1, Validation_losses_adv)\n", + "plt.ylabel(\"CCE loss\")\n", + "plt.xlabel('Epoch')" + ] + }, + { + "cell_type": "markdown", + "id": "6e0fce0b", + "metadata": {}, + "source": [ + "## Test adversarially trained models" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "8f365eb2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test AUROC: 0.9960\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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pKRIamrqGft+++23ctttt5mOsTExMdKuXTuZOHFimUP5v/rqK/nd734njRs3ltjYWDNCaMSIEfL9999LZeh5DBw40HQ6T0xMlIcfftjMi3Q2mzZtkuuuu07q1asnF1xwgYwcOVJOnDgRsM3q1auld+/eUrduXWnSpImMHTv2jOPqP+yfe+4583Pqz9u8eXN55plnzijPrl27mvfbtm1rytNphBsAgK10Xh7fY+bMmRIfHx+w7qGHHqqWEtdAsXDhQnnkkUdk/vz5VTqW72fYv3+//P3vf5ft27ebYOLvs88+k549e5pRbsuXLzchRS/0ejHXIKHrfd577z355S9/Kbm5ufLmm2/K1q1b5b//+78lISFBJkyYUOHz0okdNdjosdetWyevv/66+TwNVOX56aefpH///iZofP7557Jy5UoTyjTs+fzrX/+SAQMGyK9+9SsTwhYtWiTLli2TcePGBRzrgQcekFdffdUEnG3btpltevToUfK+TmWg59evXz/ZvHmzPPjgg3L33XfLqlWrxFFWiElPT9f2I/Nsp017jlqtxr5n9Zm22tbjAghdJ0+etL777jvz7FNUVGRl5ea78tDPrqzXXnvNSkhICFg3d+5cq0OHDlZMTIzVvn17a9asWSXv5ebmWqNGjbKaNGli3m/ZsqU1ZcoU816rVq3M97fvoctns2DBAuuXv/yldfz4cSsuLs5KTU0NeH/YsGHWTTfddMZ+H330kTn+sWPHyv0Z/vKXvwRcS7RsLr30Uqt79+5WYWFhwLabN2+2wsLCrGnTppnlrKwsq2HDhtbNN99c5nn7PrciVqxYYYWHh1tpaWkl62bPnm3Fx8ebsizLyy+/bCUmJgac59dff21+nh9++MEsjx8/3vws/pYtW2bFxsZaGRkZZln/NiMjI61t27aVe36PPPKIddlllwWsS0lJsZKTkyv8N38+12/mubErJNp1IAA4i5P5hXLpRIf/1VuO7yYnS1x01S4bWkuhtQovvfSSXHHFFaZWQJtiateuLcOGDZO//OUv5l//ixcvlpYtW8revXvNQ33xxRem2eW1114zNQrnmgdl3rx58h//8R+mNuSGG24wNRqVqRUpz6FDh2Tp0qXm833noLUS3333nbz11ltnDN3v3LmzqSn529/+Zpp2tNbiyJEjpkapLNpU5NO6dWtTo1JeU9769evl8ssvN01bPsnJyXLvvfea2hgt49K0tkgnhPQ/z1q1apnntWvXmhod3UabyvzpNjk5ObJx40b5t3/7N/nf//1fueiii0wtlP4+tIlKf87p06dLgwYNSs5P1/nT89MaHM83S82aNcv8ArUgtUpvw4YNZ91e2047dOhgttdf6ooVK6SmoM8NAJRv0qRJ8vzzz8stt9wiF154oXkePXq0vPzyyyX9R7TvyZVXXimtWrUyz7fffrt5T/ux+C7+2gfEt1yWH374wTQTad8YpSFHQ9H5Dv5IT083twTQEKZB4qOPPpJRo0aZZeXrJ3PJJZeUub+u922j56b0OnYubdq0kYYNG5b7flpaWkCwUY2Ll/W9slxzzTXmvWeffdY0Zx07dqykuUmb3nwBRJu5NJBp05c2x02ePDlgm507d8qePXvMNfmNN94w4VGDz6233nrO88vIyJCTJ0+KU1yvudF2vDFjxsicOXNMsNH2WS1Ubc/UhF6aFrb+oU+dOlV+/etfm5SsHcK0c1THjh1d+RkAoLrUioowNShufXZVZGVlyY8//ih33XWXqa3x0U6qWruitJZC+6e0b9/e1Abo9/z1119f6c/SPjZ6LfEFA+0/op/74YcfyrXXXlvp42mnWr3O6My5//jHP0wNVOmOs6oi4akyAUs79drtsssuM31z9No7fvx4U/v0X//1XyZ0+GpztMw1/Nxzzz0yZMgQ0xlYa70++eSTkm109myt4dFgox2KfbVl3bp1M9dw/R26xfWamxkzZpg/8uHDh8ull15qQo72+C6v89cLL7xg/uC1N7gm4aeeesr0wtYqTjflFRS5+vkAQoOO2NGmITceVZ091jcaZ+7cuaYZx/f45ptvTC2L0u9z7YSq3+36L3sdeeRfE1ARWtOgF2/t1BsZGWkeel05evRowLVFOwlrjUxpOkpKL/i+WhmlF3RtrtHrjoYC7QysTT8+vou7dgwui673beN71g64VaU1WAcPHgxYd7B4Wd8rz+DBg02titbI/Pzzz6bZS+/rpM1MPvpzallobZo2o910001mvW8bHTWmZev7efxrrnwjyco7Py17X1OY58KNVodpFZZ/e5z+AemyttOVpbz2u/K211Sp1V/+DyfsPJzlyHEBwCu0ZkBvAKrNGRoU/B/aROWjFz5tTtIQpLX7OjpJg4mKiooy4eVstKtCZmam6c/jH6K0iWXJkiUlQ7y1ZkH7peh1wp/W0Oj56GeVR5tx9Nx0W9WlSxfTzPTnP//Z1Gj405FHH3zwQUnzmtaKaI2S9k0pi+/8KqJXr16yZcsW0w/I5/333zdlqBUGFfmdaHOb/iza1UNrzfxpoNXfmQYRLT+9z5kGUNWnTx9T66a1cT6+pjdtUvSdX+naJz0/Xe8oy0X79+83PZ/XrVsXsP7hhx+2evToUeY+UVFR1ltvvRWwTnvaa8/vskyaNCmgd73v4cRoqfaPr7D+9I+tth4XQOg628iRYFF6pJGOlKpVq5b1wgsvWNu3bzejdObPn289//zz5n191u/4rVu3mvfvuusuM3LKN7Ln4osvtu69917rwIED1tGjR8v8TB0BpSNyStNj6LFeeumlklFJeu247bbbrC+//NKMFJo3b55Vt25dM+KovJ/BR/cbOHBgyfKnn35qRmXpKKjPP//c2rNnj7V48WIrKSnJ6t27t5WTk1Oy7bvvvmuuZ4MGDbLef/99a9euXdYXX3xhrn/+537NNddYL774YrnlW1BQYHXs2NG6/vrrzaislStXWo0aNTKjnXz0XHRU2r59+0rW6TE3btxoyljLw/c78Td9+nTz+/nmm2+syZMnm/NdunRpQHl27drVuuqqq6xNmzaZMuzZs6d13XXXlWyzc+dOUyb6c+nvVK/XERER5jydHC3l+XCjf0xaEL7H3r17HQk3AGA3L4Yb9eabb1pdunSxoqOjrfr165uL45IlS8x7r7zyinmvdu3aZjjztddeay6c/sOR27Zta4YglzUUXIdE63saKsqiweiKK64oWdaL+29+8xurWbNm5jM7d+5sApj/sPfyws369evN9UTDg4+Ggd/+9rdWgwYNzPWqTZs21uOPP26Gf5emYeaWW24xYUSHvevPNXLkyJLh2Ep/Rv1H+tns3r3buuGGG0xA0SHmf/zjH638/PwzhrZrgPIZMmSIOUf9HXTq1Ml64403zjhuv379zM+tw781tOiw87Ku4/oz1KlTx2rcuLF15513Wj///HPANvr5vt/3RRddZMqzPHaFmzD9j7jYLKXtoO+8807ALJE6HFCr5f7nf/7njH10aKC2A/oPI9Pe9zpjpVb9nYs2S2nHNW1n1Wo7AKipdNit9j/RJpLSw3KBUPubz6jE9dvVPjc6zl57Vfu3x2lbpS6X1x7nWvsdAAAICq4PBddaGK2p6d69u5myWYeC63BBHT2lhg4dau5VoUO/fVM9X3311WaeBJ3SWafW/vLLL+WVV15x+ScBAAA1gevhRnvE6/AznbFSh6Vpj3O9z4Vv0h8dTuY/i6LexEvntnn88cfl0UcfNZM9aZMUc9wAAADlap8bN9DnBkCwoM8NQk2OF/rcAADOLcT+DYoQZtn0t064AYAayjeJXHZ2ttunAlTbKGp1rpui1vg+NwCAsukXvN4k0jf7rE6dUdVbIAA1lY6W1j64+neut3WoCsINANRgvvsD+U+vD3hVeHi4mc+uqiGecAMANZh+yesNChMTE80dqQEvi46ODhghfb4INwAQJE1UVe2HAIQKOhQDAABPIdwAAABPIdwAAABPiQzVCYJ0pkMAABAcfNftikz0F3LhJjMz0zwnJSW5fSoAAOA8ruN6G4azCbl7S+kkQT/99JPUrVvX9smwNFVqaNq7d+8573sByrmm4++ZcvYa/qaDu5w1rmiwadas2TmHi4dczY0WSIsWLRz9DP1lEm6cRzlXD8qZcvYa/qaDt5zPVWPjQ4diAADgKYQbAADgKYQbG8XExMikSZPMM5xDOVcPyply9hr+pkOnnEOuQzEAAPA2am4AAICnEG4AAICnEG4AAICnEG4AAICnEG4qadasWdK6dWuJjY2Vnj17yoYNG866/dtvvy0dOnQw219++eWyYsWKqvy+QkZlynnu3LnSt29fqV+/vnn079//nL8XVL6c/S1cuNDM8H3zzTdTlDb/Pavjx4/LqFGjpGnTpmbESbt27fjucKCcZ86cKe3bt5datWqZGXVHjx4tOTk5/E2fxccffyyDBg0yswTrd8C7774r57JmzRrp2rWr+Vtu27atLFiwQByno6VQMQsXLrSio6Ot+fPnW99++601YsQIq169etbBgwfL3P7TTz+1IiIirOnTp1vfffed9fjjj1tRUVHWli1bKHIby3nw4MHWrFmzrK+++sraunWrdeedd1oJCQnWvn37KGcby9ln165dVvPmza2+fftaN910E2Vscznn5uZa3bt3twYMGGCtXbvWlPeaNWuszZs3U9Y2lvObb75pxcTEmGct41WrVllNmza1Ro8eTTmfxYoVK6zHHnvMWrJkiY60tpYuXXq2za2dO3dacXFx1pgxY8x18MUXXzTXxZUrV1pOItxUQo8ePaxRo0aVLBcWFlrNmjWzpk6dWub2t912mzVw4MCAdT179rR+//vfn+/vKyRUtpxLKygosOrWrWu9/vrrDp5laJazlm3v3r2tV1991Ro2bBjhxoFynj17tnXRRRdZeXl5lfuFhrjKlrNue8011wSs0wtwnz59HD9Xr5AKhJtHHnnEuuyyywLWpaSkWMnJyY6eG81SFZSXlycbN240TR7+96nS5fXr15e5j673314lJyeXuz3Or5xLy87Olvz8fGnQoAFFauPfs5o8ebIkJibKXXfdRdk6VM7Lli2TXr16mWapxo0bS8eOHWXKlClSWFhImdtYzr179zb7+Jqudu7caZr+BgwYQDnbyK3rYMjdOPN8HTlyxHy56JeNP13etm1bmfukpaWVub2uh33lXNrYsWNNe3Dp/6FQtXJeu3atzJs3TzZv3kxROljOepH98MMP5Y477jAX2x07dsgf/vAHE9h11lfYU86DBw82+1155ZXmbtMFBQVyzz33yKOPPkoR26i866DeOfzkyZOmv5MTqLmBp0ybNs10dl26dKnpVAh7ZGZmypAhQ0zn7YYNG1KsDioqKjK1Y6+88op069ZNUlJS5LHHHpM5c+ZQ7jbSTq5aI/bXv/5VNm3aJEuWLJHly5fLU089RTl7ADU3FaRf6BEREXLw4MGA9brcpEmTMvfR9ZXZHudXzj7PPfecCTcffPCBdOrUieK08e/5xx9/lN27d5tREv4XYRUZGSnbt2+XNm3aUOZVLGelI6SioqLMfj6XXHKJ+RewNr9ER0dTzjaU84QJE0xgv/vuu82yjmbNysqSkSNHmjCpzVqouvKug/Hx8Y7V2ih+exWkXyj6r6jVq1cHfLnrsraPl0XX+2+v3n///XK3x/mVs5o+fbr5F9fKlSule/fuFKXNf886ncGWLVtMk5TvceONN0q/fv3Max1Gi6qXs+rTp49pivKFR/X999+b0EOwsefv2dc3r3SA8QVKbrloH9eug452V/bgUEMdOrhgwQIzpG3kyJFmqGFaWpp5f8iQIda4ceMChoJHRkZazz33nBmiPGnSJIaCO1DO06ZNM0NA33nnHevAgQMlj8zMTPv/CEK4nEtjtJQz5ZyammpG+913333W9u3brffee89KTEy0nn766Sr+xr2tsuWs38dazn/729/McOV//vOfVps2bcwoV5RPv1d12g19aISYMWOGeb1nzx7zvpaxlnXpoeAPP/ywuQ7qtB0MBa+BdIx+y5YtzcVUhx5+9tlnJe9dffXV5gvf3+LFi6127dqZ7XU43PLly104a2+Xc6tWrcz/ZKUf+uUF+8q5NMKNM3/Pat26dWbaCL1Y67DwZ555xgzDh33lnJ+fbz3xxBMm0MTGxlpJSUnWH/7wB+vYsWMU81l89NFHZX7f+spWn7WsS+/TpUsX83vRv+fXXnvNclqY/sfZuiEAAIDqQ58bAADgKYQbAADgKYQbAADgKYQbAADgKYQbAADgKYQbAADgKYQbAADgKYQbAADgKYQbAADgKYQbADXenXfeKWFhYWc89AaT/u/pDRTbtm0rkydPloKCArPvmjVrAvZp1KiRDBgwwNwIFIA3EW4ABIVf/epXcuDAgYDHhRdeGPDeDz/8IH/84x/liSeekGeffTZg/+3bt5ttVq1aJbm5uTJw4EDJy8tz6acB4CTCDYCgEBMTI02aNAl4REREBLzXqlUruffee6V///6ybNmygP0TExPNNl27dpUHH3xQ9u7dK9u2bXPppwHgJMINAM+pVatWubUy6enpsnDhQvNam7EAeE+k2ycAABXx3nvvSZ06dUqWb7jhBnn77bcDtrEsS1avXm2anu6///6A91q0aGGes7KyzPONN94oHTp0oPABDyLcAAgK/fr1k9mzZ5cs165d+4zgk5+fL0VFRTJ48GDT78bfJ598InFxcfLZZ5/JlClTZM6cOdV6/gCqD+EGQFDQMKMjoc4WfLSZqVmzZhIZeeZXm3Y+rlevnrRv314OHTokKSkp8vHHH1fDmQOobvS5AeCZ4NOyZcsyg01po0aNkm+++UaWLl1aLecHoHoRbgCEHG2eGjFihEyaNMn00wHgLYQbACHpvvvuk61bt57RKRlA8Auz+GcLAADwEGpuAACApxBuAACApxBuAACApxBuAACApxBuAACApxBuAACApxBuAACApxBuAACApxBuAACApxBuAACApxBuAACAeMn/A6a+ypOYjYL9AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Now test the model selected using diff_score\n", + "test_pred = mytools.test_clas(test_loader, final_clas_adv, device)\n", + "test_pred=torch.sigmoid(torch.tensor(test_pred)).numpy()\n", + "\n", + "\n", + "fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_test, test_pred, pos_label=1)\n", + "auc_test = sklearn.metrics.auc(fpr, tpr)\n", + "print(f'Test AUROC: {auc_test:0.4f}')\n", + "\n", + "plt.plot(fpr, tpr, label=f'Test AUROC: {auc_test:0.4f}')\n", + "plt.xlabel('FPR')\n", + "plt.ylabel('TPR')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "cb609604", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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NmPGDO46b4J6hF2+fNnquK+++grvv/+++d48//zzOHjwIFq2bMnNVukYE48++ig3TKU5FzUigMoJOk+/kp6CIAjlFLL8kPh55LW34VcluMivF339KtZ+N5mvWxYFUG5k7whfFFBj1u7du2PgwIHcdZ16Y1HTV7I+LV++nMccPXqUO7VTM1MSINT5nRq5pqen45tvvrF53qSkJG4MS41Tt2zZwttIQD322GPYs2ePuUksQU1ghw0bZl739MzySrz99tuoUqUKfvzxR3z00UcsqEzzWrp0KZ+nOMQPoxQCMTExSnkiLi6OJDovSzundx5VvhnQW1k/ewkvab0g3Lx4no87umk9L2ldEAT7JSUlRTl16hQvs/+dKK6/D3dzvWXLlimNGzdWXF1dFT8/P6Vbt25KYmKief+8efOU+vXrK1qtVqlXr54yc+ZM877Q0FD+m3/48GHztuPHjysPPfSQ4u7urgQGBirPPvusEhkZad6fnp6uTJgwQaldu7bi4uKiBAcHK1988QXvo3NZvrp27crbhwwZovTp08d8jtTUVOX1119XKlSowPPq2LGjsm/fPvP+rVu38vGbNm1SWrZsqeh0OqV9+/bKmTNncr0Pc+bM4fnS/EwcO3aMz3P+vHo/x4wZo7Rq1crquL/++ovvXXx8vM3zbtiwQXF0dLR6LsbGxioODg7Kxo0bzduqV6+ufPvtt7nOr0GDBsq6dev4/dq1a5WGDRuadURISIhy5coV5W6+o3fz/C5wEPSECRNYpZkYMGAA/P39WdGRqhQEQRCE4iQ8PByDBg3Ciy++iNOnT2Pbtm3o27ev2d20cOFCfPLJJ+ziof3jx49n68WCBQtsno/idB588EHcd999OHDgANavX49bt27x887EmDFj8PXXX/N5Tp06hUWLFpldQfv27ePlpk2beG4rVqyweZ13330Xf/zxB8+D3FYhISHo1asXoqOjrcaRpWby5Mk8F3JR0efMDXJNubi4WFlkdDodL8l1ZRrjmq2LOo2hLuvkmsrtvA4ODtBqteZtdA66jum8Jui+kC6g+zdp0iQYjRSjqkIWJLovGRkZ+Oeff9C0aVPePnr0aLz66qsIDi56C6MZpYDUqFFD2blzJ7//559/FB8fH1aGL730ktKjRw+lPCAWIEEQ7JWyaAE6ePAg/68/LCzM5n6y0ixatMhq27hx49ia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+vXrVvvCw8Oh0Wh4P40rKebPn291f0xCpEGDBjnGLlu2jPfVqFEjX+emztsBAQH4+uuvbe4fN24cKlasCIPBwD8nuifSUFmwF0QACYJQZNCDVafTITAwsETvcpUqVfDLL79YbVuwYAFvL2nI+pP9/ri7u3M37N27d1tt//HHH1GtWrV8n9vFxQXPPvssi6zsUBckElvPP/88d+imn1OlSpX4GEGwB0QACYIdcP/99+ONN97Au+++Cz8/P37QjR071rx/8ODBbAHILl7IemASDuvXr0enTp34ge3v749HH30UFy9ezGFtWbp0Kbp27QpXV1csXLgwhwuMjunTpw9bHjw8PNC6dWts2rTJ6tpk4Rg/fjxefPFFeHp68kN/7ty55v01a9bk5X333cfXpM+XF0OGDMkhAmidtluSnp6Ol156ic9PgqBevXqYNm2a1Zht27ahTZs2LFLoc3Xs2BGXL1/mfUePHsUDDzzAc/by8kLLli1x4MABFBSyTNHP5KeffjJvu3btGl+btmdn1apVaNGiBd/zWrVq4bPPPoPRaOR99HnIwrRjxw6rY7Zv345Lly7xfkGwR0QACYKdQBYPemjv3bsXEydOxOeff46NGzfyvmeeeQZ///03EhMTzeM3bNiA5ORkPPnkk7yelJSEUaNG8QN98+bNcHR05H0ZGRlW13n//fcxcuRInD59Gr169coxD7rGI488wuc4fPgwHnroITz22GO4cuWK1bjJkyejVatWPOaVV17BiBEjcPbsWd63b98+XpJwIrfNihUr8vzsjz/+OGJiYswigJa0Tte1hD5L1apV2dV06tQpfPLJJ/jggw/w+++/834SFeRaJIF37NgxttAMHz6cRZjpPtLx+/fvx8GDB/lekHXlbiDxR9elnwFBQpLuFQlHS/777z+24tA9pznPmTOHx3755Ze8v0mTJiwyLcWUSQB26NAB9evXv6v5CUKZp9D7ywuCUOIMGTJE6dOnj3m9a9euSqdOnazGtG7dWnnvvff4vcFgUAICApRffvnFvH/QoEHKwIEDc71GZGSkQn9Cjh8/zuuhoaG8PnXqVKtx8+fPV7y9vfOcb6NGjZQZM2aY16tXr648++yz5vWMjAwlMDBQmTVrltW1Dh8+nOd5Lce9+eabytChQ3k7Ld966y3eTvtpXG68+uqrSr9+/fj97du3efy2bdtsjvX09FR+/vlnJb/Quf78889c71fz5s2VBQsW8OevXbu2smrVKuXbb7/l+2OiW7duyvjx463O8euvvyqVK1c2r8+ePVvx8PBQEhISeD0+Pl5xc3NTfvjhhxxzou/KyJEj8/0ZBKGsIhYgQbATmjZtarVeuXJljjMxuVwGDBjALiuTtYfcKmTRMHH+/HkMGjSIXSzk3jEF4ma33JDVJi/IAvTOO+9wkC+5kMgNRtai7OexnC9ZWMhtZ5rv3VpUyLJz8+ZNXtK6LWbOnMmuqwoVKvDcyPVmmhu5DynAnCxbZD0i9xhZoEyQhez//u//0L17dw48tnQR3u2cyVJD7ir6mZDlLDvkdiNrHs3V9Bo2bBjPy2Q9op8bufdMlixyU5IFL7vbUxDsCRFAgmAnZHfFkKiwdF+R2CG3FImMlStXcgwMuVxM0AM/Ojoa8+bNYzcavUyZRpaQmy0vSPz8+eefHOND7hvK5CI3Tfbz3Gm+BYWuQe4eEgMkvho3bpxjzJIlS3h+FBfzzz//8NyGDh1qNTcSJOT6IvcRCYm6detiz549vI/iqk6ePInevXtjy5YtaNiwIX/Wu4V+JnRuOu9zzz3HQtWWoKSYH5qr6XX8+HEWrBQTRJBgfeqpp8xxULQkwUtiSRDslZy/TYIg2CX0QA8ODuaH+rp169C/f3+zCLl9+zbH35D46dy5M2/LHlSbX3bu3MlWFFNsET3AC5qGbspUIqtGQS0qFE80a9asXOdG94HGmLBlxaHga3qNGTOG08YXLVqEdu3a8T4SRPR66623WGyR2DB91oJCFieKXyLLzezZs22OoeBn+tmEhITkeS4SdRQsvnr1auzatQuTJk26qzkJQnlBBJAgCGYow4getJQ1tHXrVvN2X19fzvwidxC5zsglRAG+d0OdOnU4aJksSmTV+fjjjwts2aG0cbJQUWYaBR2TpcPb2/uOx5FriIRdboUHaW6U9UYB4JQJ9uuvv3JAsynrLDQ0lO8BiZKgoCAWHmRpoSDklJQUjB49mi0tNJ6ytujYfv364V6ggObvv/+e778tKFCbMvIoU46uTa4tcoudOHECX3zxhXlcly5dWCTRXMkSRkJPEOwZcYEJgmDlcqFMIqqPQ+nd5j8Ujo7sHqLMJnIdkXXjbi0IU6ZMYUFFD2ASQRRPQ1aMgkCuoOnTp3PGEwkRSqvP73GU2m/LlUS8/PLL6Nu3L8fGtG3bli1fltYgNzc3nDlzhkUNWXkoA+zVV1/l46jYIo0ngUH7yMX08MMPs3vqXiChl5v4Iej+kVWHXHaU7UWWqG+//RbVq1e3GkdikyxglP2WW/yTINgTDhQJXdKTEARBsEdIlFCMUGmp2k2Qm6x58+aYOnVqSU9FEIoUsQAJgiCUIBQnRG68koYyACkomgLTBcEeEAuQIAhCCXHhwgVekvvMFGdUUiQkJODWrVv8nmKkyFUoCOUZEUCCIPx/e3ZAAwAAgCDM/qkNwp+CDYAcCwwAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABAKs5XQTmhD7nTHsAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df_test[\"Class_adv\"] = test_pred\n", + "df_bkg = df_test.loc[ (df_test.PhiKK == 0)]\n", + "per = np.percentile(df_bkg['Class_adv'],90.0)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals = 1000*df_cut.InvM\n", + "per = np.percentile(df_bkg['Class_adv'],95.0)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals2 = 1000*df_cut.InvM\n", + "per = np.percentile(df_bkg['Class_adv'],99.0)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals3 = 1000*df_cut.InvM\n", + "per = np.percentile(df_bkg['Class_adv'],99.5)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals4 = 1000*df_cut.InvM\n", + "per = np.percentile(df_bkg['Class_adv'],99.9)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals5 = 1000*df_cut.InvM\n", + "per = np.percentile(df_bkg['Class_adv'],99.95)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals6 = 1000*df_cut.InvM\n", + "per = np.percentile(df_bkg['Class_adv'],99.99)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals7 = 1000*df_cut.InvM\n", + "\n", + "plt.figure()\n", + "#plt.hist(1000*df_bkg.InvM, bins=np.arange(40,300,1), histtype='step', label='No selection', density=True)\n", + "plt.hist(x_vals, bins=np.arange(40,300,1), histtype='step', label= 'selection 90%', density=True)\n", + "plt.hist(x_vals2, bins=np.arange(40,300,1), histtype='step', label= 'selection 95%', density=True)\n", + "plt.hist(x_vals3, bins=np.arange(40,300,1), histtype='step', label= 'selection 99%', density=True)\n", + "plt.hist(x_vals4, bins=np.arange(40,300,1), histtype='step', label= 'selection 99.5%', density=True)\n", + "plt.hist(x_vals5, bins=np.arange(40,300,1), histtype='step', label= 'selection 99.9%', density=True)\n", + "plt.hist(x_vals6, bins=np.arange(40,300,1), histtype='step', label= 'selection 99.95%', density=True)\n", + "#plt.hist(x_vals7, bins=np.arange(40,300,1), histtype='step', label= 'selection 99.99%', density=True)\n", + "#plt.axvline(1019.461, color='k', linestyle='dashed', linewidth=1, label='PDG phi mass')\n", + "plt.xlabel('Invariant Mass [MeV]')\n", + "plt.ylabel('Normalized Counts')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "7f4d37ef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test AUROC: 0.9941\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAANKJJREFUeJzt3Qt4FOXd9/F/ziFAAhhCOARQKAergEBBQLQoNYpFrbWlooA8ClXBKngAVMAj8HhAaqWgKGLfagEt+PAKD1TRvEpBqSCtB0QQMAgkgEASkpDD7rzX/042ZCEJCczsbHa/n+sad2d2ZndyJ+78uA9zR1iWZQkAAECIiHT7BAAAAOxEuAEAACGFcAMAAEIK4QYAAIQUwg0AAAgphBsAABBSCDcAACCkREuY8Xq9sm/fPmncuLFERES4fToAAKAW9LZ8eXl50qpVK4mMrLluJuzCjQabtLQ0t08DAACcgT179kibNm1q3Cfswo3W2PgKJzEx0e3TAQAAtZCbm2sqJ3zX8ZqEXbjxNUVpsCHcAABQv9SmSwkdigEAQEgh3AAAgJBCuAEAACGFcAMAAEIK4QYAAIQUwg0AAAgphBsAABBSCDcAACCkEG4AAEBIIdwAAICQ4mq4+eijj2To0KFmhk+9nfI777xz2mMyMjKkZ8+eEhcXJx07dpRFixYF5FwBAED94Gq4yc/Pl+7du8vcuXNrtf+uXbvkmmuukUGDBsmWLVvk3nvvldtvv13WrFnj+LkCAID6wdWJM6+++mqz1Nb8+fPl3HPPleeee86sd+3aVdatWyfPP/+8pKenO3imAIKdZVnm0Wudfp8qXzvt+9fwWg1H13ScuPSZNb3sVBlJCP0sNZ3vGb50Vp95NmXk8VgS6UA1R2x0pKQ0jhe31KtZwTds2CCDBw/226ahRmtwqlNUVGSWylOmI/jpl1Kp15JSjyUlXm/Zo8drlhPPdZ8T2zy6v7fyo7diXf/n91onnnssy6zrhdDrLXu+53ChpCbFicdbtq9u91R+LN/XU77ue0/fo76u533iuf+6b1893jwv/zkr3qf8wuzbplu85edSm33LXjvxhV55e9n+ZU8q3qvSPvo5ctL2rNzjktI4ruKLsextT7y3b63i8yrt57et0r6VP6PyOZ78nieOKTv/mo4HEHx6tm0iy+4a4Nrn16twk5WVJS1atPDbpusaWAoLC6VBgwanHDNz5kx57LHHAniWoanU45VjRaVmKSj2mKVQl5JSOV7ileJSrxSVesxjYYnHbNNH3aeguFQKdb3YY/Y5XqKPXinS4zxlx+qjCSylZaFF1+G+A3kn/mEAhJuIiBpeq/G4iDM67vSfeYYnJNW/rN/FKi7a3uqbmCh3xyvVq3BzJqZMmSITJ06sWNcglJaWJuFIawyOFhTLkYJiOZhXLIeOFZnnuYUlkldUKvlFpXKkoERyCkpMiMk7XiJ5x/VRw4nH7dOXyIiy/2HKlgiJ1sfIssfoqAiJiYyUKLMeUfYYGSGREWXPddEvHH2PqIiy5/r/nr5uFn1dRLYfOCY/bZVo9tGqWt/xvv3MMb73Ne8jFe9bto9v/cS2mvbR18y6+O9Xtr3s6yjypH1178iT9q18vO7i22b2r/y6vl/5z1r29pW2+861fJvSwBkXHVWx7nvd97zsHSp/IZ94zbfJd07+x5z4DL/t5U8iqtm38sdEVPNZWrOjvzNHLhAOXOxOf2xNx9Xwc57uKurAZ57+WBcCwdkUBOqtehVuUlNTJTs722+bricmJlZZa6N0VJUu4UBrSHYezJfvDh6TvUcLZf/R47I/p1Cyc4vkQN5xOXSs2AScs6HpPiE2ShrGRUuDmCiJN0ukuQDqa9rOqtvjyrfrvgmx0Wa/sv0jK46JjdL99LHsOF9o8QUY3a5BxRdc9KIMAEBIhZt+/frJqlWr/La99957Znu4yT1eIpu/PyL/+SFHvtqXI9uy8uT7wwW16oOQGB8tyY3jJLlRnJzTMFYS42OkUXy0CSxNE2KkSUKMNIrTJVoSG0RL47gYaRwfbfZxu6oRAICgDjfHjh2THTt2+A311iHezZo1k7Zt25ompb1798pf/vIX8/odd9whL774ojz44IPyX//1X/LBBx/I0qVLZeXKlRIOtmfnyYp/75OPvj0oX+7LrbIWplnDWOnQvKGkNUuQlknxkprUQFomxktKYpy0SIw3YUabcQAACFWuhpvPPvvM3LPGx9c3ZtSoUebmfPv375fMzMyK13UYuAaZCRMmyB//+Edp06aNvPLKKyE9DPxIfrG8vekH+fvmH+SbrDy/19qdkyA90prIha2TpGvLROnUorE0bxweTXAAAFQnwqrpRgAhSDsUJyUlSU5OjumrE6z2HS2Ulz/aKYv/lWlGHintj3JZp+Zy1QUtpV+Hc6R1k6r7GQEAEM7X73rV5yYc6FDpVz7eJX/6YHtFqNHROzf1aStDu7WSpIQYt08RAICgRrgJIjsO5Mldb2yWb7OPmfXe7ZrKH674iQz8STLDGQEAqCXCTZDQTsJ3/nWT5Bd7zCimh6/pItf3aE2oAQCgjgg3QWDlf/bLPYs/N1MF9D23mfxp+EWuzskBAEB9Rrhx2fodh+TeJWXBZmj3VvLcb7qbG9oBAIAzQ7hxkd49+A+LPzdzKV1zYUuZM6xHjbeNBwAAp0cVgUv0Bnz3/G2LmRKhS2pjee633Qk2AADYgHDjkrc37ZGNuw9Lw9gomX9LLzPfEgAAOHuEGxcUl3rlhbVl007cO7iTtE9u6MZpAAAQkgg3Lnhr0x4za7dOlTCiXzs3TgEAgJBFuAkwr9eSl/7fTvP8rp93oDkKAACbEW4C7JOdP0rm4QJpHBctv/tZ20B/PAAAIY9wE2D/55PvzeO1PVpJg1g6EQMAYDfCTQDlFJTI+1uzzfNbLqavDQAATiDcBND//c8+c8M+va9N15Y1T9cOAADODOEmgP7vv/eZxxt6tg7kxwIAEFYINwFyJL9YPvv+iHl+9QUtA/WxAACEHcJNgHy47YCZckGbo9KaJQTqYwEACDuEmwDJ2HbQPP68c/NAfSQAAGGJcBOgG/d9vL0s3FzeJSUQHwkAQNgi3ATA1qxcOVJQIgmxUdIjrUkgPhIAgLBFuAmAjbsOm8eftW8mMVEUOQAATuJKGwC+UVK92jUNxMcBABDWCDcOsyxLPt35o3l+8XnnOP1xAACEPcKNw3YeypdDx4olLjpSuqclhf0fHAAATiPcOGxzeZPUha2TJC6aiTIBAHAa4cZhmzOPmkf62wAAEBiEG4d9tS/HPHZrwxBwAAACgXDj8M37tmcfM8+7tGzs5EcBAIByhBuHOxMXlngkPiZS2jGfFAAAAUG4cdC2rDzz2Dk1UaK5eR8AAAFBuHHQt9ll4aZTSiMnPwYAAFRCuHHQ9gO+mhv62wAAECiEGwftPJhvHs9r3tDJjwEAAJUQbhwcKbXrUHm4SaZZCgCAQCHcOGTv0UIpKvVKbFSkpDFSCgCAgCHcODgMXLU7J0GiIiOc+hgAAHASwo1DMn/0hRv62wAAEEiEG4dkHi4wj21pkgIAIKAINw7Zdags3LRPTnDqIwAAQBUINw754Qg1NwAAuIFw45B9RwvNY+smDZz6CAAAUAXCjQMKiz2Se7zUPG+RFO/ERwAAgGoQbhyQnXvcPDaIiZLGcdFOfAQAAKgG4cYBWeXhJjUpXiIiuMcNAACBRLhxwKFjReYxuVGsE28PAABqQLhxwMG8snDTvHGcE28PAABqQLhxwOH8YvN4TkPCDQAAgUa4ccCP5eGmaUOapQAACDTCjQNyCkrMY7OEGCfeHgAA1IBw44CjhWU1N0mEGwAAAo5w44DcwrIb+CU1oOYGAIBAI9w4IPd4WbNU43jCDQAAgUa4cUBOYVm4oeYGAIDAI9zYzLIsySufVyqRmhsAAAKOcGOzolKveLyWed4onnmlAAAINMKNzXy1NjqlVEJMlN1vDwAAgj3czJ07V9q3by/x8fHSt29f2bhxY437z5kzRzp37iwNGjSQtLQ0mTBhghw/XjZRZTDIK+9M3Cg2WiIjmTQTAICwCjdLliyRiRMnyvTp02Xz5s3SvXt3SU9PlwMHDlS5/5tvvimTJ082+2/dulVeffVV8x4PPfSQBIv8Io95bBhHkxQAAGEXbmbPni1jxoyR0aNHy/nnny/z58+XhIQEWbhwYZX7r1+/XgYMGCDDhw83tT1XXnml3HTTTTXW9hQVFUlubq7f4qRjRWXNUg3jaJICACCswk1xcbFs2rRJBg8efOJkIiPN+oYNG6o8pn///uYYX5jZuXOnrFq1SoYMGVLt58ycOVOSkpIqFm3KclJBcVm4aUTNDQAArnCt7eTQoUPi8XikRYsWftt1/ZtvvqnyGK2x0eMuueQSM+S6tLRU7rjjjhqbpaZMmWKavny05sbJgFNQXNYslRBLsxQAAGHZobguMjIyZMaMGfLnP//Z9NFZtmyZrFy5Up544olqj4mLi5PExES/xUmF5eGmQSzNUgAAuMG16oXk5GSJioqS7Oxsv+26npqaWuUxU6dOlREjRsjtt99u1i+88ELJz8+XsWPHysMPP2yatdxWWFIWbuJj3D8XAADCkWtX4NjYWOnVq5esXbu2YpvX6zXr/fr1q/KYgoKCUwKMBiSlzVTB4ES4oeYGAAA3uNoxRPvCjBo1Snr37i19+vQx97DRmhgdPaVGjhwprVu3Np2C1dChQ80Iq4suusjcE2fHjh2mNke3+0KO23zNUgk0SwEAEH7hZtiwYXLw4EGZNm2aZGVlSY8ePWT16tUVnYwzMzP9amoeeeQRiYiIMI979+6V5s2bm2Dz1FNPSbA47qu5iQ6OsAUAQLiJsIKlPSdAdLSUDgnPyclxpHPxoyu+kkXrd8u4QR3kgfQutr8/AADhqC7Xb3q9OjBxpoqj5gYAAFcQbmxWXB5uYqMpWgAA3MAV2GbHS8v63MQRbgAAcAXhxqGaG5qlAABwB+HGZiWesnATExVh91sDAIBaINzYjD43AAC4i3DjVLiJomgBAHADV2CHmqUYLQUAgDsINzYr9pTdEzGGmhsAAFxBuLFZaUWHYooWAAA3cAW2GaOlAABwF+HGZiXlzVLR1NwAAOAKwo3Nin0digk3AAC4gnDjUJ+b2Ghu4gcAgBsIN041S0VStAAAuIErsFMdipk4EwAAVxBunAo3kTRLAQDgBsKNjTxeS7xlrVKMlgIAwCWEGxuVestqbVQ0s4IDAOAKwo2NSss7E6sYOhQDAOAKwo1D4YaaGwAA3EG4sVFJ5WYpOhQDAOAKwo3NHYpVVGSEREQwWgoAADcQbmxUWincAAAAdxBubOT1hRtqbQAAcA3hxoGaG/rbAADgHsKNA31uImmWAgDANYQbhzoUAwAAdxBubES4AQDAfYQbJ8INHYoBAHAN4cZGBcWl5tGSE3cqBgAAgUW4sVF0VFlxZucW2fm2AACgDgg3DjRLnZfc0M63BQAAdUC4sREdigEAcB/hxkaEGwAA3Ee4sZHHKr+JH6OlAABwDeHGgbmloqO4iR8AAG4h3DgwtxQ1NwAAuIdwYyP63AAA4D7CjY24QzEAAO4j3DjRoZhSBQDANVyGHehQzKzgAAC4h3BjIy9DwQEAcB3hxkZ0KAYAwH2EGxtRcwMAgPsINzbyeMsLlTsUAwDgGsKNAzU3UZQqAACu4TLsSLhh+gUAANxCuHGgQzHNUgAAuIdwY6PybEO4AQDARYQbG1k0SwEA4DrCjQPNUgyWAgDAPYQbG9EsBQCA+wg3ToyWouoGAADXEG4cmDiTWcEBAHAP4caBZqkIam4AAAjfcDN37lxp3769xMfHS9++fWXjxo017n/06FEZN26ctGzZUuLi4qRTp06yatUqCQYemqUAAHBdtJsfvmTJEpk4caLMnz/fBJs5c+ZIenq6bNu2TVJSUk7Zv7i4WH7xi1+Y195++21p3bq1fP/999KkSRMJpqHg3KAYAIAwDTezZ8+WMWPGyOjRo826hpyVK1fKwoULZfLkyafsr9sPHz4s69evl5iYGLNNa31qUlRUZBaf3NxccbpDMc1SAACEYbOU1sJs2rRJBg8efOJkIiPN+oYNG6o8ZsWKFdKvXz/TLNWiRQu54IILZMaMGeLxeKr9nJkzZ0pSUlLFkpaWJk5hVnAAAMI43Bw6dMiEEg0plel6VlZWlcfs3LnTNEfpcdrPZurUqfLcc8/Jk08+We3nTJkyRXJyciqWPXv2iPN3KHbsIwAAQDA3S9WV1+s1/W1efvlliYqKkl69esnevXvlmWeekenTp1d5jHY61iUg51fR54ZZwQEACLtwk5ycbAJKdna233ZdT01NrfIYHSGlfW30OJ+uXbuamh5t5oqNjZWguEMxPYoBAHCNaw0oGkS05mXt2rV+NTO6rv1qqjJgwADZsWOH2c/n22+/NaHH7WDjX3Pj9pkAABC+XO0dosPAFyxYIK+//rps3bpV7rzzTsnPz68YPTVy5EjTZ8ZHX9fRUvfcc48JNTqySjsUawfjYFCebWiWAgAgXPvcDBs2TA4ePCjTpk0zTUs9evSQ1atXV3QyzszMNCOofHSk05o1a2TChAnSrVs3c58bDTqTJk2SYMBQcAAA3Od6h+Lx48ebpSoZGRmnbNMmq08++USCEc1SAAC4j0HLTnQoZrQUAACuIdw4cJ8b+hMDAOAewo2NfIO4GAoOAIB7CDeOdCi2810BAEBdEG5sRJ8bAADcR7hxoM8NN/EDAMA9hBsbMbcUAADuI9w40CwVQacbAABcQ7ix0a5D+WWFSodiAABcQ7ix0TmNyibv/PFYsZ1vCwAA6oBwY6MGMVHmsUlCjJ1vCwAA6oBw48Cs4HHlIQcAAAQe4cZGTJwJAID7CDc24iZ+AAC4j3BjKybOBAAgZMLNsmXLpFu3bhLOqLkBAKCehZuXXnpJbrzxRhk+fLh8+umnZtsHH3wgF110kYwYMUIGDBgg4YyJMwEAqEfhZtasWXL33XfL7t27ZcWKFXL55ZfLjBkz5Oabb5Zhw4bJDz/8IPPmzZNwRs0NAADui67tjq+99posWLBARo0aJR9//LFcdtllsn79etmxY4c0bNjQ2bOsZxNnMvsCAAD1oOYmMzPT1NaogQMHSkxMjDz22GMEmyrucxNJugEAIPjDTVFRkcTHx1esx8bGSrNmzZw6r3qJPjcAANSjZik1depUSUhIMM+Li4vlySeflKSkJL99Zs+eLeGKmhsAAOpRuLn00ktl27ZtFev9+/eXnTt3+u0TEebNMSfuUBze5QAAQL0INxkZGc6eSQjV3JBtAACoJ81Subm55v422iTVp08fad68uXNnVg9Z5XcojqTiBgCA4A83W7ZskSFDhkhWVpZZb9y4sSxdulTS09OdPL96eZ8bEdINAABBP1pq0qRJcu6558o///lP2bRpk1xxxRUyfvx4Z8+unvGUp5soqm4AAAj+mhsNNP/4xz+kZ8+eZn3hwoVmKLg2VSUmJjp5jvWGr+KGehsAAOpBzc3hw4elTZs2FetNmjQxN/D78ccfnTq3+oc7FAMAUL86FH/99dcVfW580w1s3bpV8vLyKraF88zgvpobhoIDAFBPwo32s/HNn+Tzy1/+0tzfRrfro8fjkXC/zw3tUgAA1INws2vXLmfPJASQbQAAqEfh5vXXX5f777+/YvoFnIrpFwAAqEcdinUG8GPHjjl7NvUcE2cCAFCPws3JfW1QvQg63QAAEPzhRoX7xJinw9xSAADUs9FSnTp1Om3A0fvhhPvcUmRAAADqSbjRfjdJSUnOnU2IzC1FsxQAAPUk3Pzud7+TlJQU586mnvP1S6LmBgCAetDnhv42p8fcUgAAuI/RUg6km0hmBQcAIPibpbxer7NnEkr3uXH7RAAACGN1GgqOWjZLkW4AAHAN4caR+9yQbgAAcAvhxkY0SwEA4D7CjY2ouQEAwH2EGwfQKAUAgHsINzbae7TQPNLlBgAA9xBubNQyKd48lniYQR0AALcQbhzocxMXTbECAOAWrsIOzAoOAADcQ7hxZLSUne8KAADqgnDjyMSZpBsAANxCuLERNTcAALiPcGOr8okzqbgBAMA1hBsnam5olgIAILzDzdy5c6V9+/YSHx8vffv2lY0bN9bquMWLF5tJKq+//noJBswKDgCA+1wPN0uWLJGJEyfK9OnTZfPmzdK9e3dJT0+XAwcO1Hjc7t275f7775eBAwdKsLDKq25olQIAIIzDzezZs2XMmDEyevRoOf/882X+/PmSkJAgCxcurPYYj8cjN998szz22GNy3nnnSbCg5gYAgDAPN8XFxbJp0yYZPHjwiROKjDTrGzZsqPa4xx9/XFJSUuS222477WcUFRVJbm6u3+J0nxvqbgAACNNwc+jQIVML06JFC7/tup6VlVXlMevWrZNXX31VFixYUKvPmDlzpiQlJVUsaWlp4nizFO1SAACEb7NUXeTl5cmIESNMsElOTq7VMVOmTJGcnJyKZc+ePY6dH5MvAADgvmg3P1wDSlRUlGRnZ/tt1/XU1NRT9v/uu+9MR+KhQ4dWbPN6veYxOjpatm3bJh06dPA7Ji4uziwBUTEUHAAAuMXVmpvY2Fjp1auXrF271i+s6Hq/fv1O2b9Lly7yxRdfyJYtWyqWa6+9VgYNGmSeO9nkVLcOxcQbAADCsuZG6TDwUaNGSe/evaVPnz4yZ84cyc/PN6On1MiRI6V169am74zeB+eCCy7wO75Jkybm8eTtbmAoOAAA7nM93AwbNkwOHjwo06ZNM52Ie/ToIatXr67oZJyZmWlGUNUHDAUHAMB9EZavuiFM6FBwHTWlnYsTExNtfe+uU1dLYYlHPnpgkLQ9J8HW9wYAIJzl1uH6XT+qROoJi4kzAQBwHeHGRuFVBwYAQHAi3DiAwVIAALiHcGMjhoIDAOA+wo2duIkfAACuI9zYiA7FAAC4j3BjIzoUAwDgPsKNE31umF0KAADXEG6cmH6BqaUAAHAN4caRmhsAAOAWwo0TfW5INwAAuIZw4wD63AAA4B7CjU0qzz8aSc0NAACuIdw4IIIexQAAuIZwYxPucQMAQHAg3Nik8oTgtEoBAOAewo0DfW5olQIAwD2EGwcwWgoAAPcQbhxolgIAAO4h3DjRoZhONwAAuIZwYxOrUt0NfW4AAHAP4cYBVNwAAOAewo1NuM8NAADBgXDjAO5QDACAewg3DtTc0CwFAIB7CDc2oUMxAADBgXADAABCCuHGkWYpGqYAAHAL4cYmfvfwI9sAAOAawo0DE2cCAAD3EG4cQM0NAADuIdzYhHobAACCA+HGJnQoBgAgOBBu7FJ5tBQdigEAcA3hxgFkGwAA3EO4ceAOxQAAwD2EGyf63NAuBQCAawg3TtzEz643BQAAdUa4cQAVNwAAuIdwYxPuUAwAQHAg3DgytxQNUwAAuIVwYxOmlgIAIDgQbmxGpQ0AAO4i3NiE+9wAABAcCDc2d7qhtw0AAO4i3NjcoZjOxAAAuItwYzNqbgAAcBfhxiaMlgIAIDgQbmxS6vWWPzKBJgAAbiLc2FWQjAEHACAoEG5sFhtFkQIA4CauxDahMQoAgOBAuLEbw6UAAHAV4QYAAIQUwo1NrPKx4FTcAADgLsINAAAIKUERbubOnSvt27eX+Ph46du3r2zcuLHafRcsWCADBw6Upk2bmmXw4ME17g8AAMKL6+FmyZIlMnHiRJk+fbps3rxZunfvLunp6XLgwIEq98/IyJCbbrpJPvzwQ9mwYYOkpaXJlVdeKXv37pVguEMxt7sBAMBdEZavs4hLtKbmZz/7mbz44otm3ev1msBy9913y+TJk097vMfjMTU4evzIkSNPu39ubq4kJSVJTk6OJCYmil32HC6QgU9/KPExkfLNE1fb9r4AAEDqdP12teamuLhYNm3aZJqWKk4oMtKsa61MbRQUFEhJSYk0a9asyteLiopMgVReAABA6HI13Bw6dMjUvLRo0cJvu65nZWXV6j0mTZokrVq18gtIlc2cOdMkPd+itUJOimC8FAAA4d3n5mzMmjVLFi9eLMuXLzedkasyZcoUU4XlW/bs2RPw8wQAAIETLS5KTk6WqKgoyc7O9tuu66mpqTUe++yzz5pw8/7770u3bt2q3S8uLs4sTnO35xIAAAiKmpvY2Fjp1auXrF27tmKbdijW9X79+lV73NNPPy1PPPGErF69Wnr37i3BhNFSAACEcc2N0mHgo0aNMiGlT58+MmfOHMnPz5fRo0eb13UEVOvWrU3fGfXf//3fMm3aNHnzzTfNvXF8fXMaNWpkFgAAEN5cDzfDhg2TgwcPmsCiQaVHjx6mRsbXyTgzM9OMoPKZN2+eGWV14403+r2P3ifn0UcfFbdY5fOCM/0CAABhfp+bQHPqPjff/5gvlz2TIQ1jo+Srx6+y7X0BAIDUn/vcAAAA2I1wY/v0CzRMAQDgJsINAAAIKYQbAAAQUgg3NvH1yqZRCgAAdxFuAABASCHcAACAkEK4sUnF7YJolwIAwFWEGwAAEFIINzahQzEAAMGBcAMAAEIK4QYAAIQUwo1NmH4BAIDgQLgBAAAhhXADAABCCuHG5vFSTAoOAIC7CDcAACCkEG4AAEBIIdzYhNkXAAAIDoQbAAAQUgg3NougRzEAAK4i3Ng8txQAAHAX4QYAAIQUwo3NIux+QwAAUCeEG5tHSwEAAHcRbgAAQEgh3NjEYvoFAACCAuEGAACEFMINAAAIKYQb2zsUM14KAAA3EW4AAEBIIdzYjNkXAABwF+HGJtznBgCA4EC4AQAAIYVwYzO6EwMA4C7Cjc038QMAAO4i3AAAgJBCuLEZo6UAAHAX4cYmjJYCACA4EG4AAEBIIdzYLILxUgAAuIpwAwAAQgrhxmZ0KAYAwF2EG5vQoRgAgOAQ7fYJAABqZlmWlJaWisfjoagQ0mJiYiQqKuqs34dwYzOmXwBgp+LiYtm/f78UFBRQsAh5ERER0qZNG2nUqNFZvQ/hxiZMvwDAbl6vV3bt2mX+JduqVSuJjY01X/5AqNZQHjx4UH744Qf5yU9+clY1OIQbAAjiWhsNOGlpaZKQkOD26QCOa968uezevVtKSkrOKtzQodhm/KsKgN0iI/mqRniIsKlmkv9jbMJoKQAAggPhBgAAhBTCDQAACCmEG5tYdr0RAIRAv4malkcfffSs3vudd96p9f6///3vTcfUt95665TXbr31Vrn++utP2Z6RkWE+5+jRo2Z90aJFFeeu/Z9atmwpw4YNk8zMzFOO/eqrr+S3v/2t6RgbFxcnnTp1kmnTplU5lP/zzz+X3/zmN9KiRQuJj483I4TGjBkj3377rdSFnsc111xjOp2npKTIAw88YO6LVJPNmzfLL37xC2nSpImcc845MnbsWDl27JjfPmvXrpX+/ftL48aNJTU1VSZNmlTt++7YscPsp+93cnn8+te/lvbt25vymzNnjgQC4cZmjNIEEO70vjy+RS9miYmJftvuv//+gJyHBorFixfLgw8+KAsXLjyr9/L9DHv37pW///3vsm3bNhNMKvvkk0+kb9++ZpTbypUrTUh56qmnTDjSIKHbfd599125+OKLpaioSN544w3ZunWr/PWvf5WkpCSZOnVqrc9Lb+yowUbfe/369fL666+bz9NAVZ19+/bJ4MGDpWPHjvLpp5/K6tWrTQjRsOfz73//W4YMGSJXXXWVCWFLliyRFStWyOTJk095Px3ZdNNNN8nAgQOr/B2cd955MmvWLBOQAsYKMzk5OVrJYh7t9HnmEavdpHetAbPW2vq+AMJXYWGh9fXXX5tHH6/Xa+UXlbiy6GfX1WuvvWYlJSX5bVuwYIHVpUsXKy4uzurcubM1d+7citeKioqscePGWampqeb1tm3bWjNmzDCvtWvXznx/+xZdr8miRYusiy++2Dp69KiVkJBgZWZm+r0+atQo67rrrjvluA8//NC8/5EjR6r9GV544QW/a4mWzfnnn2/17t3b8ng8fvtu2bLFioiIsGbNmmXW8/PzreTkZOv666+v8rx9n1sbq1atsiIjI62srKyKbfPmzbMSExNNWVblpZdeslJSUvzO8z//+Y/5ebZv327Wp0yZYn6WylasWGHFx8dbubm5ftsffPBB65ZbbqmynCrT39fzzz9f57/5M7l+c58b+0KiXW8FANUqLPHI+dPWuFJCXz+eLgmxZ3fZ0FoKrVV48cUX5aKLLjK1AtoU07BhQxk1apS88MILpoZg6dKl0rZtW9mzZ49Z1L/+9S/T7PLaa6+ZGoXT3Qfl1VdflVtuucXUhlx99dWmRqMutSLVOXDggCxfvtx8vu8ctmzZIl9//bW8+eabpwzd7969u6kp+dvf/maadtasWSOHDh0yNUpVqdy0o805WqNSXVPehg0b5MILLzRNWz7p6ely5513mtoYLeOTaW2R3hCy8nk2aNDAPK5bt87U6Og+2lRWme5z/Phx2bRpk/z85z832z744APT5Kc//7JlyyRYBEWz1Ny5c80vUAtSq/Q2btxY4/5akF26dDH76y911apVEixolgKA6k2fPl2ee+45ueGGG+Tcc881jxMmTJCXXnqpov+I9j255JJLpF27duZRmzyU9mPxXfy1icO3XpXt27ebZiLtG6M05GgoOtN/iObk5JgpATSEaZD48MMPZdy4cWZd+frJdO3atcrjdbtvHz03pdex0+nQoYMkJydX+3pWVpZfsFEtytf1tapcfvnl5rVnnnnGNGcdOXKkorlJm958AUmbuTSQadOXNsc9/vjjfvv8+OOPJnhpaNRmu2Dies2NtuNNnDhR5s+fb4KNts9qoWp7pib0k2lh6x/6zJkz5Ze//KVJydohTDtHXXDBBa78DAAQKA1iokwNiluffTby8/Plu+++k9tuu83U1vhoJ1WtXVF6sdT+KZ07dza1M/o9f+WVV9b5s7SPjV5LfMFA+4/o52pNwxVXXFHn99POsnqd0f4l//u//2tqoLQ/zclqE57qErC0U6/dfvrTn5q+OXrtnTJliql9+sMf/mBCka82R8tcw88dd9whI0aMMJ2jtdbr448/rthHf4fDhw+XSy+9VIKN6zU3s2fPNgU0evRoOf/8803I0R7f1XX++uMf/2j+4LU3uCbhJ554Qnr27GmqON1EoxSAQNARJ9o05MZytneP9Y3GWbBggWnG8C1ffvmlqWVR+n2u82npd3thYaEZeXTjjTfW6XO0pkEv3tqpNzo62ix6XTl8+LDftUVrG7RG5mQ6Skov+L5aGaUXdG2u0euOhgLtDKxNPz46Kkppx+Cq6HbfPr7Hb775Rs6W1mBlZ2f7bcsuX6+pA6+GEq290RoZrYHRZi+d10k7//roz6llobVp2ox23XXXme2+fTQoPvvssxVlrOFRy1Ofn20H7nodbrQ6TNvutC2y4oQiI826tiNWRbdX3l9pOq9uf203zM3N9VucFMG84ABQJa0Z0AlAd+7caYJC5UWbqCqHDm1O0hCktfs6OkmDiYqJiTHhpSbaVSEvL8/056kcorSJRfuF+IZ4a+2Q9kvR60RlWkOj56OfVR1txtFz031Vjx49TDPT888/b+YDq0xHHr3//vsVzWtaK6I1Sk8//XSV7+07v9ro16+ffPHFF6YfkM97771nylArDGrzO9HmNv1ZtKuH1ppVpoFWf2fa30bLT+c50wCq9LpbuXy12UpruPT5r371KwnbcKNJUP9Iq2ovrK6tsLr2xer21+Yrre70LfqLcUJkRITEx0RKXLTrlWEAELQee+wx872sHYe1D4pemLUvjNbiK33Ui6jWaujr2sdSayB8nWy1f6Y21eh3vvYVqa4jsQ6P1o682l3Bt2gtkL6PNimpm2++2Vy8R44caf6hrfdq0RoH7R5x33331fhz6LVEL+C+Idf6Pvq52qlY7+uifUe1xkPPf+jQoSaE3HvvvWZfrRF65ZVXTM3Stddea4KPThb52WefmU7G2hTko01oNbVMaFDSEKNNRxqitLPyI488YvoDaVOS0nPR4KW1ND76nhrMtIy13+v48ePN76VyZ2ZtltLfjwZArUnT4dz6e/N1otZarMrl27p1a1NBoc+bNm1aUYnhCz/6XM9Bn2tZO8py0d69e82wrvXr1/ttf+CBB6w+ffpUeUxMTIz15ptv+m3TYYQ6rK0qx48fN8PGfMuePXscGQoOAHaraVhsfVHV8OA33njD6tGjhxUbG2s1bdrUuvTSS61ly5aZ115++WXzWsOGDc1w5iuuuMLavHmz33Dkjh07WtHR0VUOBdch0fra0qVLqzyfO++807rooosq1rdt22b96le/slq1amU+s3v37maoeuVh79UNcd6wYYO5nnz66ad+Q6p//etfW82aNTPXqw4dOliPPPKIGf59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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Now test the model without early stopping criteria\n", + "test_pred = mytools.test_clas(test_loader, final_classifier, device)\n", + "test_pred=torch.sigmoid(torch.tensor(test_pred)).numpy()\n", + "\n", + "\n", + "fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_test, test_pred, pos_label=1)\n", + "auc_test = sklearn.metrics.auc(fpr, tpr)\n", + "print(f'Test AUROC: {auc_test:0.4f}')\n", + "\n", + "plt.plot(fpr, tpr, label=f'Test AUROC: {auc_test:0.4f}')\n", + "plt.xlabel('FPR')\n", + "plt.ylabel('TPR')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "e836dc52", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df_test[\"Class_adv\"] = test_pred\n", + "df_bkg = df_test.loc[ (df_test.PhiKK == 0)]\n", + "per = np.percentile(df_bkg['Class_adv'],99.999)\n", + "df_cut = df_bkg[df_bkg['Class_adv']>per].reset_index(drop=True)\n", + "x_vals = 1000*df_cut.InvM\n", + "\n", + "plt.figure()\n", + "plt.hist(1000*df_bkg.InvM, bins=np.arange(40,300,1), histtype='step', label='No selection', density=True)\n", + "plt.hist(x_vals, bins=np.arange(40,300,1), histtype='step', label= 'selection', density=True)\n", + "#plt.axvline(1019.461, color='k', linestyle='dashed', linewidth=1, label='PDG phi mass')\n", + "plt.xlabel('Invariant Mass [MeV]')\n", + "plt.ylabel('Normalized Counts')\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "5556d9cd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "class 0 test auc score: 0.5\n", + "class 1 test auc score: 0.5\n", + "class 2 test auc score: 0.5\n", + "class 3 test auc score: 0.5\n", + "class 4 test auc score: 0.5\n", + "class 5 test auc score: 0.5\n", + "class 6 test auc score: 0.5\n", + "class 7 test auc score: 0.5\n", + "class 8 test auc score: 0.5\n", + "class 9 test auc score: 0.5\n", + "average score: 0.5\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "adv_test_pred = mytools.test_adv(test_adv_loader, final_classifier, final_adv, Num_classes, device)\n", + "adv_test_pred = torch.softmax(torch.tensor(adv_test_pred),dim=1).numpy()\n", + "score=0\n", + "\n", + "for i in range(adv_test_pred.shape[1]):\n", + " fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_adv_test[:,i], adv_test_pred[:,i], pos_label=1)\n", + " auc_test = sklearn.metrics.auc(fpr, tpr)\n", + " print(\"class \" + str(i)+ \" test auc score: \" + str(auc_test))\n", + "\n", + " plt.plot(fpr, tpr, label='Class '+str(i)+' Test AUROC: '+str(auc_test))\n", + " plt.xlabel('FPR')\n", + " plt.ylabel('TPR')\n", + " plt.legend()\n", + "\n", + " score += auc_test\n", + "\n", + "print(\"average score: \", score/adv_test_pred.shape[1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e25e4fde", + "metadata": {}, + "outputs": [], + "source": [ + "# Save the adversarially trained models\n", + "torch.save(final_clas_adv.state_dict(), \"classifier_adv_2021_v9_pass5_run\"+str(run)+\"QualCuts_\"+str(MASS)+\"_v3.pt\")\n", + "torch.save(final_adv_adv.state_dict(), \"adversary_adv_2021_v9_pass5_run\"+str(run)+\"QualCuts_\"+str(MASS)+\"_v3.pt\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86e5bdb4", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data vs. data-like MC diagnostic\n", + "\n", + "Score histograms and ROC curve showing how well the final classifier\n", + "separates real data from data-like MC. Ideally these should be similar." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHcCAYAAAA6I8WuAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAWlhJREFUeJzt3QeYU1X6x/GXDkoXaUqRqlhAKS6of0VQFFcBG66IWLGABVzBhhQLKlKURVlRQF0VFBBZUVAQxcJKExuKNBGVIiIgIDMw5P/8jnuzmUxmJplJZpK538/zBCbJTXJyU+6b97znnGKBQCBgAAAAPlS8sBsAAABQWAiEAACAbxEIAQAA3yIQAgAAvkUgBAAAfItACAAA+BaBEAAA8C0CIQAA4FsEQgAAwLcIhIAEOOOMM9zJ8/3331uxYsVs8uTJCd/fegw9lh7TU79+ffvrX/9qBeH99993j6//k1lBviapaMiQIW7/JJNI762rrrrKvb/DX9fHH3+8kFqJVEMghAL35Zdf2sUXX2z16tWzsmXL2hFHHGFnnXWWjR07llcjzFNPPZW0B+pkbluiffLJJy5Q2LFjR2E3JSkV9feG92NDp48++ijL9Vq5qk6dOu76SD9A9u3bZ6NHj7aTTz7ZKlWq5L4HmzRpYn379rXvvvuugJ4FPCWDfwEFdABp37691a1b166//nqrWbOmbdy40f7zn//YE088YbfcckuRfB0U9P3xxx9WqlSpmA8o1apVc796o9WzZ0+77LLLrEyZMnloaf7b9n//93/uuZYuXdqK8vt46NCh7rlXrly5sJuTdPLyvs2rCRMm2MGDB60wKIB5+eWX7dRTT810+QcffGA//vhjxM/gtm3b7JxzzrFly5a5IOnyyy+38uXL26pVq2zKlCn2zDPPWHp6egE+CxAIoUA99NBD7hfQkiVLshxAtm7dWqBt2bt3rx1yyCEF8lj6ZagvzUTas2ePHXrooVaiRAl3KizFixdP+HMFPLH+uIinzp0722uvvWZPPvmklSz5v8OpgqOWLVu6oCecgsPPPvvMpk2bZhdddFGm6x544AG79957C6Tt+B+6xlCg1q5da8cee2zEX9HVq1fPctm//vUva9OmjQtYqlSp4rIN77zzTpZfn7pP/fqqXbu29enTJ0uXhep1jjvuOPcrTPeh+7vnnnvcdWlpaTZ48GBr1KiRuw+ltAcMGOAuj4Z+wTVs2NDKlSvn2vrhhx9GVY+yefNmu/rqq+3II490j1urVi3r0qVLsLZHdQ9ff/21+3XppeG9uiMvNa/rbr75ZrfvdD/Z1Qh5tO9atGjhApVmzZrZjBkzoqoLCb/PnNqWXY2QDhg6OGg/KVtwxRVX2E8//ZTlIKFfx7q8a9eu7u/DDz/c/v73v1tGRkambTdt2mTffvut7d+/P9fXSO8H3beCcL33evXqFbFb64svvnDbNWjQwO0jZSyvueYa+/XXXzPtozvvvNP9fdRRRwWfv7dvJk2aZGeeeaZ7TfS6aj8//fTTubZRNS26nw0bNmS57u6773YZtt9++82dX716tTuIqn1qp157ZQF37txpeaHundatW7v70nv5n//8Z8TtonluOb03tm/f7l7L448/3r22FStWtHPPPdc+//xzy6vwGqFI1FXVu3dvtw9D3/P6fvHek1WrVnX7UBnqaP3tb39z74133303eJmyOQpylOkJ9+mnn9rs2bPt2muvzRIEifYptU0Fj4wQCryLaNGiRfbVV1+5wCQn6nrQQaddu3Y2bNgw9yWmL5L33nvPzj77bLeNrtd2HTt2tJtuusmll/XFrIzTxx9/nOnXor6w9KWrLzsdhGvUqOFS6hdccIE7EOiL8phjjnE1TOq/V1/9zJkzc2zjc889ZzfccINr4+23327r1q1z96cvVQVUOdEXoQ4Y6g7UF7kyYvpC/eGHH9z5MWPGuOt0wPB+JarNoRQEKVC4//77XUYoJzp4du/e3W688UYXCOigdskll9icOXNcjVYsomlbeCCloE8H2+HDh9uWLVtcV6heI/06Dg2MFfB06tTJ1U/ooDBv3jwbOXKkO0DrNQ4NDp5//nlbv359jgdCHQQVYOo11nPXa/z666+7fRBO+1+vodqqIEOvjwJd/a/uWx3UL7zwQvfeeOWVV9z7REGd6HUQvf8UmOt9oCzBv//9b/c66b2mID07l156qQvAX3311WCg5dFles/rx4AOtNo/CtT1GqidChzffPNNF9wp2IuF3u+6b7Vfn6cDBw64HwaRXs9onltO7w3tW32m9L5TEKn3gYKu008/3VauXOl+yMSb3k8KZqdOnepe9/POOy+YnR40aJDb79ddd5398ssvrk5RP5TC35PZ0fuubdu27r2g7xZ5++23XUCq7xll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91rlypXd+R07dtghhxxi1atXJxACAABFq2ts/fr1wZMmUmzRooV98803tn37dnfS3yeddFJwgkUUMK3ZktspEOBlAQAgv/MIDRo0yK0y37Rp0+Bl+ltZo4svvth69OiRl7tFfkSzcN24mUmztgsAAClbLL1p0yY7cOBAlsszMjJsy5Yt8WgXAABAcmaEOnToYDfccIM9++yzrjtMli1b5pbc6NixY7zbiOyULvNnlicn6hKLJlsEAIAP5SkjNHHiRKtZs6a1atXKypQp405t2rSxGjVquOAIBaRYsT+7unI7AQCA+GWEtL7YW2+9Zd999519++237rKjjz7amjRpkpe7AwAASL1FV+vXr2+BQMAaNmxoJUvme/1WAACA5O8a0/xB1157rZs36Nhjj7UffvjBXX7LLbfYI488Eu82AgAAJE8gdPfdd9vnn39u77//vpUt+78aFBVKT5061ZLJm2++6Yb2N27cmPolAACQSZ76s2bOnOkCnr/85S9WTAW7/6Xs0Nq1ay1ZaIh///79bcGCBVapUiVr2bKldevWzQ477LDCbhoAAEjVjJCW19BSGuH27NmTKTAqbIsXL3bB2RFHHGHly5e3c8891955553CbhYAAEjlQEjD5mfPnh087wU/Gjrftm3buDVu4cKFdv7551vt2rXdYygTFW7cuHGuaFtddCeffLILfjw///yzC4I8+vunn36KW/sAAIAPu8Yefvhhl11ZuXKl63564okn3N+ffPKJffDBB3FrnDJMzZs3t2uuucYuvPDCLNere05dX+PHj3dB0JgxY6xTp062atWqiBmrVLVvf0au25QpVSKpsnEAABTZQOjUU0+1FStWuBFixx9/vOtu0gzTixYtcufjRcGWTtkZNWqUXX/99Xb11Ve78wqIlKnShI933XWXyySFZoD0tyZ+TDXdR83LdZs3BnaysqWZwgAAgFjk+cipuYMmTJhghSU9Pd0t66ERbJ7ixYu7kWsKyERBz1dffeUCIBVLv/32227B2OykpaW5k2fXrl0JfhYAACDlAqHly5dbqVKlgtmfN954wyZNmmTNmjWzIUOGWOnSpS3Rtm3b5hZ51bIeoXTem+1akzyOHDnS2rdvbwcPHrQBAwbkOGJs+PDhNnToUEsG6upSlie3LrNoskUAACCOxdJacFXLa8i6deuse/fubnLF1157zQUb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XVGLFy+2iy66yJo0aWLHHnus9e/f3/7zn/9ke7uBAwe6bQ855BCXSRo0aJALLjyff/65tW/f3ipUqGAVK1a0li1b2tKlS911GzZscBmnKlWq2KGHHuoe76233srSNaa/dXs588wz3eW6LFLX2BtvvGEnnXSSlS1b1rVn6NChduDAgeD1uu3TTz9tF1xwgXvMhx56KOLzeuqpp6xx48bufmrUqOEyUZ6DBw/aY4895rrplDmrW7dupvv58ssvXTsVpB122GHWu3dvF8h5rrrqKuvatau7jYK6pk2busuV6br00kvdc6patap16dLFBagAYkdGCEDUtm/fbnPmzHEHZgUH4XKqw1GAooBEB3QFANdff727bMCAAe76Hj162IknnuiCD9X2rFixIpiBUaZJ2ZCFCxe6x125cqWVL18+YjfZqlWrXMAwffp0d16BQniQ8OGHH9qVV15pTz75pJ122mm2du1aF4TI4MGDg9sNGTLEHnnkERszZoyVLJn161KB2q233movvviieyztH9235+6777YJEybY6NGj7dRTT3VdeF7GbM+ePS5r1bZtW1uyZInLYl133XXWt29ft5886m5UYPjuu++68woevdvpsdSuBx980M455xz74osvyBgBsUr4+vYAioxPP/00oK+NGTNm5Lqttnv99dezvX7EiBGBli1bBs9XqFAhMHny5IjbHn/88YEhQ4ZEvG7BggXusX777Td3Xv/rvC73TJo0KVCpUqXg+Q4dOgQefvjhTPfz4osvBmrVqpWp/bfffnuOz3H69OmBihUrBnbt2pXlOl1WpkyZwIQJEyLe9plnnglUqVIlsHv37uBls2fPDhQvXjywefNmd75Xr16BGjVqBNLS0jK1s2nTpoGDBw8GL9P15cqVC8ydOzfH9gLIiowQgFh+OOV5b02dOtVlYJR9UfePuqGU6fCoa00ZEWVXOnbsaJdccok1bNjQXaesy0033WTvvPOOu05dcieccEKe26JuuI8//jhTN1VGRobt27fP9u7d67rvpFWrVjnez1lnnWX16tVzXWvKyOjUrVs3d/tvvvnG0tLSrEOHDhFvq+ubN2+eKbOm2it1pymrpW42UQ2W6qBC275mzZpgF6BHbde+BRAbaoQARE21MKqdibUgetGiRa7rq3Pnzvbmm2/aZ599Zvfee6/r7grthvr666/tvPPOs/fee8+aNWtmr7/+urtOAdK6deusZ8+erltNAcrYsWPz/MopEFNNkLrfvJPud/Xq1a7WxxOp+y+UgpHly5fbK6+8YrVq1bL777/fBTeqV8pvcXZ2bVDbVT8V2nadNCru8ssvj8tjAn5CIAQgaqq3UX3KuHHjXI1LuNC5fEJ98sknLnOi4EdBjAIqFUCHUzF1v379XObnwgsvtEmTJgWv0xD9G2+80WbMmGF33HGHq73JKxVJK+uiIubwU/HisX0tqkZHWSoVRatGR/VICuT0HBUMZTelwDHHHOOyO6H7UVkqPb5XFJ1d2xWwVa9ePUvbQ6cQABAdAiEAMVEQpG6kNm3auIJkHZTVzaNuLxXwRqKg4IcffrApU6a47htt62V75I8//nBFwhrhpQBJAYEKiBUsiCZF1PD89evXuwzMggULgtflhTI3L7zwgssKKQul9qtt9913X0z3o+yWnosyMmq37lNdWwpklFnSSDkVg+tyPW+NqnvuuefcbZUh0za9evWyr776yj0nzc2krJfXLRaJbletWjU3UkzF0ton2m/qPvzxxx/zvE8A34pQNwQAOfr5558Dffr0CdSrVy9QunTpwBFHHBG44IILMhUohxdL33nnnYHDDjssUL58+UD37t0Do0ePDhYwq9j3sssuC9SpU8fdX+3atQN9+/YN/PHHH+56/d2wYUNXfHz44YcHevbsGdi2bVuei6Vlzpw5gXbt2rkiYxU8t2nTxhUwZ9f+SD788MPA6aef7oqedT8nnHBCYOrUqcHrMzIyAg8++KDbT6VKlQrUrVs3U5H2F198EWjfvn2gbNmygapVqwauv/76wO+//x68XsXSXbp0yfK4mzZtClx55ZWBatWquX3SoEEDd9udO3fyzgViVEz/FHYwBgAAUBjoGgMAAL5FIAQAAHyLQAgAAPgWgRAAAPAtAiEAAOBbBEIAAMC3CIQAAIBvEQgBAADfIhACAAC+RSAEAAB8i0AIAAD4FoEQAAAwv/p/PpAKgmHkxrgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data-vs-MC AUROC (ideally ~0.5): 0.7487\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Build a test set of real data background vs. data-like MC background\n", + "# to check whether the classifier has learned MC/data artefacts.\n", + "df_mc_bkg_test_diag = df_mc_bkg_test.copy()\n", + "df_data_bkg_test_diag = df_bigpreselectblind_test.copy()\n", + "df_mc_bkg_test_diag ['MC_label'] = 1.0\n", + "df_data_bkg_test_diag['MC_label'] = 0.0\n", + "\n", + "df_dm_diag = pd.concat([df_data_bkg_test_diag, df_mc_bkg_test_diag],\n", + " ignore_index=True, sort=False)\n", + "X_dm_diag = df_dm_diag.drop(columns=['InvM','PhiKK','MC_label'])\n", + "y_dm_diag = df_dm_diag['MC_label'].to_numpy()\n", + "X_dm_diag = pd.DataFrame(scaler.transform(X_dm_diag),\n", + " columns=X_dm_diag.columns, index=X_dm_diag.index)\n", + "\n", + "dm_diag_loader = DataLoader(\n", + " TensorDataset(torch.from_numpy(X_dm_diag.to_numpy().astype(np.float32))),\n", + " batch_size=bsize, shuffle=False)\n", + "\n", + "scores_diag = mytools.test_clas(dm_diag_loader, final_clas_adv, device)\n", + "scores_diag = torch.sigmoid(torch.tensor(scores_diag)).numpy()\n", + "\n", + "scores_data_diag = scores_diag[y_dm_diag == 0]\n", + "scores_mc_diag = scores_diag[y_dm_diag == 1]\n", + "\n", + "# Unit-normalised score histograms\n", + "bins = np.linspace(0, 1, 51)\n", + "plt.figure()\n", + "plt.hist(scores_data_diag, bins=bins, histtype='step', density=True,\n", + " label='Real data', color='steelblue', linewidth=1.5)\n", + "plt.hist(scores_mc_diag, bins=bins, histtype='step', density=True,\n", + " label='Data-like MC (wab+tritrig)', color='tomato', linewidth=1.5)\n", + "plt.xlabel('Classifier score')\n", + "plt.ylabel('Normalised counts (a.u.)')\n", + "plt.yscale('log')\n", + "plt.title('Score distribution: data vs. data-like MC\\n(final_clas_adv)')\n", + "plt.legend()\n", + "plt.show()\n", + "\n", + "# ROC curve for data vs. MC (diagnostic — should be close to random)\n", + "fpr_dm, tpr_dm, _ = sklearn.metrics.roc_curve(y_dm_diag, scores_diag, pos_label=1)\n", + "auc_dm = sklearn.metrics.auc(fpr_dm, tpr_dm)\n", + "print(f'Data-vs-MC AUROC (ideally ~0.5): {auc_dm:.4f}')\n", + "plt.figure()\n", + "plt.plot(fpr_dm, tpr_dm, label=f'Data vs MC AUROC: {auc_dm:.4f}')\n", + "plt.plot([0,1],[0,1],'k--', label='Random')\n", + "plt.xlabel('FPR')\n", + "plt.ylabel('TPR')\n", + "plt.title('ROC – data vs. data-like MC (diagnostic)')\n", + "plt.legend()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "9349eb4c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnAAAAKFCAYAAABMTNQJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAc2JJREFUeJzt3QeYE+X69/Gbtrv0IkpRECmigAKCKJaDKIrCsaAiHlGxN7CABVGpFrBQFFFsgHpUUJocCyhYsKA08SAISlFQKVKkyi4sea/fc/7ZN7ub3U12k00m+X6uK7CZTCZPZiaZO/fTSvh8Pp8BAADAM0rGugAAAAAIDwEcAACAxxDAAQAAeAwBHAAAgMcQwAEAAHgMARwAAIDHEMABAAB4DAEcAACAxxDAAQAAeAwBHOAxZ5xxhrv5/fLLL1aiRAmbMGFC1F9br6HX0mv61atXz/75z39acfjss8/c6+v/eFacx8SLBg0a5PZPPAl2bl1zzTXu/M55XJ966qkYlRL4/wjgkFCWLl1ql156qR155JGWlpZmhx9+uJ199tk2evToWBct7jz33HNxG2DEc9mi7euvv3YBzl9//RXrosSlRD83/D+SdPvyyy9zPa7ZL+vUqeMeD/bDad++fTZy5Eg76aSTrHLlyu578Oijj7ZevXrZTz/9VEzvAsWhdLG8ClBMF7727dtb3bp17cYbb7SaNWva+vXr7ZtvvrGnn37abr/99oQ8DgpW//77bytTpkzYF8Lq1au7LEOorrrqKrv88sstNTW1ECUtetn+8Y9/uPeakpJiiXweDx482L33KlWqxLo4cacw521hvfTSS3bw4EGLBQVeb775pp122mnZln/++ef222+/Bf0Mbtmyxc4991xbtGiRC+6uuOIKq1Chgq1cudImTpxoL774omVkZBTju0A0EcAhYTz66KPuF+eCBQtyXfg2b95crGXZu3evlStXrlheS7/E9WUfTXv27LHy5ctbqVKl3C1WSpYsGfX3CviF+6Mokjp16mTvvPOOPfPMM1a69P+/VCuoa9WqlQvWclJQ+91339nkyZPtkksuyfbYww8/bA8++GCxlB3FgypUJIzVq1db06ZNg2YtDjvssFzL/v3vf1ubNm1coFW1alWX3fnoo49y/drXNvVrt3bt2tazZ89cVVtqj9asWTP3q1fb0PYeeOAB91h6eroNHDjQGjZs6Lahqo/77rvPLQ+FfjE3aNDAypYt68r6xRdfhNTeauPGjXbttdfaEUcc4V63Vq1aduGFF2a1XVO7nmXLlrlf8/7qGn+7On8Vjh677bbb3L7TdvJqA+enfdeiRQsXYDVp0sSmTp0aUrunnNvMr2x5tYHThU4XNe0nZWeuvPJK+/3333Nd3JSN0PKLLrrI/X3ooYfaPffcY5mZmdnW3bBhg61YscL2799f4DHS+aBt68eDzr0ePXoErf7873//69arX7++20fKEF933XW2devWbPvo3nvvdX8fddRRWe/fv2/Gjx9vZ555pjsmOq7az88//3yBZVSbLW3n119/zfVYv379XEZz+/bt7v7PP//sLv4qn8qpY6+s644dO6wwVA144oknum3pXH7hhReCrhfKe8vv3Ni2bZs7lscdd5w7tpUqVbLzzjvPvv/+eyusnG3gglGV5k033eT2YeA5r+8X/zlZrVo1tw9VIxCqf/3rX+7c+Pjjj7OWKXum4EyZtZy+/fZbe//99+3666/PFbyJ9ilt9xILGTgkVFXivHnz7IcffnABVX5URaWL5SmnnGJDhgxxX776Avzkk0/snHPOcevoca3XoUMHu/XWW101hC4oyvB99dVX2X6d64tWFwt9SSt4qFGjhqt6ueCCC9wFTF/wxx57rGujp/Ypaosyffr0fMv4yiuv2M033+zKeNddd9maNWvc9nQxUCCYH32B60KnamNdgJSB1IVg3bp17v6oUaPcY7rQ+X+Vq8yBFLwpwBkwYIDLwOVHF/1u3brZLbfc4gIYXYy7du1qM2fOdG0QwxFK2XIGgApWFSQMHTrUNm3a5KrMdYyUjQgM6BWodezY0bUP0sVs9uzZNnz4cBdY6BgHBjWvvvqqrV27Nt8LuC7eCox1jPXedYynTZvm9kFO2v86hiqrgiMdHwXo+l/V/ApGLr74YnduvPXWW+48UTAqOg6i808/KHQeKCvzn//8xx0nnWv6cZGXyy67zP1wePvtt7MCRD8t0zmvHzEKELR/9ANDx0DlVMD73nvvuaBUQWo4dL5r2yq/Pk8HDhxwP2iCHc9Q3lt+54b2rT5TOu8U/Oo8ULDYrl07W758ufsBFmk6nxSET5o0yR33zp07Z9UG9O/f3+33G264wf7880/XDlc/8HKek3nRede2bVt3Lui7RT788EMXSOt7Rpm5QDNmzMhq5oAk4QMSxEcffeQrVaqUu7Vt29Z33333+WbNmuXLyMjItt7PP//sK1mypK9Lly6+zMzMbI8dPHjQ/b9582ZfSkqK75xzzsm2zrPPPuvTx2bcuHFZy9q1a+eWjR07Ntu2Xn/9dfc6X3zxRbblWk/rf/XVV3m+F5X5sMMO87Vo0cKXnp6etfzFF190z9Vr+q1du9YtGz9+vLu/fft2d//JJ5/Md381bdo023b8tB09/7TTTvMdOHAg6GN6Tb8jjzzSLZsyZUrWsh07dvhq1arla9myZdaygQMHuvXyer3AbeZVtk8//dStq/8D91OzZs18f//9d9Z67733nltvwIABWct69Ojhlg0ZMiTbNlXGVq1aZVvmXzewTMFMnz7drffEE09kLdM+O/3007MdE9m7d2+u57/11ltuvblz52Yt03HL67WDbaNjx46++vXr+wqiz0TO9zl//nz3Wq+99pq7/91337n777zzji8SLrroIl9aWprv119/zVq2fPly9xnNeS6E+t7yOjf27duX6/OsfZiamprrmAeT89zynwc6vwO35/9s7d+/39etWzdf2bJl3feM3y+//OLe36OPPppt+0uXLvWVLl061/K8Pg8LFixw3zcVK1bM2jddu3b1tW/f3v2tcnXu3Dnrefo+0/P0+UdyoAoVCUOZHmXg9Ate1SZPPPGEyyaoJ6r/16noV7p+1SuzpDZVgfxVfMrMKBuhzFfgOuocoaoZVVXkrJ5QZiVntZ4yMsccc4xrr+K/qZpIPv300zzfy8KFC13WTFmdwAb7/qq6/KjKRs9RNaO/Wqww9F5Dbe+m7EaXLl2y7msfXX311S7boOrcaPHvJ2VqAtvGKROi/Z7zOIn2aaDTTz/dZW9yZvWUXSuo+uyDDz5w2aLA7J32WbAOMzougT0FdS6cfPLJ7v7ixYtDer+B21AmRttQhknlL6iKUxlSVfOrqYGfMkc6d5VFFP+5NWvWLNeOs6jZKW1H1dXqWOSnz4Q+l5F8b6L34f+s6rWVFVemrnHjxiHv31Dpu0GZPmUmdQ74s/aialR9vyj7Fvi5VzazUaNG+X7uc9I21GlHr7Nr1y73f7DqU9m5c6f7v2LFihF4h/ACAjgkFFWj6QtUgcv8+fNdVZi++DS0iKpRRBcwfdGrjU1e/G2F9OUfSIGR2jDlbEukIDFnz0hVK6p6TNVHgTd16S+oY4V/+/rCD6RqW71+QReyxx9/3FW3qHpJ1TYKZsMNpFQNFSq18cvZvs3/PoO1l4uUvI6TKIDLeZwU5PmrI/1UdVjYQFfbV/tCBQqBgpVHbbTuvPNOd0wUrKgc/n0cavsyVQurSl8dSlQNp23421sWtA0FHDrvFbSJAlT9yFD1nAJuUXn69OljL7/8squ+VaA1ZsyYQrV/U7Whgo+c53Be+6co700UNKnaWa+nz4DKr22o7WHg8/U5CLypjOFSVb1+CKo9WuCYjP7PvfatypHzs//jjz+G1aFKz9E+UccFfa8pMNV3WTD+Y6jvOyQH2sAhISmYUjCnmwIJZcd0sVL7m2gIzB4EXlDUoHrEiBFBn1NQO7aiUObw/PPPdxcZZUHUHkcXHbXxa9myZaHfU1HkNXBrzg4E0RTLHrTKpmiIELVBU2cPBX06RzTsQyhDVeiHx1lnneUCU51TOn90nisDpMCloG0oS6pso9q8KTBSuzu1iVSwH0htApXpfffdd13HlDvuuMOdO1rf35kl0or63uSxxx5z57napKnHpdqKKmDVZyHw+Qq4A6m9ZrhDkiiwVftO/TBSABeY/dVr6VzXD6hg51vOYL8gyrgpG65gU8F2Xu3ntO/87Q51nJH4COCQ8Fq3bp3Vs1DUYF1fssrI6UKaV4cIUceFwIyXqk7UsF2/igui11FVri5M4Y467399/Zr3V7mKekXq9Zs3bx7S6999993upu3overirN5xEsmR8FetWuWyDoHb9A8a6q+GVKZL1Bg+8CIUrGdkqGULPE6B+8m/zP94tGj7c+bMsd27d2e7MOu1AynDp/XUKUZV9346LqG+dzXqV+cCNQcIrJIMp0pO1aiqblb5lIlTj2kF+jnph4duDz30kAs6Tz31VBs7dqw98sgjYWWP9CMg2HvMuX/CeW957R9lwzQOpDr/BNL55u8MIoG9OkUdJ8Klqm9VxWusNWU21YHBP9SHPnf6LCib6c9CF4WaJqgzkwJof/Y0GB1HBdr6fBPAJQeqUJEw9GWvL86c9Cs+sNpGbXL0y1y9T3P+svc/XwGaMgDq6RW4TV0cVB3j721WUMZFPfg0GGhOqrbJr2engk5dAHXRDBx4U22zChqhX22X1MYqkC4qahsTOHyJqqoiNdr/H3/84S5ige1xXnvtNRc0qu2Pvwwyd+7crPW0D9TbM6dQy6b9pGEntJ8C35uyH6quCuU4BRPqMCIaq0s9KwOHu1BGMefMH/5MTM7zU70qg713yfn+g21D56IySKFS72RtRz0blZFWAOJ/Pf9x0/sJpEBOn5fA/avMnfZPfvQ6ylQpC6z1/XRclBUu7HvL69zQNnLuX73HnMPJ6LMdeMuZkQuVnqvBcZWJU89P/3eJehKrLArWc5ZH9wOHjQmFfhjo/FIv3mDBtp96rCqbq+rvYD3c9T2iYVaQOMjAIWGo4biCF/1iVXWCvrCUPdCvVmWB/J0M1F5LQxComkW/VPWFqzYzGh5E1Uz6FavgSe3n9CWsL0V1jFDWQOPCqVpWQ4UURF/qqq7SL3UFl8pi6OKuC5+W6yLmzw7mpLZuynbol7cyS8qcKPOmC1pBbeCU+VLWTwGk2vkpM6DgSsMqaPgBP41RpQuDXkf7RIFQzixWqJRp0PhT2odq4zVu3Dj3eoEXYDX0VnZF66kaURc5rad9HXiBD6ds2k+qAtSxVYN3jZ3lH0ZEx7x3796Fej+hDiOiC6qO6/333+/a+vnHv8vZZkvtk/xtERUUqs2kqie1/Zz03kXnqI6X3qNeR/tPPyr0t84LZf3040D7xp9dLojWVZZK1ZRqK6XzKpCq2DXlkrJKOqYK5l5//XV3rALHFlMHFY3FFuwHUyB9fhTg6HOmzJ+2p+BWWS+1TfML573ldW4oGNWPMp0LGnpHVYlvvPFGgZ+XotCPQZ3j2h86xhq2RD9UVDadQzontI5+POlY63OoIYXCDaSCDUsTjH40aV/qO037Ut8DCniVBVWwqX3JWHAJJNbdYIFI+fDDD33XXXed75hjjvFVqFDBDQPSsGFD3+233+7btGlTrvU1FIiGkNAwA1WrVnVDE3z88cfZ1lE3fm2vTJkyvho1avhuvfXWXN309TwNbRCMhrl4/PHH3eP+19FQDoMHD3ZDbRTkueee8x111FHuua1bt3bDTej18htGZMuWLb6ePXu6cpcvX95XuXJl30knneR7++23s21748aNbhgCDVMQODRJ4DAGOeU1jIi2o6EUjj/+eFdWvXawoSgWLVrkyqJjU7duXd+IESOCbjOvsgUb6kEmTZqUdSyrVavm6969u++3337Lto6GhND+yCnY8CahDiMiW7du9V111VW+SpUquX2tv/3DcQQOI6LyaKiHKlWquPU0JMQff/zh1lMZAj388MO+ww8/3A1DE1iOGTNmuH2soTnq1avnzi2dx6GWVV566SW3vvZt4NArsmbNGvcZatCggXsN7UsNWzF79uxs6/mHzgnF559/7s55HXMNCaJhdILt81DfW17nhoYRufvuu93wNRra49RTT/XNmzcv1+clUsOI5Pycavk999yTtUzD6mgoHp1zuukzoc/lypUr8y1Hfp+/QDmHEfHTkCNPPfWU78QTT8z6HmzUqJH7Hly1alWB+wHeUUL/xDqIBAAAQOhoAwcAAOAxBHAAAAAeQwAHAADgMQRwAAAAHkMABwAA4DEEcAAAAB5DAAegWGgQWw2w7B+xXoO13nDDDW6mBk2PpDkrNfCp/taME9GkwXnDnf8yFJoXM+fk5l5S2P2imTA0SHPgbA0AoosADkDUaYomzZjQt29fNy2Tf/JxBWq33nqrG+1fM1fAmxT0aeYTzUQAoHgwlRaAqNOUWZpGSVNdBU7bpEnBBw4cmLVM44prnlhNHwXvSEtLc9M9aYouTWmX14TzACKHDByAqNN8kZpPVhd6v82bN1uVKlWyracLv9bxT24O79Dcu7/++qub9xdA9BHAAYgqTeKtics7dOjg7n/22WcuUNPy999/3/2tm9q/BWsDp+q5ChUq2O+//+4mBtffhx56qJsQPDMzM9traaJuTWR+yCGHWNmyZd3E55MnT47Ye/n3v/9tbdq0sXLlylnVqlXdBPWalD4vqlYcMGCAK0flypXdxOKa2D1YkKPJxrWeJj7XxOjHHXecPf3002EHyprYXRO8p6amWpMmTdzE7zkp06kJ14844gj3XjTB/bJly7Kts3DhQncsXn311VzPnzVrlnvsvffey1qmslerVs3efffdsMoMoHAI4ABE1ddff+3+P+GEE9z/xx57rGvzVr16dWvRooX7WzcFZXlRoNaxY0cXmClIa9eunQ0fPtxefPHFbOsp4GnZsqUNGTLEtbErXbq0de3a1QWKRTV48GDXTk/Vu9q+7tepU8dVBefX9u/ll192HRvUBnDQoEH2559/uveyZMmSrPU+/vhjV72soFDrDRs2zD3nq6++CquMCtaOPPJIe+CBB9z+Ufluu+02GzNmTLb1FFT279/fmjdvbk8++aTVr1/fzjnnHNuzZ0/WOq1bt3bL33777VyvM2nSJFdWvY9AOsbhlhlAIYUx8T0AhO2hhx7y6atm165d2ZYfeeSRvs6dO2dbtnbtWrfu+PHjs5b16NHDLRsyZEi2dVu2bOlr1apVtmV79+7Ndj8jI8PXrFkz35lnnpnrtbXdUP3888++kiVL+rp06eLLzMzM9tjBgwez/m7Xrp27+R04cMCXnp6ebf3t27f7atSo4bvuuuuylt15552+SpUqufWLIuf7l44dO/rq16+fdX/z5s2+lJQUt+8Dy/7AAw+4/Ry4X/r16+crU6aMb9u2bVnL9H6qVKmSrfx+N910k69s2bJFeg8AQkMGDkBUbd261WXCVPVZFLfccku2+6qKXLNmTbZlqjb12759u+3YscOtt3jx4iK99vTp093wJ8pc+XvR+uXXYF9t+VJSUtzfev62bdtcZw5ltwLLpLaAyn4pE1cUge9f733Lli0uW6n9pPsye/ZsV7Wbs7OBhnHJqVu3brZ//36bOnVq1jJVGf/111/usZyUlVMnlL179xbpfQAoGAEcgLinjg05q1gVLChIC6Q2WerZqvXVHkvPUbWiP3gprNWrV7vATW3KwqU2ZMcff7wrk6qAVSZV6QaWSdWcRx99tJ133nmuXdp1111nM2fODPu1VH2ptoZqa6egUK+l6lTxv546GkijRo2yPVfrap8GUhWrxu5Tlamf/lb1t9raBWtbJ/RCBaKPAA5AVCloUdZp165dhd5GKL1Sv/jii6yers8995x98MEHLqN1xRVXZAUWxU2dHtQJo0GDBvbKK6+4oExlUvDjH9BY1OlAbeJmzJjh3oM6OSiY09Ac4QSZZ511lsu6aTgPBYl6rd69e7vHA18vHMq0qTzargbqVRkvueQSl1XNSQG1OkUEZgIBRAfjwAGIKmVwRL1OlYmKlilTprjgTT0k1QMzsGdmUSkAUwC0fPly1/EiVOoBq44AqoIMzEoFjn3np6rW888/3930WsrKaWBcdTZo2LBhga/1n//8JyvA0qwIfjl7vKqTg/z888+ubH7qXJEzo+kP4NRhQ/u3Ro0armPG5ZdfHrQMOsbqpAIg+sjAAYiqtm3bZg1LEU3K0ilIChxaRMOSqP1aUWn4ElWhqvdpzkxWftk9f+YwcJ1vv/3W5s2bl6udYCC9lj/YDXV6qmCvpWrTnAGsqljVk3b06NHZ1h01alTQ7Sog05AmqjrVrVatWm74lGDUrk/DuACIPjJwAKJKWZ5mzZq5xvNq2xUtnTt3dlWH5557rqs21UDBGj5D2SuNQ1cU2saDDz5oDz/8sOsUcfHFF7ss34IFC6x27do2dOjQoM/75z//6bJvXbp0ceVThkrzhqotneaC9dOcsOrgoKpVtYFTOzUFWMr2hZrR0jAg/izezTff7Lb/0ksvuerZDRs2ZK3nH0NPZVb5OnXqZN999519+OGHrm1bMMrCqQOHMpzXX399ro4csmjRIvceLrzwwpDKC6BoyMABiDoFbqriUw/FaFHwo3ZmGzdudD0q33rrLTemmoKnSFD2TVOC6T0omFNAo0BL7c7yovZvGo/u+++/tzvuuMNV76pdnHqhBrryyiuz2u6p6lQdHxQ0KagKFiwF07hxY1dlqyykAjQFijfddJPdeeedudbVIL6qFlXgdu+997r2c+pdqs4Pwagsyjyqd2mw3qfyzjvvuKrbYJ0bAEReCY0lEoXtAkC2qjxl4p544gmXwUFiUTVvvXr17P777w8aMAKIPDJwAKJO00jdd999btT/wvaGRPxSOzu1q8s5Vh+A6CEDByCpqco1PxoSQwFoLHmhjACKFwEcgKRW0KCzGottwoQJFkteKCOA4kUvVABJraDpq9TLNNa8UEYAxYsMHAAAgMfQiQEAAMBjqELNh3rL/fHHH1axYkUmZwYAAFGn0d00d7SaRuQ3DiQBXD4UvNWpUycaxwcAACBP69evdzOz5IUALh/KvPl3YqVKlfJbFQAAoMh27tzpkkf+GCQvBHAhdN1X8EYABwAA4mX4IDoxAAAAeAwBHAAAgMcQwAEAAHgMbeAAIIGHIzhw4IBlZmbGuigA/k+pUqWsdOnSRR6ejAAOABJQRkaGbdiwwfbu3RvrogDIoVy5clarVi1LSUmxwiKAA4AEHIR87dq17pe+BgPVRaKov/YBRCYrrh9Xf/75p/uMNmrUKN/BevNDABfEmDFj3I1qBwBepAuEgjiNJaVf+gDiR9myZa1MmTL266+/us9qWlpaobZDJ4YgevbsacuXL7cFCxYU9TgBQMwU9pc9gPj/bPLpBgAA8BgCOAAAAI8hgAMAJJVrrrnGLrroorCeM2HCBKtSpUrUygSEiwAOABA3gZV6y+qmRt5HHXWU3XfffbZv3z7zonr16tmoUaNiXQwkKHqhAgDixrnnnmvjx4+3/fv326JFi6xHjx4uoHv88cdjXTQgrpCBi/F4MPsyDgS96TEAiPZ3TbRv4X6XpaamWs2aNd0QKKrm7NChg3388cdZj2t4lKFDh7rsnIZjaN68uU2ePDnrcQ3/dP3112c93rhxY3v66afD3meqMq1bt64bhqVLly62devWbI+vXr3aLrzwQqtRo4ZVqFDBTjzxRJs9e3bW42eccYYbJqJ3795ZWUXRdv71r3/Z4Ycf7rZ93HHH2VtvvRV2+QAycDGUvj/TLnx8VtDHmtapasN7tGXwTQBR/a6Jtnf7drS0lMJdan744Qf7+uuv7cgjj8xapuDt3//+t40dO9YNgjp37ly78sor7dBDD7V27dq5AO+II46wd955xw455BD3/JtuusmNen/ZZZeF9LrffvutCwL1WgoiZ86caQMHDsy2zu7du61Tp0726KOPuqDztddes/PPP99WrlzpAr+pU6e64FKvfeONN2Y9T9XBrVq1sr59+1qlSpXs/ffft6uuusoaNGhgbdq0KdR+QnIigItTy9Zvd1+6hf3iAwAveu+991xGS3O4pqenu/Gynn32WfeY7j/22GMu09W2bVu3rH79+vbll1/aCy+84AI4tZ0bPHhw1vaUiZs3b569/fbbIQdwytipKlft7+Too492gaACOT8FZ7r5PfzwwzZt2jSbMWOG9erVy6pVq+ZmwqhYsaLLKPop83bPPfdk3b/99ttt1qxZrnwEcAgH0UEMpZYp5X6dBtq3P9O6jfj/aXgAiMZ3TXG+djjat29vzz//vO3Zs8dGjhzpJv2+5JJL3GOrVq1yc7ueffbZ2Z6j0exbtmyZdV8z6YwbN87WrVtnf//9t3u8RYsWIZfhxx9/dNWmgRQwBgZwysANGjTIZdA056wCTr2WXjM/quJVEKqA7ffff3dlU2DKjBkIFwFcDKlNBBk2AHzX/H/ly5e3hg0bur8VhCnL9corr7gqTQVNoqBJmaxAqsaUiRMnugzX8OHDXdClDNiTTz7pqkUjSa+htnlPPfWUK6/a21166aUuIMuPyqIMn3qnqv2b3u9dd91V4POAnAjggmAuVACIPVWfPvDAA9anTx+74oorrEmTJi5QU5ZL1aXBfPXVV3bKKafYbbfdlq3DQTiOPfbYXAHfN998k+t1NOyJP1On4PKXX37Jtk5KSkquObX1PHV+ULs9UZu9n376yb03IBz0Qg2CuVABID507drVtSXTD2tl05T5Us/OV1991QVmixcvttGjR7v7oo4NCxcudO3KFBj1798/7Hmt77jjDlddquzazz//7NrgBVaf+l9HHRWWLFli33//vQswFYzlHAdOnSxUVbply5as5ylzpzZ1qqq9+eabbdOmTUXeT0g+BHAAgLilNnDqFPDEE0+4dnHqLKCgTD1ElSlTZwNVqaqzgigguvjii61bt2520kknuWE7ArNxoTj55JPtpZdeclWdqsL96KOP7KGHHsq2zogRI6xq1aou26fepx07drQTTjgh2zpDhgxxWTn1MFUvWdF2tJ7W11Aj6uAQ7qwQgJTwMeBYnnbu3GmVK1e2HTt2uO7exUHjJvm7+xel+z2A5KWhKtauXeuCmrS0tFgXB0AYn9FQYw8ycAAAAB5DAAcAAOAxBHAAAAAeQwAHAADgMQRwAAAAHkMABwAA4DEEcAAAAB5DAAcAAOAxBHAAAAAeQwAHAEA+JkyYYFWqVMm6P2jQIGvRokXWfU1q7+XpsObMmeOmJcvMzIzK9nPur+J2xhln2F133VWo5xbm2GoqtilTpli0EcABAOKCLpYlSpRwtzJlyliNGjXs7LPPtnHjxuWaKD7coCuS7rnnHhf0RJN/P3zzzTfZlqenp9shhxziHvvss8+yPfbpp59ap06d3OPlypWzJk2a2N13322///57vq913333uTlaS5UqZV5RokQJmz59ekjrTp061c2h61evXj0bNWpUSM/VfLg6l8KhfXn//feHfc6GiwAOABA3NDn9hg0b3CTwH374obVv397uvPNO++c//2kHDhyweFChQgUXJEVbnTp1bPz48dmWTZs2zb1+Ti+88IJ16NDBatas6bI/y5cvt7Fjx7r5NIcPH57na3z55Ze2evVqu+SSSyzegnll7ooiIyPD/V+tWjWrWLFiWM9VNlIBmOYkDfeHwHnnnWe7du1y5280EcAFMWbMGPfL5cQTT4zqzgeAYuHzmaXvi81Nrx2G1NRUF4QcfvjhdsIJJ9gDDzxg7777rrsYBmZCRowYYccdd5yVL1/eBTq33Xab7d692z2mzNS1117rghd/JssfDLz++uvWunVrd0HX61xxxRW2efPmiFYJLliwwA499FB7/PHH3f2//vrLbrjhBrdMk5OfeeaZ9v333xf4Oj169LCJEyfa33//nbVM2UgtD/Tbb7/Z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O/z111+ud2swgwYNch0mcqINHAAAiKc2cHEbwGnQ3iVLlrgBbXN26W3RokVWD9VQaeDfDz/80AVvRxxxRMgZuDp16hDAAQCAYuH5TgyHHHKIvfvuu7mWa5keC4eyeO+99559+umneQZvkpqa6nZW4A0AACDexFUbuECqyrzhhhvss88+s5NOOskt+/bbb13HhJdeeimkbSi5ePvtt9u0adPcdo466qgolxoAACCJA7hrrrnGzcLwzDPPuLHcRPdVBeoP6AqiIUTefPNNl7WrWLGibdy40S1XalLjwgEAAHhR3LWB++STT6xdu3ZWqlSpIm8rryFJxo8f7wLEgjCQLwAAKE6hxh5xl4FTtal6ip577rl24YUX2nnnnVfotmhxFpsCAABERNx1YlizZo1rr9akSRMbPny41ahRw84++2wbPXq0rVu3LtbFAwAAiLm4C+Dk+OOPt4ceesjmz59vq1evdmPCaQgQzWGqIUQGDBhgCxcujHUxAQAAYiIuA7hAtWvXtltuucU++OAD27JliwvsfvnlF1fF+thjj8W6eAAAAMUu7trA5ad8+fJ26aWXultmZmZI86ECAAAkmrjLwKkXqtq/BZuHVD0ymjZtal988YXrpXrooYfGpIwAAACxFHcBnCa0v/HGG4P2PFW32ptvvtlGjBgRk7IBAADEg7gL4L7//nvXvi0v55xzji1atKhYywQAABBP4i6A27Rpk5UpUybPx0uXLm1//vlnsZYJAAAgnsRdAHf44YfbDz/8kOfj//3vf61WrVrFWiYAAIB4EncBXKdOnax///62b9++XI/9/fffNnDgQPvnP/8Zk7IBAADEg7ibC1VVqCeccILrZdqrVy83eK+sWLHCxowZ44YPWbx4sZuhIdqYCxUAABQnz86FqsDsq6++sttuu8369euXNZ+pJqbv2LGjC+KKI3gDAACIV3EXwEm9evXczAvbt2+3VatWuSCuUaNGVrVq1VgXDQAAIObiLoC77rrrQlpv3LhxUS8LAABAPIq7AG7ChAl25JFHWsuWLbOqTwEAABDHAdytt95qb731lq1du9auvfZau/LKK61atWqxLhYAAEDciLthRNRJYcOGDXbffffZf/7zH6tTp45ddtllNmvWLDJyAAAA8RjASWpqqv3rX/+yjz/+2JYvX+4msFevVHVu2L17d6yLBwAAEFNxGcAF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v/32261+/fqWmprq5jzVhPY55xYsDqeccopt2LDBKleunLXs5ptvtksvvdTWr1/v5szt1q2bmwsUAIpT6WJ9NQDIxy+//OImS1dG68knn7TjjjvO9u/fb7NmzbKePXvaihUrin1i8Zo1a2bd3717t23evNk6duxotWvXzlpetmzZIr2O3mOZMmUsljIyMpiMHvAQMnAA4sZtt93mqiznz59vl1xyiR199NHWtGlT69Onj33zzTd5Pq9v375u3XLlyrnMXf/+/V1Q5Pf9999b+/btrWLFilapUiVr1aqVLVy40D3266+/ugxf1apVrXz58u71Pvjgg1xVqPpbz5czzzzTLdeyYFWo7777rp1wwgmWlpbmyjN48GA7cOBA1uN67vPPP28XXHCBe81HH3006Pt67rnnrFGjRm47NWrUcJk/v4MHD9oTTzzhqnOVqaxbt2627SxdutSVU8HlIYccYjfddJMLQP2uueYau+iii9xzFIw2btzYLVdm8bLLLnPvqVq1anbhhRe6wBpAfCEDByAubNu2zWbOnOkCCgU1OeXXzkyBlQIpBSIKXG688Ua37L777nOPd+/e3Vq2bOmCJrVdW7JkSVbGS5k9ZZ/mzp3rXnf58uVWoUKFoNWpK1eudIHOlClT3H0FODmDmy+++MKuvvpqe+aZZ+z000+31atXu+BJBg4cmLXeoEGDbNiwYTZq1CgrXTr3V7ECzDvuuMNef/1191raP9q2X79+/eyll16ykSNH2mmnneaqev0Zyj179rgsYdu2bW3BggUua3jDDTdYr1693H7yU7W0AtqPP/7Y3VfQ63+eXkvleuSRR+zcc8+1//73v2TogHgSxsT3ABA13377rU9fSVOnTi1wXa03bdq0PB9/8sknfa1atcq6X7FiRd+ECROCrnvcccf5Bg0aFPSxTz/91L3W9u3b3X39r/ta7jd+/Hhf5cqVs+6fddZZvsceeyzbdl5//XVfrVq1spX/rrvuyvc9TpkyxVepUiXfzp07cz2mZampqb6XXnop6HNffPFFX9WqVX27d+/OWvb+++/7SpYs6du4caO736NHD1+NGjV86enp2crZuHFj38GDB7OW6fGyZcv6Zs2alW95ARQvMnAA4sL/4prCmTRpkst4KdulakJVVyqz5KcqWGWglM3q0KGDde3a1Ro0aOAeU5br1ltvtY8++sg9pqrb448/vtBlUXXtV199la06MzMz0/bt22d79+511bzSunXrfLdz9tln25FHHumqYJUB061Lly7u+T/++KOlp6fbWWedFfS5erx58+bZMplqW6hqV2URVR0ramOodn6BZV+1alVWVbGfyq59CyB+0AYOQFxQWy+1DQu3o8K8efNcFWmnTp3svffes++++84efPBBVy0aWF25bNky69y5s33yySfWpEkTmzZtmntMgd2aNWvsqquuctWvCqxGjx5d6PehAFJt3lRN679puz///LNry+YXrJo4kIKoxYsX21tvvWW1atWyAQMGuKBM7fGK2mkirzKo7GofGFh23dTL9oorrojIawKIDAI4AHFB7cnU/mrMmDGuDVdOgWOxBfr6669dpkpBm4IvBYLqmJCTOjn07t3bZdouvvhiGz9+fNZjGqrklltusalTp9rdd9/t2pYVljovKMulzgU5byVLhveVqzZoygqqs4LaoKm9nQJQvUcFcXkNrXLssce6bFrgflRWUK/v76yQV9kVaB522GG5yh44lAqA2COAAxA3FLypurFNmzauo4CCCVUHqnpUDeuDUTCzbt06mzhxoqvm07r+7Jr8/fffrvG+eowqsFMgo4b9CnJEg/FqmJK1a9e6jNenn36a9VhhKFP22muvuSycsn4qv8r20EMPhbUdZRP1XpQBU7m1TVWBKgBTJk89b9VJQ8v1vtVL95VXXnHPVUZS6/To0cN++OEH9540tp6yjP7q02D0vOrVq7uep+rEoH2i/aZq5t9++63Q+wRA5NEGDkDcUHsvBVFqP6ZMmHpWHnrooa5aTz1Ig9FQHMqsKUhTuzBVk2oYEVWbinqdbt261fUM3bRpkwtQlIFTgCUKGNUTVQGK2s2prZl6dhaWsogKvoYMGWKPP/646+16zDHHuKracKjXrTKCeh9qg6ZAVdWpGuZE9B6VoVPA+Mcff7hqVmURRe3kFJTeeeedduKJJ7r7ats3YsSIfF9T66k3roJD7aNdu3bZ4Ycf7traBbYpBBB7JdSTIdaFAAAAQOioQgUAAPAYAjgAAACPIYADAADwGAI4AAAAjyGAAwAA8BgCOAAAAI8hgAMAAPAYAjgAAACPIYADAADwGAI4AAAAjyGAAwAA8BgCOAAAAPOW/wfIEHiV2DuO1AAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data-vs-MC AUROC (ideally ~0.5): 0.7487\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAeetJREFUeJzt3QV0VEcXB/AbN0KCJQECCR7c3WnwIjXcreiHVJAWKFIohWItUpy2uJbiXgrFCgQLHiABYkiEuLzv3Et3uwlJSCCbtf/vnIXV7Oy8t/vum7kzY6YoikIAAAAARsJc1wUAAAAAyE4IbgAAAMCoILgBAAAAo4LgBgAAAIwKghsAAAAwKghuAAAAwKgguAEAAACjguAGAAAAjAqCGwAAADAqCG4Asuibb74hMzMz1Fs6PD09qU+fPnpVP1weLpcm3oa8LVNv16dPn5Ixa9OmDQ0cOPCNz1uzZo3Ux4MHD9T3NWnSRC6mJK19500SEhKoSJEitHjxYq2VCzKG4AbeieoHUHWxtLSkwoULyw/C48eP03wNr/jx66+/UqNGjcjZ2Zns7e2pYsWKNHXqVIqKikr3vXbs2EGtW7em/Pnzk7W1NRUqVIg6depER48eNZityD92XGfwuidPnkiA4ePjY7TVw4EBf09KlSqV5uOHDh1Sf5e2bt362uP37t2jTz/9lIoXL062traUO3duql+/Pi1YsIBiYmLe+P6nTp2igwcP0tixY7Pl8xiL7N73rKysaMyYMfTtt99SbGxstvxNyBrLLD4fIE0cmBQrVky+yGfOnJED+MmTJ+natWvyI6ySlJRE3bp1o82bN1PDhg3lB4WDm7/++oumTJlCW7ZsocOHD5Orq2uKYKhfv37yN6tWrSo/Gm5ubhQYGCgBz3vvvSc/2vXq1TOI4IaDM31r2dCXAwzvA3yWXKVKFa2/HwcDHIznNP4+3L17l86dO0e1atVK8di6devk8bQOiHv27KFPPvmEbGxsqFevXlShQgWKj4+X79kXX3xB169fp2XLlmX43rNnz5bvS8mSJd+q7BwYmdq+t3z5ckpOTs7y3+zbty+NGzeO1q9fL79fkLMQ3EC24BaVGjVqyPUBAwbIAXzWrFm0a9cuaV1R+f777yWw+fzzz+WHVmXQoEHyvI4dO8qBf9++ferHfvjhBwlsRo0aRXPnzk3RJfTVV19JK5AuDlJg2DSD7pxUokQJSkxMpA0bNqQIbjig4WC9bdu2tG3bthSvuX//PnXp0oU8PDykpbJgwYLqx4YNGybBEgc/GQkJCZHnLF269K3Lzi2mpoZbYd4Gt0q3aNFCfrsQ3OQ8dEuBVnCrjKoZXfNMmQOa0qVL08yZM197Tbt27ah37960f/9+af1RvYaf6+XlRXPmzEkz16Vnz56vnQFnFz4rrlmzphwI+aD0888/p/m81atXU7NmzcjFxUXOrMuVK0dLlixJ8Rw+K+Sz6z///FPd9aDKX3j+/LkEfNw9lytXLulu4IDx8uXLbywjn8E3bdr0tfv5bJO7CD/++GP1fRs3bqTq1auTo6OjvAe/H3dpvA1uUZs+fTq5u7tL6xuXgT9fapn5bMePH5d6Vp3xqupH1YXHLXvcalG0aFGpX85nGD16dKa6YtKTOucmLQ8fPpRWDq7j4OBguS8sLEwCbS4Dl4Uf50A+K2f3Xbt2pU2bNqV4zR9//EHR0dEpTgY0TwpevnxJK1euTBHYqHAZRo4cmeF7cmDDQZW3t/drj/F24/3Xzs5Otidv17Q+T+qcG245mjRpkuxTTk5O5ODgIN/9Y8eOvfbaZ8+eyXeVtz8f+Pm7zvuA5nZmfHLD+wl3a/PJDl8vUKCA7EPc8quJu7E/++wz9bYoU6aM/E7wvpm6u69Bgwbyvvz3+HkTJkzI1L6XVs4N1w1/b3if5t8GLl+rVq3on3/+SfG85s2by28IfwcgZ+F0F7RClYSYJ08e9X38JX/x4oX8CKfX0sLN7Rwo7N69m+rUqaP+YeCDiYWFRY5uratXr8qZF/9w8UGQDwyTJ09O0WWmwoFM+fLlqX379vLZ+EA1dOhQ+RHkM2s2f/58GjFihPy4cosTU/0tPz8/2rlzpxzAuXuPD6QcSDVu3Jh8fX0lvyg9nTt3lvIFBQVJd50K1x03t/MZv+oHng+q3C3BB2N248YN6dJ704ExLXxQ44MgJ6jy5eLFi1JffMDTlJnPVrZsWena5L/JrXiq4FjV1cjdlXzgHzJkCOXLl0+6dH788Ud69OiRPKYNHJjzAT9v3rxSd9wayWXgcvOBl3NfONj6+++/afz48dJNyts4M7hrlrcZH1j5PRh3X/C24QA5Nd6fOM/mXbpeuZxcd9z6o4n3Gw5Mef/mbhQOULh7iwOdN4mIiKAVK1bIfsVJypGRkRKAtWzZUraRqouHvwd88sL38Tbkk5Xff/9dApy0cBDDf6N27doSrHBXNbfg8gkGv55xAMPfNw6k+vfvL+914MAB6aLj7TNv3jx14Pb+++9TpUqVZB/jIIhbuni/Z2/a99LC78fBDwfp3FLNdccBOJ+UqVqwGQd9XE6uey4D5CAF4B2sXr2aT5GUw4cPK6GhoUpAQICydetWpUCBAoqNjY3cVpk/f748d8eOHen+vefPn8tzPvzwQ7m9YMGCN75GWzp27KjY2toqDx8+VN/n6+urWFhYSJk0RUdHv/b6li1bKsWLF09xX/ny5ZXGjRu/9tzY2FglKSkpxX3379+XOpw6dWqG5bx165aU58cff0xx/9ChQ5VcuXKpyzZy5Egld+7cSmJiovKuQkJCFGtra6Vt27ZKcnKy+v4JEyZIWXr37p3lz3b+/Hl5Le9TqaVVvzNnzlTMzMxSbJ/0cHk8PDxS3MfvNXnyZPVtvs738X5848YNpVChQkrNmjVln1SZNm2a4uDgoNy+fTvF3xo3bpzsF/7+/hmWg7c97wOsRo0aSv/+/eX6ixcvpD7Xrl2rHDt2TMqxZcsWeSw8PFxud+jQQXkXDRo0UKpXr/7a/aNGjZK/f/bs2RTb18nJSe7nbaVZfs39l/eluLi4FH+PP4urq6vSr18/9X3btm2Tv8W/ASq8TzRr1uy1bc7biu9Lvd9XrVo1Rfl37twpz5s+fXqK53388ceyX9y9e1duz5s3T71d05PRvpd63zl69Kg893//+99rz9X8LrAnT57Ic2fNmpXue4N2oFsKsgU3dXMLBzcPczcIn/1xvg03cavwWR3jLpH0qB7jM0LN/zN6jTbwmSOfBXKzOJ+dq/BZHp9RpqZ5lhseHi7DifkMn1st+Pab8Nmkubm5+r25CV/VfM4tIhnhbj4+a+VuDs3y82gbPltWlY2b5LkZn1sh3hWfSXMLDbdEaXYVcgtbdn62tOqXPwPXL59Zc4xy6dIlyk6cBM/bjrsi+HNqtj5yKxGf2fN9XAbVhfd//mwnTpzI9Ptw68327dulHnlbccvkBx988Nrzsus7wPWu+VlU9u7dK62kml27/F3u3r37G/8ml1mVh8OtM9zKyq0Y3HqhuW25q5lzVzSHoPM+oWrVTMvgwYNT3OZ65++TZrn5/f/3v/+leB53U/F+ocrb4/2ecUvR2yQGp8b5ULzPcytuaqm7zVX1bezTC+gjBDeQLRYtWiQHTf6R5i4K/jLzQU2T6sdZFeSkJXUAxP3zb3rNm4SGhkrTe+oL35/RazifI60hu3xQTo2buPkAx0Ed/5jywUHVp5+Z4IZ/dLkZnd+P6427QPhvXLlyJVOv564pLoNq+D13d3ACKd+vwt1kHAhxUzoHnZzkyAedt8G5KCx1/XCZUx9A3/WzMX9/f8l94C4iVQ4GByBM9Td4e6Xexm+DA0Le/zi4Ve1/Knfu3JE64/fXvKjyWLjOM4u7C7nsfBDmUVLcbZFWAJMd3wGV1Lkoqm2Z2f08LWvXrpUuH8494W4vrg/O79HctvwenCvEuVma0hu1pcpj0cT7FXdra/5N7tJMXWd8AqJ6nPF3gIfLc/cRdwNzvfOghrcNdLi7kt+X98XM1jfmxcp5CG4gW/BZH//Af/TRR9JiwwmYfGbKSZCpf3T4oJYe1WOckMu4b16V//K2OFmQf1hTX1RJhO+Kf+w4V4IDOh7NxT/sHOhxwivLzI/ojBkzZIg7z/3z22+/yYGV/wbn8WTm9fwDzj+kqvwT/vHmBE9OclThXA6ex4O3jypXgQOd9PIessu7fjZuEeHETK5Xnp+F83f49aqET9Xf4Jar1Nv4bfA+zNuUA47U+L24LPz+aV34tZnF5ePkXM4l4RYf/r6khYMbPphyi9K74MBDMzjIDrw9OejkXBjOteHAj+uB84jepZUkO/PruNWP65db4TihmX9j+PvC2zF1gnJ2U9U3B/SQs5BQDNmOf5h4hBMnKf7000+SpMhUoxU4cZITatP6Afvll1/kf1XyHb+Gz9h42Cy3hLzNjx4fpNIaVZNRwiSfNfLjfKae2q1bt15L9oyLi5OgQbMLK60RI+mdwXGLF9cXHyA08ciczPwwcqIuB5h8gB8+fLh0d3CXWurWM+5C4JYJvvDBh1tzOLl34sSJWZr7RJWUyvXDia6aLV6pD6CZ/Wzp1Q0Htrdv35YWAk44V0ndvcbdhdnR5cYj+jgpnOuGWwU0gw4+iHPAntaIo7fBf5tbFPh7wS2e6eHvAyf5nj59murWrftW78UnCqmHmKu2ZWb287TwtuXtz/ub5vZL3WXD78HfB07I1my94cTet8V/kwMWbtHSbL25efOm+nHNLjA+AeELn4BwwM2/QVwm3pZZaVnhfYADdO6Ce1PrDQ/h1zyxg5yDlhvQCj4j5YMtjx5RTUjGP2o8nJN/NFWjhTTxmTmfjfNBinMAVK/hs3Ue1cP/p9WszmePPAojPdwkzT9gqS98f3o4iOJycCsBd4mocDn4hy31c5lm2bhJnkd9pcbdVnxQT+v9Un82boVJb5bntPDZKI/WWLVqlbQiaXZJqXIuNPEPPncnMA7OVNPG88GBR/5khOuPcyh4xJJmudMaLZTZz8Z1w1LXT1r1y9dTD2HnlpDU2/ht8IGOAwnOHeNWLQ5aVXiYNgcYqfcBVbk53yQr+D04EODJHTOaQ+bLL7+U+uFASDUkXRO3NL1pSD8HRRx4auatMA6qeL/R/A5xkJpWy1VqaW2bs2fPSh1p4u8S71s8IZ4KB9fcnf22uNzc8sInUJq4C5S3IbdKsrSGYatGcan2+/T2vbRw6xx/Xp70L7XU+/mFCxekLG8bkMLbQ8sNaA0PyeThvxywqJIDuRWHE0B5KDL/APIPBbeQ8LBlDlL4DIfP0FP/HR7Oyc33fKbFBwQe8sw5FRx88I8yD7XMbvzjxc3snMjIZ/F84OKDOXenaHat8fBnVYsIDw/mM3v+EeduoNRBAg8N5WHjPISaW0r4OdyEz2fmPByV59ngRFlureCDi2aryJvwgZeDR77wGWXqgzsfGPmHnt+Pc244J4E/D//Qq84sOeDg63xQz2iZCNW8I9xCx2XnAw1vV84fSd3SlNnPxmfE3ILBk8zxmTgfcHgoMLc48GP8flw+7qbhFojs7mJJHfjx/sitX1yvnLzK9cb7Igc7/Jm4O4a3Jyc482fiVgyeAiErXRDcdfimuXYYf35u8eSAlbeP5gzFvO9zsPimWa95ckBukeLWDh7yrBk48USY3IXJUwKohoJzy0dGXciM64FbbTgRmv8+t1Tw9uNuZc0uaa5HPtnhZF9ureFtyvWoCjzeJieFv2/cIsgnSlzvlStXlhmUOXGYE9u5zhjve9wtxeXjz8R5URxM8neAW4Yz2ve4RTQ1fk/u3lq4cKG0eHG9caDGQ8H5MW45VeGWRD6J4i5ByGFaGoUFJjYUnIdSpsZDPUuUKCEXzeHHfD+/rn79+jI0mYdb8/DYKVOmKC9fvkz3vXiIeYsWLZS8efMqlpaWSsGCBZXOnTsrx48f19rn+/PPP2X4KQ/T5WHdS5cuVQ8Z1rRr1y6lUqVK8lk8PT1l6OeqVateG0obFBQkw6cdHR3lMdWwWh4u/dlnn8lnsrOzk7o5ffr0a0Nv34Rfx393wIAB6dafi4uLfJ6iRYsqn376qRIYGKh+Dpc19VDu9PB25G2mKnOTJk2Ua9euybDZ1EPBM/vZfv/9d6VcuXKyfTWH5vIQfG9vbxnanj9/fmXgwIHK5cuX0x2++65DwTWHoHMZ+X3PnDkj90VGRirjx49XSpYsKfXI5alXr54yZ84cJT4+PtNDwdOTeii4Jh6Czp+d9zF+b96PuD55GgCu5zdp37698t577712/5UrV6RsvP8WLlxYhryvXLnyjUPBeejzjBkzpG55aD8P1969e3ea9c312q1bNykzDzPv06ePcurUKXmPjRs3qp/Hr+Xh9qml9b3jbTF69GgZtm9lZaWUKlVKmT17dooh2UeOHJFh9PwcrjP+v2vXrq8N509v30vrs/DvGb+Pl5eX/E2e+qJ169bKhQsX1M8JCwuTx1asWPGGrQLaYMb/5HRABQAAOY9bF7jLmLse01u8Mydxyyu3+nDLbUbdxIaIu2h5ZmnuMszMhIiQvRDcAACYENVUAJr5LzmBk/o1D/KcL8NdurxkAXcxG1MAwPlF3NXF3fDcpQ05D8ENAABoHed8cYDDybWcyMu5OpwvxCOXePkKgOyE4AYAALSOE6J5UAAnFPMISk6o53WiNBNwAbILghsAAAAwKpjnBgAAAIwKghsAAAAwKiY3iR9PtvTkyROZqAmLmQEAABgGnrmGl9vgtdZ4os2MmFxww4FNkSJFdF0MAAAAeAsBAQEynUFGTC64US2wxpXD07gDAACA/ouIiJDGCc2FUtNjcsGNqiuKAxsENwAAAIYlMyklSCgGAAAAo4LgBgAAAIwKghsAAAAwKiaXc5NZvKgbL34GAIbBysqKLCwsdF0MANADCG7SGEfPK9SGhYXpZosAwFtzdnYmNzc3zGEFYOIQ3KSiCmxcXFzI3t4eP5IABnJSEh0dTSEhIXK7YMGCui4SAOgQgptUXVGqwCZfvny62yoAkGV2dnbyPwc4/B1GFxWA6UJCsQZVjg232ACA4VF9d5EvB2DaENykAWtOARgmfHcBgCG4AQAAAKOi0+DmxIkT1K5dO1nhk8+4du7c+cbXHD9+nKpVq0Y2NjZUsmRJWrNmTY6UFQAAAAyDToObqKgoqly5Mi1atChTz79//z61bduWmjZtSj4+PjRq1CgaMGAAHThwgExdnz59JEDkC8/34erqSs2bN6dVq1ZRcnJylv4WB4w8pFYfqT7jmTNnUtwfFxcnSeD8GAfAmo4dO0Zt2rSRxzkno1y5cvTZZ5/R48eP3/h+M2fOlMTU2bNnv/bYN998Q1WqVHnt/gcPHkg5eB9lXB5VuflSoEABKc/Vq1dfey0v6NqvXz8J+K2trcnDw4NGjhxJz549e+25d+/epb59+8rquBzsFytWjLp27Ur//PMPZcXz58+pe/fustYab/f+/fvTy5cv032+6vOlddmyZctrz+eycxn58dRTLKxbt05+A3i78Agn/uyan7VJkyZpvg//DgAA6GVw07p1a5o+fTp98MEHmXr+0qVL5Qf8hx9+oLJly9Lw4cPp448/pnnz5mm9rIagVatWFBgYKAefffv2SRDIB8b333+fEhMTyVjwqrCrV69Ocd+OHTsoV65crz33559/Jm9vb5n7ZNu2beTr6yv7UXh4uOxHb8LB4Zdffin/v6tbt27J9uFgnIMxPkDHx8erH/fz86MaNWrQnTt3aMOGDRK8cFmPHDlCdevWlSBEhQOY6tWr0+3bt+Uz8ufiOvDy8pLALSs4sLl+/TodOnSIdu/eLS2qgwYNyrD++XNoXqZMmSL1z9/p1DhYqlSp0mv3nzp1inr16iWP8/tzYHTu3DkaOHCg+jnbt29P8T7Xrl2TYPOTTz7J0mcEgOyTnKxQXGISxcQnUWRsAoVHJ9DzqHgKiYyloPBYevQimoIjYkmnFD3BRdmxY0eGz2nYsKEycuTIFPetWrVKyZ07d7qviY2NVcLDw9WXgIAAeS++nlpMTIzi6+sr/xua3r17Kx06dHjt/iNHjsjnXb58ufq+H374QalQoYJib2+vuLu7K0OGDFEiIyPlsWPHjsnzNS+TJ0+Wx3755RelevXqSq5cuRRXV1ela9euSnBwcLplGj9+vFKrVq3X7q9UqZIyZcoU9fvVrFlTyuLk5KTUq1dPefDgQbp/k8vz9ddfyzaPjo5W39+8eXNl4sSJ8jj/Tcbb2traWhk1alSaf+vFixdKRo4fP64ULlxYiY+PVwoVKqScOnUqxeNcL5UrV37tdffv35dyXLp0Sf0Z+bbm++3atUvuu3z5svq+Vq1ayfbQ/FwsMDBQ6mfw4MFyOzk5WSlfvrxsi6SkpCx/Lk28v3M5zp8/r75v3759ipmZmfL48eNM/50qVaoo/fr1e+3+xYsXK40bN1bvh5plmz17tlK8ePEUz1+4cKHUeXrmzZunODo6Ki9fvkzzcUP+DgNoQ3JyshIVl6A8ehGtXH0Upvx1O1T54/JjZdM5f2XlX37KwsO3lSG//aN8veOqMnrjJWXg2vNK12WnlfY//qU0n3tcqf/dEaX6tINKxcn7lTJf71U8xu7O1OXDxSl/L7MDH7fTO36nZmloE+xxd4smvh0REUExMTHqeS5SdyvwWeXb4uNpTEIS6YKdlcU7j/5o1qyZNPvzGTB34TFzc3NauHChtIJxa8HQoUOldWLx4sVUr149mj9/Pk2aNElaGpiqRYSH106bNo3KlCkjc4mMGTNGusP27t2bbosA1/+9e/eoRIkSch+foV+5ckVaUbg1qWPHjnKmzi0V3IrBZ+5v+szcYuHp6Sl/o0ePHuTv7y+tDdy9yeVT4ZYA/pv82dLypq63lStXSjcPd/Px/3yb6+ddcavRxo0b5Tp3PTFuleEWnW+//fa1/ZhbnbguN23aJNuIu7u4HtevXy/bMqPPxd06XFfp5aadPn1ans8tRirc0sV/9+zZs5lqVb1w4YKUKXX3MrcmTZ06Vf4O72epcWvUhAkTZP/hFh/ep7Zu3SpddunhbdClSxdycHB4Y7kAjLnl5FlUvLSOhEbGySUoIlZaT56+jKNnL+MpIjaBXkTFU+jLOEpI4phAu5Kiw8mMFLLOlYcszM3IyuLdjl3vyqCCm7cxfvx4OQircCDEzeqZxYFNuUm6yenxndqS7K3ffRNxVwUHFCqcq6TCBz7uGhw8eLAcOPlg6+TkJAEGH1Q1cT6ESvHixSVAqlmzpuRnpNUlVL58eQms+CA8ceJEdY5F7dq1JRmcD+h8oOduM1Xww92NmcFl4a4iDm74wM0HRM5l0cTdO5xH8jaz1fJ+wgdaPvgzfp+GDRvSggUL0vysmcF5J6pcM9a+fXvZNqqyciCd3ufn+1+8eEGhoaHyXKZ6bUaKFi2a4efnEwae8E6TpaUl5c2bVx7LDA44uHyagR93u3FAyLlKXIa0gpv69evL/tC5c2eKjY2VYJcHGKSXg8eBL3dL8fsBGCP+DXgRnUCB4TESuDwJi5VgJTgijkIi/rv+LCrrAYu1hTk521tRHntrcrK3olw2lmRnbUGONpZka2UhgVH5QrnJwcaSHG0tycHakuxtLOQkmx+3sTQnKwtzsrY0J0sOXv7939zMjP4++Rf17DFIfgf4JE0fJtA0qOCGD7bBwcEp7uPbfABLq9WGcaIlX0z9C6PZGnL48GFpUbl586YcxPmgwgcXnr4+owkM+Qydk2gvX74sB1pVojK3nHCSblq4xYGDEA5uuBzcQqMKNvkAyi0/LVu2lORnbjHo1KlTpoIRDjbGjRsnB00ObjjQetPnzgouJwdcHJwxThzm5F5uPeEckbfx119/Sf1yMvSMGTMknyatMr9JZp6j8ssvv5A2cYupZvCqeVLBP3S8ndLDLTucE8athLwPcE7NF198IYF2WgEM31exYkWqVauWVj4LgLZFxydKwMI5KY9exNCTsBh1q0tgeCw9fhGT6Z4C/mnLn8uGCvDF0YYKOtlSXgdrypfLhvLnsiYnOytytreWx5ztrMje+t17AlLjYwAfS/g7zNf5WMwtsPqw/IlBBTfcjJ26C4STIPl+beGolVtQdIHfOzvcuHFDuqAYJxtzS8mQIUOkC4QDjJMnT8oBm7tw0gtuuLWBD0B84bNtbiXhoIZvaybFpsZn72PHjqWLFy/KgZBHA/GZugonBv/vf/+j/fv3S+Dw9ddfyzatU6dOhp+JRz7x5+Byc2DG3RqRkZEpnlO6dGlpGeKDZla/bHwg5a4fbsVQ4S8vB2qq4Ia/yPz3U1ONCOIWME28DbgLSNWtx/XA3WmMW7L4h4e3VVpdQXx/njx5pN75czEOTqtWrUrvesKgWo9JhYNdblVL3XKXFm7d4qCYE4M1HT16VEaD8eOaAVn+/Pnpq6++kq5i/lHk1hsOaBgnHXN3E7eQcWui5jbj/Y+78ribC0BfJSUr0ury4Gk0PXgWRQEvoiVgCXgRI/9zy0tmcHDi4mhLhZzt5Lprblu5qK7nd7QhF0cbaUnRleDgYOrZs6f8XjP+DeBW17dt2Taq4Ia7M3hEiOZQb+675wMuN2Xz2R8P11WdffIZ3U8//SQ5FNwtwT+gmzdvpj179mitjHzAyY6uIV1RHWRGjx6tbn3hgzSPFFLla3AdauKuKV5nSxMfSHmI7nfffafu1svMkGPuimncuLEERBzccAtN6m4QPkDzhbc3B6rcEvCm4IbxPsDdURw8pdUMyiPpuHXn+++/T3NEHQchaeXdcH3xZ+Mh3LwvqvABn3NYuC64S4iDlEePHsmXXDMXjAM5W1tb2YfTM2zYMDm48wgnDmY4WOO64a5B3laaLZHcPcT1xz8evD9yKxK3lPE25AApdd5Nep8rLVzf/HzeLziXSbXP8D7C3YeZCQK5ey11lyDnQ/H2Vjl//rxsL269UnVBclCkGTwy1XZM3TrF+VPc1ZVRSxBATuFclptBkRLA3At5SfdCX9LDZ9ESzLypu4i7g9zz2FFhZzv5n1tf3JxsqaCTHRXOY0eFnG3JxlL33ToZ4d8IbpXn3yY+Iebfrd69e5NeUXQorZE5fOGRP4z/55EWqV/DIzN4FAyPtFi9enW2ZVsb8kgLrisebcMjax49eqRcuHBB+fbbb2Vk0/vvv68kJibK83x8fOTzz58/X7l3756MgOLRKZojWXhUEN8+fPiwEhoaqkRFRSkhISFS51988YW87vfff1dKly6dYlRQenikFo82yp8/v/Lrr7+q7/fz81PGjRun/P333zJC6sCBA0q+fPlkhE1mRtXxKAAuX1xcnNzm8muOlmKLFi2SkT88kodHP/H7nDx5Uhk0aJAyZsyYNN+DR+TVrl07zcd49Nfnn38u1xMSEmTUUtOmTaXOuF62bNmiFCxYUBk7dqz6NWmNlmJffvmlUrFiRfkc7Pbt21JHPCrwzz//VPz9/WXkEo9sK1WqlPLs2TP1a8+ePSujhnh02Z49e+S9eeTV9OnTlUaNGqmf17NnT6njjPB+U7VqVfmbXDf8XjwSToX3pzJlysjjmu7cuSN1y2V8k7TqgL+7lpaWsr25/PzeNWrUSHOEXYMGDZTOnTu/8X0M+TsM+iU2IVG5Exyh7L3yREYUjdxwUUYQVZ5yIMNRQiXG71Gazjmm9F19Tpn8+zVl+Yl7yr6rT5QrAWFKeEy8YugSEhKUsmXLyveZf/+uX7+eY++dldFSejMUPKcYc3CjCg75gFGgQAHF29tbhsqnHi48d+5cOQDb2dkpLVu2lAAn9YGHhx1zoKE5FHz9+vWKp6enYmNjo9StW1c9nPlNwQ3/XX4ND2dWDTlnQUFBSseOHaUsHDh5eHgokyZNSnN4c2amDEgruGGHDh2Sz5knTx7F1tZW8fLykgDlyZMnr/0NDpT4c3///fdpvsesWbMUFxcXGR7OeLg0133RokWlPsuVK6d899136sczCm44eOFttWnTJvV9HHzx3+Oh9lZWVkqRIkWUESNGKE+fPn2tLLdu3VJ69eolgaOq/jgouXjxovo5fHKgOllIDwdN/DoOhHmIfd++fVNsJ9XQ9tT1ykP9uXwZba831QEP/eY647rj/aB79+4STGm6efOmvPbgwYNvfB9D/g6DbiQmJSt3QyIliJlz4KYMheYh0BykZBTENJh1ROm96qzyza5ryq+nHyin7oQqAc+j5O8ZOx8fHzlG8IlvTspKcGPG/5AJ4QRazoXgXAnOmdDEuRvcNca5EdytAACGBd9hyHD/SEiiG4ERdO1JhPzv+ySCbgZFUGxC2rO4O1hbUAmXXFTy30vx/A5UNK8Deea3N+h0haw6ePAgPXz4MMUEm/p2/E7NdLYOAACYDJ7n5WZgJF17HC6X608i6G7oS0n6TWvwRmnXXFTK1VGGQ3vmd6Ayro4yAsmUV5pPTEykyZMnS34g58dxXh6v7WgIENwAAIDBD7G+HBBOPgFhdP1JuLTI+D19NZ9UajziqFwhJwliyhbMTRUK5SaPfA4y8Rz8hwdL8GhXHk3LeJRoelN+6CMENwAAYHCBDAcx3Bpz5VGYBDJpJVgUcrKlcoVyU/lCTlSxsBOVL5yb3HKbdmtMZvCUKzw6k0fIOjo60ooVK2QOMkOC4AYAAPQWT3LHLTKX/F/QJf8wuhTwIs0cGe5CquzuTFWLOlNpN0eqVNhJJrSDrOF5qHiSUcZdUDxViGr6BkOC4AYAAPRGeEwCnbv/nE7fe0Yn74bS7eCXaQYyldy5a+lV91LlIs4yXwy8u7z/zu01YsQIWT7FUGf4R3ADAAA6ExmbQGf9ntO5B8/p7P3ndPVRGGnm/HIPUmkXR6pcxImqFc1DVYvmkeRfdC1ln6ioKPVitLw8Dk/g2aBBAzJkCG4AACBHh2NfDgiTQObU3ad00f/Fa7P6Fi/gQLWL5aUGJQtQ3RL5ZM0kyH7x8fEy4z8vdsmziPPSCRw0GnpgwxDcAACA1sQlJtEZv+d0xu+ZdDXxsOzEVMOxPfPZU90S+ammZx6qUzyfrKkE2uXn5yfLt6iW0fnjjz9kdJSxQHADAADZKjgilv6685SO3gymE7ef0su4xBSP86KPNYvlldaZJqVdqGi+tBfsBe3Ytm2brPXGk+Lxgrxr166ldu3aGVV1I7iBd8bNmLwAZMeOHVGbACYoOVmha0/Caf+1IDp8I/i1JGAOZhqVLiDBDHcz8aKRyJnRzQzen3/+uazezerVq0cbNmzIcJFfQ4Xgxkj06dNHom/GM0nyatyffPIJTZ06FUtJAIBW5ps5djOUTtwOpWO3QigkMi5FEjDPK9OkdAFqVtZVhmWbY5I8nfviiy/Ugc3YsWNp2rRpZGVlRcYIwY0RadWqFa1evZoSEhLowoULsgQ9nx3NmjVL10UDACMQFB5Lh3yDaNvFx3QrKJJiEpJSrMPUuEwBalHOjRqXLkB5kASsl3PYHD9+XIZ48/HCmJnrugCQfXg+Ajc3NypSpIh0EXl7e9OhQ4fkMZ5pkpPFChcuTPb29lSxYkVpjtTUpEkT+t///ifZ8zzXAf+tb775JsVz7ty5Q40aNZLWIJ6KW/X3NV29epWaNWtGdnZ2lC9fPho0aBC9fPkyRSsTl48ninJ1dSVnZ2dpYeJ1TPjMgt+bW544UAMA3XoSFkOrT92nj5b8TXW/O0ITf78uk+pxYFMkrx0NaFCM1varRRcnNafF3atTx6qFEdjoiZiYGFq/fr36Nv+mX7582egDG4aWmyzMA5AeCwuLFF0/GT3X3NxcDvpveq5qzoG3de3aNfr777/Jw8ND3dfKi55xUySvprpnzx7q2bOnzDxZq1Yt9eu4a4vnOTh79iydPn1aApH69etT8+bNKTk5mT788EMJSPhxXpl11KhRKd6XP0/Lli2pbt26MrQwJCSEBgwYQMOHD6c1a9aon3f06FEJYE6cOEGnTp2SdUu4vBw48d/etGkTffrpp/K+/DwAyDlh0fGSP7P94mOZf0ZTtaLO1MzLReac4fwZ5M7op5s3b8qSCXyyyakKquUT+BhkEhQTEx4ezmMQ5f/UYmJiFF9fX/k/NX5Nepc2bdqkeK69vX26z23cuHGK5+bPnz/N52VV7969FQsLC8XBwUGxsbGRv2Fubq5s3bo13de0bdtW+eyzz9S3uWwNGjRI8ZyaNWsqY8eOlesHDhxQLC0tlcePH6sf37dvn7zXjh075PayZcuUPHnyKC9fvlQ/Z8+ePVKWoKAgdVk9PDyUpKQk9XPKlCmjNGzYUH07MTFRPsuGDRuyXBdgujL6DkPGwmPila3/BCh9Vp1VSk7Yo3iM3a2+fLT4lLLiLz/lSVg0qtEArF27Vn0ccnFxUQ4dOqQY+/E7NbTcGJGmTZvSkiVLpPVk3rx5Eq1/9NFH8lhSUpJ0A/E6IY8fP5bJm+Li4qSLSlOlSpVS3C5YsKC0vrAbN25Il1ehQoXUj3MLjSZ+TuXKlVO0PHHLD7f63Lp1S1p9WPny5VOcQfD9FSpUSNEaxl1aqvcGAO3MQXP0RgjtuPRYkoI1J9PjWYA7VClMH1QtjHlnDAT/9vOyCav/7dLn9IDffvtNfsdNDYKbTNLMGUmND8SaMjogp24SfPDgAWUXDihKliwp11etWiVBxsqVK6XLhxPIFixYQPPnz5d8G34udylxkKMpdeY8NzlzYJLd0nqfnHpvAFNf7uDYrVDaeyWQ/roTSlHx/yUFlyjgQO9XKkTvVypIpVwddVpOyJrr169L15Ovr68cZyZPniwJxKmPT6YCwU0mZSUHRlvPzQreuSdMmCD5M926dZO8lg4dOlCPHj3kcQ4abt++LUnBmVW2bFkKCAigwMBA9ZnAmTNnXnsO59ZorlXC783lKVOmTLZ+RgDInMSkZDpxJ5R2XnpC+68HUXzifycNrrltJAmYW2i83HKjSg3UvXv3JLDh3+b169fLABFThuDGiPE8N6p5DUqVKkVbt26VpF2ekXLu3LkUHBycpeCGR1+VLl1ahphzSxDPbslnBpq6d+8uZwz8HB5pFRoaKs2knLys6pICgJxx9VE4bb/0iHb5PKFnUfEpljtoW6mgDNvm+WgwB41h4nRQVUJ3+/btacWKFTLTsIuLC5k6BDdGjHNueJTS999/T5cuXZK1RHgkE+fZ8PBsHo7NI54yi1tfeCZi7ubiEVaenp60cOHCFMMK+W/zImwjR46kmjVrym3O++FgCgC0LzQyjnZdfkJ7rjyhi/5h6vt58cn2lQtJK01ldyeMcjJwPKR76NChtHHjRsmFZPzbDK+YcVYxmRBubXBycpKDOg+J1sTDpe/fv0/FihXDrL4ABshUv8Mx8Ul05GYw/e7zhI7eDKGkfxemtDQ3o5bl3ejj6u7UoFR+srIwkWHARowP2cuWLZMTSB4Uwi30PFDE1I/fqaHlBgDAQF3yf0G/nH5IB64HUbRGYnCVIs7UrnIhalepILnkNp0gzxQO7tzqzvOAsbZt29LixYt1XSy9hOAGAMDAFqn883YoLf3zHp29/98Ee7wYJQc0H1YrTKUx0snoXLx4kTp37kx3796VlIOZM2fKgBGTmZQvixDcAAAYgPCYBNp95QmtPvWA7oa8mprCysKM2lUqRD3qelDVIs7IozFSx44dk9xGnrqDV/Dmlps6deroulh6DcENAIAeD+HmOWm2X3wkk+zFJrwawu1oayl5NIMaFaeCTv8t5wLGiQMZnkqjePHiMocZr78HGUNwkwYTy7EGMBrG8t0NDI+hDecCaPP5AAqKiFXfX9IlF3WpWYQ61SxCuW1TTnoJxjcpn5eXl0zCx+sRcusNBzVYyytzENxoUM2QGx0dnWJxSwAwDPzdZalnuzaUwOy03zP65e+HdPhGMCX+O+Ipj70VfVKjCLWtWJAqYQi30eP9gGeS50WOJ02aRF9//bXcz8vRQOYhuNHAEbKzs7N6+QSeowVRMoBhHBA4sOHvLn+HDWnK+ej4RBnCveWfgBTz0tT0zEM96nhQqwpuZGNpOJ8H3t7z58+pT58+9Mcff8jta9eupZioDzIPwU0qbm5u8j8WbAQwPBzYqL7D+u5uSCRtPBdAWy48kmRhZm1pLrk0vet6Uhk3rO1kSnj2+C5dusgSN9bW1rL48ZAhQxDYvCUEN6lwhMxrc/D01QkJr35wAED/cVeUIbTYXHj4nBYcuUsnboeq7yua15661CpCH1VzJ1fMS2NSeJ2/OXPmyFqASUlJsvgxT8pXtWpVXRfNoCG4SQf/SBrCDyUAGMbcNEduhtCPR+/QlUevljwxNyNq5uVCXWoWpaZeLmTBd4BJLnjJuTUc2HTt2pV+/vlncnREq927QnADAKDFfJpN5wPotzMP6V5olNxnbWFO7asUohHNSpJHPgfUvYnjRY1/+uknya0ZMGAAuqGyCYIbAAAtLF657uxDmXBPlU/jYG1BPet6ytw0vIglmG431HfffUfe3t6yADHjoAayF4IbAIBscjMogpYev0d7rwZRfNKrCfc889lTn3qe9FF1d3LE3DQmLTg4mHr27EmHDh2i5cuXy2goBwe03mkDghsAgHd05VEYzTl4O0WScGV3JxrQsDi1qVgQ+TRAR48epe7du1NQUJDMozZ58mQENlqE4AYA4C398+A5LTz638gnzgluWd6NBjcuQZWLOKNeQRKFp02bRlOnTpW8mvLly8toqHLlyqF2tAjBDQBAFkc+8QzCa/5+QH/fe6YOajpUKUyjvUtT0Xz2qE8QERER1KFDBzp+/Ljc7tevH/34448yQSxoF4IbAIBMjnxaf9ZfgppHL2Je/YCam8mke0OalMDIJ3hNrly5pOuJL0uXLqUePXqglnIIghsAgAwEPI+mdWf9ZfRTZGyi3Jfb1pI61ShCfep7knsenIXDfxITE2UCWM6rMTc3p7Vr19LTp09lVW/IOQhuAADScNH/Ba06eZ92XwlU3+eRz56GNilB7SsXJjtrTPIJKT169Ii6detGxYoVk6BGteAlFr3MeQhuAAD+lcQzCd8IplWn7tMZv+fqeilXMLdMusfJwuaYSRjSsHfvXurVqxc9e/aMfHx8aMqUKeTp6Ym60hEENwBg8mITkmjzPwG0/C8/Cnj+Kp/GysKM2lUuRP3qF6MKhZ1Mvo4gbdwF9dVXX9Hs2bPldrVq1WjTpk0IbHQMwQ0AmKyw6Hhaf85fZhLmWYWZk50VdalZhHrX86RCzna6LiLoMX9/f1nJ+/Tp03J7xIgREuTY2NjoumgmD8ENAJgcDmSWnbhHG84F0Mu4V0nChZ3taGDDYtS5ZlHk00CmllFo1aoV3bhxg5ycnGjVqlX04Ycfoub0BIIbADAZd0Ne0oZz/vTrmYcUn/hqeQQvN0fq36CYzFNjbWmu6yKCgeCRUAsWLJAVvdevXy9JxKA/zBSeMtHEJlXiKDs8PJxy586t6+IAgJbxT9xB32DaeM6fjt36b3mEKkWcJUm4mZcLVmKGTPHz86N79+5R8+bNU7TgcKAD+nX8RssNABitCw9f0NTdvnQ5IEx933teLtSjrgc1KV0AQQ1k2rZt22SGYXbx4kUqUaKEXEdgo58Q3ACA0bnk/4IWHrmjbqnhkU/Ny7nS8KalqFwhtNhC5sXGxtLnn39OixYtktt169YlKysrVKGeQ3ADAEaz5tORmyG0/IQfnXvwXL08wgdVC9OwpiXJM7+DrosIBubOnTvUuXNnunTpktz+8ssvafr06QhuDACCGwAw+DWftl14JMO5/Z5GyX0W5mbUoUohGtqkJJV0yaXrIoIB2rhxIw0aNIgiIyNlhuFffvmF2rRpo+tiQSYhuAEAgxQenUCr/75Pv5x+SM+j4tVrPnWtVZT61i9Gbk62ui4iGLCzZ89KYNOwYUMZDeXu7q7rIkEWILgBAINbyHLlyfu08bw/xSYkq9d86lXXUybfc7DBzxq8/cg6MzMzuT5r1iwqWbIkffrpp2RpiX3K0GCLAYBBHHT+efiCZh+4Refu/7fmE89RM6RJCWpbsSBZWmA4Lry93377TVpodu3aJcGMtbU1DRs2DFVqoBDcAIBeBzWHb4TQj0fv0JVH4er7i+S1o287VqSGpfJjODe8k6ioKFk2YfXq1XKb/x84cCBq1cAhuAEAvQxquIXmh0O31S01tlbm1L5yIerfoDiVcXPUdRHBCFy/fp06depEvr6+EiRPnjxZPZcNGDadt+Py3AG8LLytrS3Vrl2bzp07l+Hz58+fT2XKlCE7OzsqUqQIjR49WuYhAADjCGqO3Qqhzj+foc7LzkhgY2NpToMbl6CTY5vR9x9XRmAD2bKfcQtNzZo1JbBxc3OjI0eOSHBjYWGBGjYCOm254WXhx4wZQ0uXLpXAhgOXli1b0q1bt8jFxeW153N/6Lhx42SBsnr16tHt27epT58+EnHPnTtXJ58BALJnjpr914NoweE7dCs4Uu6ztjCnj6q7yxIJWJ0bstOUKVPkwngpBc63SeuYA4ZLp2tLcUDDkfNPP/2kXqODW2O4/5ODmNSGDx8uK7ByhK3y2WefyZC9kydPZuo9sbYUgP5ISEqmXT5PaNGxu+o5arilpmcdD+rfsBgVdLLTdRHBCPFxpE6dOjR27Fg51mAJBcNgEGtLxcfH04ULF2j8+PHq+3gH8/b2ptOnT6f5Gm6t4Qibu65q1aoli5jt3buXevbsme77xMXFyUWzcgBA9y01e68F0pwDt+jBs2i5z9HWkvrU86QBDYqTkz2mt4fsw+fwly9fpipVqsjtsmXL0v379ylv3ryoZiOls+Dm6dOnlJSURK6urinu59s3b95M8zXdunWT1zVo0EB21sTERBo8eDBNmDAh3feZOXOmuvkRAHQrMSmZ9l0LktFPt4Nfyn35c1lTvwbFZJ6aXJijBrIZn9DyXDWbN2+m48ePy6R8DIGNcdN5QnFW8I45Y8YMWrx4sazKun37dtqzZw9NmzYt3ddwyxA3YakuAQEBOVpmACBKSlZo64VH9N7cP2nEhksS2NhZWdAo71J07PMmskwCAhvIbrwmVPXq1WUpBc7N5O4oMA06a7nJnz+/ZKUHBwenuJ9vc+Z6WiZOnChdUAMGDJDbFStWlDkKeP2Pr776Ks1+UxsbG7kAQM6LTUiiLf8E0NI//ehxWIy6+2lgw+LUp74n5bZF9xNkP27Z55NgHrDCKRBFixaVAIdX9AbToLPghmd/5Iiak4M7duyoTijm25w4nJbo6OjXAhjVsD0d5kUDQCrxicm05UKAjH4KiXyV8+ZkZ0UDGxaTdZ+wRAJoS1hYmJwAb9u2TW63b99ehn2jG8q06HQoOEfVvXv3pho1akiCMA8F55aYvn37yuO9evWiwoULS94Ma9eunQz5rlq1qoy0unv3rrTm8P2YmwBAP1boXnfGn1aduk+B4a/mnyroZEufNipOXWoVJVsrzCEC2rVz504JbKysrOj777+nkSNHYhZrE6TT4KZz584UGhpKkyZNoqCgIMlk379/vzrJ2N/fP0VLzddffy07Kf//+PFjKlCggAQ23377rQ4/BQBExSXSmr8f0Iq//OhFdIJUSAFHGxrSuAR1r1OUbCwR1EDO4BPmK1euUNeuXWWqETBNOp3nRhcwzw1A9omJT6IN5/xp8fF79PRlnHqFbg5qOlYtjJYa0Lrnz5/LCS+38PMcKGC8DGKeGwAwXBGxCfTr6Ye06uR9ehYVL/e557Gjz1uUofcrYYVuyBk8J1qXLl2klZ8PeOvWrUPVg0BwAwCZFhoZR+vP+tPKk34UEZso9xV2tqPBTUpQl5pFyMrCoGaXAAPFg09++OEHmeOM5zsrUaKEzFYPoILgBgDeKCw6nladepVTEx2fJPeVKOBAw5qWlJW6LRHUQA7hiVw5r4Znp1flbi5btuyN3RRgWhDcAECGicLc9bTsLz+K/LelppK7E/VvUIzer1SILMzNUHuQY3x8fOj999+XASU8f9nChQtp4MCBGA0Fr0FwAwBpDunm7qdlJ/zU89SUds1Fo7xLU6vybmSOoAZ0wN3dXf4vU6aMLKdQqVIlbAdIE4IbAEixoOWmfwLoxyN36Mm/89RwovAXLTlRGC01oJsRMqouJ57Z/sCBA+Th4UG5cuXC5oB0IbgBAJnh++jNEJq625ce/rtKNycKD29Wkj6sVhjz1IBOHDt2TBZM/u677yTPhpUvXx5bA94IwQ2AiQc1J+8+pXmHbtNF/zC5jxew7F3Pg0Y0K4V5akAnkpKSaPr06TR16lQZGbVo0SJZVzCt9QMB0oLgBsBE3QiMoDkHbtGRmyFy29rSnPrW96RBDYtTvlxYbBZ0IzAwkHr06EFHjx6V27wcz48//ojABrIEwQ2AiQl4Hi0zCm8670/JCpGZGVGvOh40rFlJcnG01XXxwIQdOnRIApuQkBBycHCgJUuWSIsNQFYhuAEwES/jEmnp8XsyrJtX7WYtyrnSl628qKQLkjNBt/z8/Kh169bSJVWxYkUZDeXl5YXNAm8FwQ2ACYyAWnfOX7qgwmNeLWpZp3heGtO8DNUqllfXxQMQxYsXp7Fjx9KzZ89o3rx5ZGdnh5qBt4aFMwGM2Ln7z2n6Hl+68ihcbhcv4CDrP7Wu4IaJz0Dn9u3bJ3PWcGCjSnA3435SgDRg4UwAE3crKJIWHrlDe64Gym1bK3Ma18qLetb1xKzCoHMJCQn01Vdf0ezZs6lmzZp08uRJsra2RmAD2QbdUgBG5HFYDM3ce4N2X3kV1PBJcJeaRWlM89JUwBEjoED3eAVvXsmbV/RmtWrVkhYbgOyE4AbACMTEJ9HSP+/JJS4xWYIaXibhf++VorIFsaAg6Iddu3ZRnz596MWLF+Tk5EQrV66kjz76SNfFAiOE4AbAgCUlK7T1QgD9cPC2eg2omp55aHK78lShsJOuiwcg4uPjady4cZIozLgrauPGjepcG4DshuAGwEBdeRRGk3ddp0v/zizMa0CNb12W2lREsjDoF+52OnHihFwfNWoUzZo1S3JsALQFwQ2AgQmPTqDvD9yk9ef8iVMVHKwtaHTz0tSjjgeWSwC9ohr9ZGNjI/PWXL16lTp06KDrYoEJQHADYEBdUBvP+9PMvTdlQj7WvnIhGtvaSxa5BNAXcXFx9Pnnn5OzszNNmzZN7uMuKHRDQU5BcANgAC75v5AuKNV8NaVdc9HUDhWoTvF8ui4aQAp3796lzp0708WLF2U9KF7Nu2TJkqglyFEIbgD0WEhkLE3bfYP+uPxEvWI3d0H1qYf5akD/cNfTgAEDKDIykvLly0dr165FYAM6geAGQE9zFTb/E0BT/vCl6PgkGdr9QdXCkjCM+WpA38TExNDo0aPp559/ltsNGjSgDRs2kLu7u66LBiYKwQ2AngmOiKUJ26/SkZshcrt8odw044OKVLmIs66LBpBmIO7t7U1///23JA+PHz+epkyZQpaWOLyA7mDvA9ATiUnJtOE8z1lzi8KiE8jKwoy+aFmGBjQoTubmWG8H9BMHNAMHDqQ7d+7Qb7/9Ri1atNB1kQCwcCaAPvB/Fk0jNl6iywFh6taauZ2qUBk3R10XDeA10dHR9PDhQypbtqz6Pp51OE+ePKgt0BosnAlgIJKTFVr99wNpreHcGkcbS/qsxas5aywtzHVdPIDX+Pr6UqdOnSg8PJx8fHwkcZghsAF9gm4pAB3xC31JY7ddofMPXsjtWsXy0txOlck9jz22CeilNWvW0NChQyWB2M3NjR48eKAObgD0CYIbAB201qw8eZ9+OHSLYhOSyd7agsa3KUs9aheV/AUAffPy5UsaNmwY/fLLL3KbE4g5v8bV1VXXRQNIE4IbgBz0JCyGRm3yoXP3n8vt+iXz0ayPKqG1BvQWL5nA3VA3b96USfmmTp0qI6L4OoC+QnADkEP2Xwukcduvykgobq35qm1Z6lYLrTWg33iRSw5sChUqJHPXNGrUSNdFAngjBDcAWhaXmESz99+iFSfvy+2KhZ3op25VySOfA+oe9N6iRYvIzs6OZsyYQQUKFNB1cQAyBe2KAFoU8DyaPl5yWh3YDGhQjLYNqYfABvTWpUuX6IsvvpDJ+ZiTkxMtX74cgQ0YFLTcAGjJ7itPaOzWKxQVn0R57K3o+48rU/NySMAE/cTBzJIlS2QZhfj4eCpXrhz17dtX18UCeCsIbgCyWXR8In275watP+dPfPJb0zMPzfmkMlprQG/xnDW84OXWrVvldrt27ahDhw66LhbAW0NwA5CN7j+NoqHrLtKNwAi5/Wnj4vRlSy+ywPIJoKfOnz9PnTt3pvv375OVlZUkEI8aNQrTEoBBQ3ADkE0O+wbT6E0+FBmXSC6ONjT7k8rUuDQSMEF/rVq1igYPHkwJCQnk6elJmzZtolq1aum6WADvDMENQDbkKiw6dpd+OHRbuqFqeOShBV2rUmFnO9Qt6LWSJUtSUlISffjhh7Ry5UpydsbK82AcENwAvIOouEQas9mHDlwPlts96hSlye3KkxXWhQI9FRYWpg5ieM6as2fPUvXq1dENBUYFQ8EB3mFtqE4/n5bAxsrCjL79oAJN71gRgQ3opeTkZJozZw4VK1ZMJuVTqVGjBgIbMDoIbgDewr6rgdT+p1N0/UkE5ba1pI2D6lD32h6oS9BLT58+pfbt28v8Ndxy8+uvv+q6SABahW4pgCwuejn/yB1aeOSO3K7lmZd+7FaVXHPboh5BL508eZK6du1Kjx49IhsbG1qwYAENGjRI18UC0CoENwCZFB6dQKM3+9DRmyFyu089T/q6bVmyRH4N6Gk3FA/rnjhxoiQNly5dmjZv3kyVK1fWddEAtA7BDUAmXHkURv/bcIkePIsma0tzmvFBRfq4ujvqDvTWmjVraMKECXK9R48eMvtwrly5dF0sgByB4AYgE8sojN92Veav4eHdS3tUp4ruTqg30Gu9evWijRs3UpcuXWQZBTMzM10XCSDHILgBSEdSskKzD9yipX/ek9v1SuSjxd2rkbO9NeoM9A53PfFcNX369CFra2uytLSkAwcOIKgBk4TgBiCd+WtGbvShwzdezV8zuHEJ+rxFaeTXgF4KCgqi7t2709GjR2WY99y5c+V+tNaAqUJwA5BKSEQs9Vl9nnwDIyS/ZuYHFekj5NeAnjp8+LDk1AQHB5O9vT1VrVpV10UC0DkENwAaHj6Lom7Lz9LjsBjKn8uafu5Zg6p75EEdgd5JTEykKVOm0LfffitLgFSsWFFGQ3l5eem6aAA6h+AG4F93QyKp889n6FlUPBXL70Cr+tSU/wH0zePHj6lbt2504sQJuT1w4ECZv8bODuuZATAENwBE5BMQRn1Xn6MX0Qnk5eZIv/SrRS6YmA/0VExMDF26dEmGdi9btkwm6QOA/yC4AZO392ogjdrkQ/GJyVSxsBOt7VeL8jpgRBToF+56UiUI82re3AVVokQJKlWqlK6LBqB3sLYUmLQ9VwJlcj4ObN7zcqENg+ogsAG9ExAQQI0bN5bkYZVWrVohsAFIB4IbMFmrTt6nYesvUmKyQh9ULUzLetWgXDZozAT98scff1CVKlXor7/+omHDhsl8NgCQMQQ3YJLmHbpNU3f7yvVutYvSnE8qk4U5ZnAF/REfH0+fffaZrOb9/PlzqlGjBu3bt48sLCx0XTQAvYfTVDDJwGbBv6t6f9GyDA1tUgKTnYFeefDgAXXu3JnOnTsnt0eOHCmLYPKq3gBgAC03ixYtIk9PT7K1taXatWurv8zpCQsLk6bZggULyhedV7rdu3dvjpUXDDshc9GxuxLYcF7m+NZeNKxpSQQ2oHf5NTwRH/8WOjs7044dO2j+/PkIbAAMpeVm06ZNNGbMGFq6dKkENvwFbtmyJd26dYtcXFzSbKZt3ry5PLZ161YqXLgwPXz4UH4AAN4U2PA6UYuP31O32HzauAQqDfSOu7s7tWvXju7cuSMLX3p4eOi6SAAGx0zhX30d4YCmZs2a9NNPP8nt5ORkKlKkCI0YMYLGjRv32vM5CJo9e7asnWJlZfVW7xkREUFOTk4UHh5OuXPnfufPAIYX2Exo40WDGiGwAf1x7949OUnLly+f3I6OjpbfuLf9nQMwRlk5fuusW4pbYS5cuEDe3t7/FcbcXG6fPn06zdfs2rWL6tatK91Srq6uVKFCBZoxY0aGowfi4uKkQjQvYFqBDXdDqQKbKe3LI7ABvcLz1XA3VN++fWV/ZbxGFAIbgLens+Dm6dOnEpRwkKKJb/MKt2nx8/OT7ih+HefZTJw4kX744QeaPn16uu8zc+ZMifRUF24ZAtOx6tQDmn/4VfLwuNZe1Luep66LBCBiY2NpyJAhkjgcGRkpI6Jw8gVgJAnFWcHdVpxvw9ONV69eXX4UvvrqK+muSs/48eOlCUt14WQ9MA27Lj+haf8O9x7TvDQNRo4N6Inbt29TnTp11L9d/Dt1/PhxOQEDAANOKM6fP7/M1xAcHJzifr7t5uaW5mt4hBQ31WrO81C2bFlp6eFuLmvr16fM5xFVGD5peo7fCqExm3zkep96njSiWUldFwlArFu3jj799FOKioqiAgUK0K+//ioDKQDACFpuOBDh1pcjR46kaJnh25xXk5b69evT3bt35XmaZ0Ac9KQV2IBpuuj/goatezXzcNuKBWni++Uw3Bv0AicKf/311xLYNGnShHx8fBDYABhbtxQPA1++fDmtXbuWbty4If3P/KXnxDrWq1cvaa5V4ce5X5ontOKgZs+ePZJQzAnGAOx2cCT1XX2eouKTqH7JfDS3M2YeBv3BicI8BcbkyZNlnahChQrpukgARkmn89xwzkxoaChNmjRJupZ4/ZT9+/erk4z9/f1lBJUKJwMfOHCARo8eTZUqVZJ5bjjQGTt2rA4/BeiLkIhY6rfmPIXHJFDVos60rGcNsrHEVPWgW3zyxoMg+vXrJ7dr1aolFwAw0nludAHz3BinJ2Ex1GPlWfILjSKPfPa0c2h9yuOArkrQnZcvX0qr8i+//CJ5f1euXJEZ1QFA+8dvrC0FBi86PpEGrP1HAptCTrb0a7/aCGxAp65evUqdOnWSCUe59ZnzbEqUwMSRADkFwQ0YtNiEJOq/5h/yDYyg/LmsadOndalIXntdFwtMFDeEr1y5UmZZ53lsOKdm/fr11LhxY10XDcCkILgBg5WcrNDIjZfotN8zsrE0pxW9ayKwAZ0GNr1795ah3axVq1bSJcXDvQEgZxnUJH4AmuYeuk0HrgeTlYUZrepTk6oUwQKqoDtmZmZUqlQpmYfru+++k9GcCGwAdAMtN2CQ9l4NpEXH78r1r9uWo/ol8+u6SGCirTVhYWGUJ08euT1hwgRq3749Va5cWddFAzBpaLkBg3PG7xmN2uhDPM6va62i1Kuuh66LBCaIR2zwdBY8GV9MTIzcx602CGwAdA/BDRiUwPAYybOJT0om77KuNK1Decw+DDnun3/+oWrVqtGWLVvI19eXTp06ha0AoEcQ3IDBiEtMkiHfwRFxVNIlFy3sWoUsLbALQ852Qy1cuJDq1atHfn5+5OHhQSdPniRvb29sBgA9gpwbMJiDymebL9P1JxEyMmp1n5pkb43dF3LOixcvZJbhnTt3yu2OHTvSqlWr1Pk2AKA/cNoLBmHBkTu0+0qgXP+5Z3UM+YYcN3ToUAlseJFebr3Zvn07AhsAPYVTX9B7h32Daf7hO3J9escK1KSMi66LBCZo1qxZdO/ePVqyZAlVr15d18UBgAyg5Qb0fs2oz7Zcluu963pQjzoYGQU549mzZ7RmzRr17aJFi9LZs2cR2AAYALTcgN6KiU+iwb9dkFW+K7k70Vdty+m6SGAiePRTly5d6NGjR5QvXz5q166deqI+ANB/aLkBvTVtjy9deRROzvZW9FPXamRtid0VtCs5OVlmF+a1oDiw4RmHixQpgmoHMDBouQG9tP3iI1p/1l+uc2BTNB8WwwTtCgkJoV69etGBAwfkdrdu3Wjp0qXk6OiIqgcwMNl2KswjBypVqpRdfw5M2I3ACBq3/apcH960JDUohaUVQLv+/PNPqlKligQ2tra2tGLFCvrtt98Q2ACYQnDz888/08cffyxnNJxYx44ePUpVq1alnj17Uv369bVVTjAR0fGJNGbzZYpPTKbGpQvQmOaldV0kMAGBgYFyKVu2LJ0/f5769++P/BoAU+iW4n7oSZMmSevMzZs36ffff6evvvqKfvzxRxo5ciR9+umnmPMB3klyskIjN/pIy01eB2ua/UklMjdHAidob2JIVYIwJw/Hx8fTRx99RA4ODqhyAFNpuVm9ejUtX75c1lTZt2+fLBT3999/0927d2ncuHEIbOCdLT1xjw75BpO1hTkt61mdXBxtUaugFUeOHJG1oYKCgtT3cb4NAhsAEwtu/P39qVmzZnK9YcOGZGVlRVOmTMGPAWQLn4Awmnvwtlyf1rE81fDMi5qFbJeUlCQt0M2bNycfHx/5DQMAE+6WiouLk0Q7FZ6CPG9eHIAge+azGbPZhxKTFWpbsSB1qoGht5D9njx5IvmCnDzMBgwYQD/88AOqGsDUh4JPnDiR7O1fDcnl/unp06eTk5NTiufMnTs3e0sIRu/bvb7kFxpFLo42NOODikjkhGzHo6B69OhBT58+pVy5csngCA50AMDEg5tGjRrRrVu31Lfr1atHfn5+KZ6D2Tshq/ZcCaTfzryaz+aHTpXJyd4KlQjZasuWLdSpUye5XrlyZdq8eTOVLo1ReADGLNPBzfHjx7VbEjA5Ac+jaey2K3J9UKPi1LBUAV0XCYxQq1atJJjx9vaWbijN7nUAME5Z6paKiIiQ+W24S6pWrVpUoAAORvD2w76/3HqFXsYlUnWPPPRlyzKoSsg2Z86codq1a0trMs8wzHPX5M6dGzUMYCIyPVqKRxZ4eXlRy5YtZRG5kiVLqqcpB8iqX888pNN+z8jWypzmdapClhZYNwreHZ94ff7551S3bl2aP3+++n4ENgCmJdNHlLFjx1KxYsVktdwLFy7Qe++9R8OHD9du6cAoPXgaRd/vvynXx7cui3WjIHv2qwcPJDdQNQLq8ePHqFkAE5XpbikOaA4ePCgTX7FVq1bJUHDuqsJZEWRWQlIyDd9wkaLik6imZx7qWccDlQfvbOfOndS3b18KCwsjZ2dnmXS0Y8eOqFkAE5Xplpvnz5+Tu7u7+jb/gPBsns+ePdNW2cAIrTn1gK49jiAnOyta2LUqlleAd8Lzb/HyLx988IEENpxnc+nSJQQ2ACYuSwnFvr6+KaYr57VZbty4QZGRker7sDI4ZNQd9cOhV9MJTGjjRQWd7FBZ8E74N2nx4sVy/bPPPqMZM2bIBKMAYNrMFI5QMsHc3FxGHqT1dNX9/D9Pb67PuBuNJx4MDw9Hd1oOj47qsvwMnbv/nOqVyEe/9a+NVhvIFkuXLpVW5ffffx81CmDEIrJw/M50y839+/ezo2xgwqOjOLDh0VGzPsJq3/B2YmNjZXBD//791a3EgwcPRnUCwNsFN2vXrpUhlqrlFwAyKyQiluYceNUdNa6VFxXJi30Isu727dsy0/Dly5dlcMPVq1fJ0jJLPesAYCIynVDMq+e+fPlSu6UBo8PdlcM3XKLIuESqWNiJetX11HWRwACtX7+eqlevLoENTx7Kc9ggsAGAdw5uMpmaA5DCTp/H0h1lbWEua0eZm5uhhiDToqOjaeDAgdS9e3c5uWrcuLFMKMqTiQIApCdLbbpYGBOyIiQylqb+4SvX+zbwpNKujqhAyDQemdm8eXO6du2a/PZMnDhRLmixAYBsDW548bk3BTg8Hw4A48DmRXQClS2Ymz5vgbWjIGu4+8nFxYVcXV1p3bp1Mis6AEC2Bzecd8PDsADe5NitENp9JZC4F+r7jyqRFdaOgkyIiooiCwsLWbmb/+eghrm5uaH+AEA7wU2XLl3kTAogI7EJSf91R9UvRhXdERDDm3H3E4+G4ryaJUuWyH0IagBAqwnFyLeBzFp8/B7dfxpFLo42NNK7FCoO3jhYYeXKlVSzZk2Z8XzXrl1Y1gUA3glGS0G2uhEYQUv/vCfXJ7crT7ltrVDDkC5euqVnz540YMAAmaCPR0HxaKh8+fKh1gBA+91SycnJqGbIUFxiEo3a6EPxicnUzMuF2lREngSkj+es4W4onpyP82umT59OX375pSz1AgDwLjC9J2SblSfv063gSMrnYE2zP66ErkzIcDXvNm3a0JMnT2RdqI0bN1L9+vVRYwCQLXCKBNkiNDKOFh29K9e/aluW8uWyQc1CumxsbCRpmBe75G4oBDYAkJ3QcgPZgteOiopPokruTtSxSmHUKrzmwoUL9OLFC/L29pbb7du3p3bt2qGFDwCyHVpu4J398+A5bfonQK5PfL8clliA10ZD/fjjj1SvXj3q3LkzBQS82lcYRmECgDag5QbeSXKyQpN3XZfrnWsUoZqeeVGjoMYtNf3796cdO3bI7UaNGlGuXLlQQwCgVWi5gXey+2ogXX8SQQ7WFvRlKyyxAP85e/YsVatWTQIba2trWrhwIW3fvp3y5MmDagIArULLDby1mPgkmvT7Nbk+qFEJJBGDuhtq3rx5NHbsWEpMTKTixYvT5s2bqXr16qghAMgRaLmBt7b29AMKi06QVptBjYqjJkGdR3Pz5k0JbD755BO6ePEiAhsAyFFouYG38iIqnuYdui3Xx7cpS3bWFqhJE8cTfaom4FuwYIGsEdWtWzckDQNAjkPLDbyVHw7dorjEZCrlkou61iqKWjTxoGbWrFkyZ41qJnM7Ozvq3r07AhsA0Am03ECW3Q2JpA3nXg3nndCmLFmYm6EWTVRoaCj16tWL9u/fL7d///13+uCDD3RdLAAwcWi5gSybe+g2JSUr5F3WhZp6uaAGTdSJEyeoSpUqEtjY2trSihUrqGPHjrouFgAAghvImjN+z2jv1SAyMyMa0xxDv01RUlKSLHLZtGlTWRuqbNmydP78eZnPBpPyAYA+QLcUZBq31kzb7SvXu9QsSuUK5UbtmaChQ4fSsmXL5HqfPn3op59+IgcHB10XCwBAv7qlFi1aRJ6entK0Xbt2bTp37lymXscrCfOZIprCc8bvPo9lwj5HG0v6vEXpHHpX0DdDhgyhvHnz0tq1a2n16tUIbABA7+g8uNm0aRONGTOGJk+eLPNhVK5cmVq2bEkhISEZvu7Bgwf0+eefU8OGDXOsrKYsNiFJcm3YkKaYsM/UuqFOnz6tvs15Ng8fPpREYgAAfaTz4Gbu3Lk0cOBA6tu3L5UrV46WLl1K9vb2tGrVqgx/bHmY6ZQpU2T2U9C+NX8/oEcvYsg1tw31rVcMVW4iOKfmvffekzlrOK9GBetDAYA+02lwEx8fTxcuXCBvb+//CmRuLrc1zxRTmzp1Krm4uEgCI2jf05dxtOjYXbn+ZUsvTNhnIg4cOCCtNH/++SfZ2NhIoAMAYAh0mlD89OlTaYVxdXVNcT/f5unb03Ly5ElauXIl+fj4ZOo94uLi5KISERHxjqU2PQuP3KHI2EQqXyg3daxaWNfFAS3jZRMmTpxI3333ndzmrmJeG6p0aeRZAYBh0Hm3VFZERkZSz549afny5ZQ/f/5MvWbmzJnk5OSkvhQpUkTr5TQmfqEv6bczD+X6V5iwz+gFBARQkyZN1IENj4w6c+YMAhsAMCg6bbnhAMXCwoKCg4NT3M+33dzcXnv+vXv3JJG4Xbt26vtU071bWlrSrVu3qESJEileM378eElY1my5QYCTeT//6UfJClEzLxeqVzJzASUYru3bt9OpU6cod+7cMikfL3wJAGBodBrcWFtby2rBR44cUQ/n5mCFbw8fPvy153t5edHVq1dT3Pf1119Liw4v1JdW0MK5AnyBrPN/Fk07Lj2W60ObpAwawTiNGDFCcmsGDRr02okCAICh0Pkkftyq0rt3b6pRowbVqlWL5s+fT1FRUTJ6ivFw08KFC0v3Es+DU6FChRSvd3Z2lv9T3w/v7vsDNyk+KZkalMxPNTzzokqNEA/p5vyaxYsXywgoTujnRTABAAyZzoObzp07y+J7kyZNoqCgIPVaNaokY39/f/nBhZx1JziSdl8JlGUWxrfxQvUbIV7kkmcYDgsLk8CGAxwAAGNgpiiKQiaEc244sTg8PFzyCiBtQ9ddkDWkWpZ3pZ971kA1GRGeguHLL7+UrlzGLaY8mSbPEg4AYAzHbzSJwGtuBkWoF8cc3RzDf42Jn58f1a9fXx3YfPbZZ/TXX38hsAEAo6LzbinQP0uO35P/W1dwIy83tG4Zi+PHj1OHDh3k7Ee1NtT777+v62IBAGQ7BDeQwoOnUfTH5Vcz0Q5tUhK1Y0TKlCkjSfkVK1akDRs2YEoEADBaCG4gBV5mgee1aVKmAFUo7ITaMXA8C7hqwsuCBQvKUgo8xNvKykrXRQMA0Brk3IDavdCXtO3iI7k+8r1SqBkDx60zvLDs1q1bU8wVhcAGAIwdghtQ++HgLWm1ec/LhaoWzYOaMVAxMTEyCV+3bt1kgstffvlF10UCAMhRCG5A3Aj8b4TUF63KoFYMFC84W7t2bVl/zczMTCbo4yUVAABMCXJuQPx07K7836ZiQYyQMlDcQjNkyBCKjo6WSTB/++038vb21nWxAAByHIIbkJW/914NlJoY3hQjpAzRxYsXZRkT1qxZM1q3bl2ai88CAJgCBDcgK38r/+balC2IeW0MUbVq1WRCPp69c8KECWRhYaHrIgEA6AyCGxMXFB5LO3xerfw9GCt/GwxeNYW7od577z1yd3eX++bMmaPrYgEA6AUkFJu4dWcfUnxiMtXwyCMX0H88Aqpnz56y6GXXrl0pMTFR10UCANAraLkxYRzUbDgXINf71i8mo2tAv12+fJk6depEt2/flq6ntm3bkrk5zlEAADQhuDFh2y8+oqcv48jF0YZalHfVdXHgDd1Qy5Yto5EjR1JcXJx0RW3cuFEWwQQAgJQQ3Jio5GSFlvz5aoHMQY2Kk5UFzv71uRtqwIABtHnzZrnNi12uWbOG8uXLp+uiAQDoJRzRTNShG8H08Fk05ba1pG61i+q6OJAB7n7y9fUlS0tLSRretWsXAhsAgAyg5cZErTp5X/7vXseD7K2xG+hjNxRfOJ/G3t5eWm3Cw8OpTp06ui4aAIDeQ8uNCbr+JJzO3n9OFuZm1Kuuh66LA6mEhYXRxx9/TLNmzVLfV7ZsWQQ2AACZhODGBK38t9WGl1oo6GSn6+KAhnPnzlHVqlVlPahp06ZRcHAw6gcAIIsQ3JgYHh21+/KrpRb61ffUdXHgX9wFNW/ePGrQoAE9ePCAihcvTidOnJA1ogAAIGuQbGFitvzziOKTkqmSuxNVKeKs6+IAET1//lwm5Pvjjz+kPrhLasWKFbKUAgAAZB2CGxMb/s0zErMetT0waZ8eiI+Pl1yaO3fukI2NjbTeDB48GNsGAOAdoFvKhPx19yk9ehFDjraW1K5yIV0XB4jI2tqaRo0aRaVKlaIzZ87QkCFDENgAALwjBDcm5NfTD+T/j6q5k501Vo3WladPn8q8NSoc0Pj4+FCVKlV0ViYAAGOC4MaEEomP3gyR6z3qYNI+Xfnrr7+ocuXK1K5dO5m3hvGaXjyXDQAAZA8ENybij8tPKFkhSSQu6eKo6+KYnOTkZPr222+pSZMm9OTJE+mOCg0N1XWxAACMEhKKTcTWC4/UXVKQs3iump49e9KhQ4fkdu/evWnRokXk4OCATQEAoAUIbkzAzaAIuv4kgqwtzJFInMOOHj1K3bt3p6CgIOl6Wrx4sQQ3AACgPQhuTMCOS4/l/8ZlClBeB2tdF8ek8NBuDmzKly8v60OVK1dO10UCADB6yLkxgZlv//B5Itc/qlZY18UxOatXr6bPP/9cllVAYAMAkDMQ3Bi5y4/C6Ul4LNlamVOTMi66Lo7RO3jwoAQzKvnz56fZs2djNBQAQA5Ct5SR2/lvl1TL8m5ka4W5bbQlMTGRJk+eTDNnzpTWsnr16tGHH36otfcDAID0IbgxYolJyTIEnHWsgi4pbXn06BF169ZN5rBhvHxC69attfZ+AACQMQQ3RuzUvWf0LCpekogblsqv6+IYpb1791KvXr3o2bNn5OjoKAtedurUSdfFAgAwaci5MWKqVpvWFdzI0gKbOrvNmDGD2rZtK4FN9erV6dKlSwhsAAD0AI54RiomPokOXAuS6+2xSKZWcEDDSyeMGDGCTp06RSVKlNDOGwEAQJagW8pI7b0aSJFxieSex45qeubVdXGMRkhICLm4vBp11rJlS7p+/TqVLVtW18UCAAANaLkxUtsvvVpuoVONImRubqbr4hi8+Ph4Gj16NJUpU4b8/PzU9yOwAQDQPwhujFBIZCz9fe+ZXP+gKkZJvav79+9TgwYNaP78+RQWFkb79u3Lhq0EAADaguDGCP1xOZAUhahyEWcqktde18UxaNu2baOqVavS+fPnKW/evLRr1y4aNmyYrosFAAAZQHBjxKOksNzC24uNjaXhw4fTxx9/TOHh4TIpH4+GateuXbZtJwAA0A4EN0bm0Yto8gkIIzMzolbl3XRdHIO1cOFCWrRokVwfO3YsHT9+nIoWLarrYgEAQCZgtJSRLrdQu1hecsltq+viGKyRI0fSsWPH6H//+x9mGwYAMDBouTEivKbRrn+7pD6s6q7r4hiUmJgYmjNnjqwRxWxsbCRxGMsoAAAYHrTcGBHfwAi6HfySrC3MqWUFdEll1s2bN2Vm4atXr8poqOnTp2t1OwEAgHah5caIbL/4qkvKu5wLOdlZ6bo4BuHXX3+lGjVqSGDj6upKTZo00XWRAADgHSG4MRK8AvjvPq+Cmw/QJfVGUVFR1K9fP1n0kq83a9aMfHx8yNvbW/sbCwAAtArBjZE4fCOEnr58tQJ4kzIFdF0cvXbjxg2qVasWrV69mszNzWnKlCl08OBBcnNDVx4AgDFAzo2R2HTeX73cghVWAM9QcnKyzDpcsGBBWr9+PbqiAACMDIIbI1lu4c/boXK9Uw2MkkpLUlISWVhYyPXy5cvTjh07ZOZh1SKYAABgPNAtZQT2XQ2iZF5uwd2JihfIpevi6J3Lly9TpUqV6OTJk+r7eEVvBDYAAMYJwY0R2HMlUP5vV7mQrouid/P+/Pzzz1S7dm3y9fWlL774Qu4DAADjhuDGwIVExNL5h8/lepuKBXVdHL0RERFBXbt2pcGDB1NcXBy1adOG/vjjDzLjdSkAAMCoIbgxcHuuvloBvEoRZyrkbKfr4uiFixcvUvXq1WnTpk1kaWlJs2fPlsAmf/78ui4aAADkACQUG7idPq+WW+hQBV1S7Nq1a1S3bl2Kj4+XhS43btwotwEAwHQguDFgj8Ni6PK/K4C3rYQuKdVIqPfff1/WiOJ5bPLmzavrzQQAAKbYLbVo0SLy9PQkW1tbSf48d+5cus9dvnw5NWzYkPLkySMXnlE2o+cbsy3/BPy3Arij6a4A/s8//1B4eLhc55ya3377jXbu3InABgDAROk8uOG8iDFjxtDkyZMlV6Jy5coyTDckJCTN5x8/flwSRY8dO0anT5+mIkWKUIsWLejx41dLD5jUCuD/dknxxH2miOtg3rx5VK9ePRo0aJB6JJSdnR0ShwEATJjOg5u5c+fSwIEDqW/fvlSuXDlaunQp2dvb06pVq9J8/rp162jo0KFUpUoV8vLyohUrVsiMs0eOHCFTcv7BC/J7GkV2VhbUorzpLRvw/Plz6tixowTGCQkJsg9wng0AAIBOgxs+GF24cCHFYoW81g/f5laZzIiOjpaDm6nlVvxy+oE6kTiXjWmlTvG+wcHtrl27yNraWro1N2/eTDY2NrouGgAA6AGdHhWfPn0q0+K7urqmuJ9v37x5M1N/Y+zYsVSoUKF0V3PmOU74ojn/iaELjYyjg9eD5XqPOh5kKrh1Zs6cOTRhwgTZb0qWLClBDS+jAAAAoDfdUu/iu+++k6G+vE4QJyOnZebMmeTk5KS+cI6Oodtwzp/ik5JlbpsKhZ3IVISFhdGCBQsksOG8K87RQmADAAB6FdzwpGq8mGFw8KtWCBW+7eaWcR4Jn8FzcHPw4EFZNyg948ePl5E0qktAwKsRRoYqOVmhTedffYbe9Uyn1YZx1+OGDRto2bJlknvl6Oio6yIBAIAe0mlww/kSPJOsZjKwKjk4o4nXvv/+e5o2bRrt37+fatSokeF7cB5G7ty5U1wM2dn7z2V+G0dbS2pdwbjntuF94dtvv5Wh3SqNGjWSBHQsowAAAOnReSYqj3bp3bu3BCm1atWi+fPnU1RUlIyeYr169aLChQtL9xKbNWsWTZo0idavXy9z4wQFBcn9uXLlkoux23rhkfzftmJBsrWyIGPFrXc9e/akQ4cOyei5pk2byn4AAACg98FN586dKTQ0VAIWDlR4FAy3yKiSjP39/WUElcqSJUtklNXHH3+c4u/wPDnffPMNGbPYhCQ6eP1VMPdBVeM90PMcRt26dZP9gees+emnnyRpHAAAIDPMFNXMZyaCR0txYjHn3xhaF9WeK4E0bP1FKuxsR3992ZTMzY1rhWtOFJ4+fTpNnTpVuqR4KQUeDcXzHwEAgGmLyMLxW+ctN5B5e68Fyv/vVy5odIENrwXVqlUrdf5V//79aeHChdIlBQAAYDJDwU1JZGwCHfYNVufbGBtLS0uqWbMmOTg4SAIxzzyNwAYAAN4GghsDceB6MMUlJlPx/A5U0UjmtuHWGs63UuHuqMuXL1P37t11Wi4AADBsCG4MxN6rr7qk2lUuZBTDoB89eiQjoNq2bateE8rKyopKlCih66IBAICBQ3BjIMst/Hk7VB3cGLq9e/fKqLiTJ0/KMhvXrl3TdZEAAMCIILgxAPuvB1FSskKV3Z2opIvhzuXDC5x++eWX0lrz7NkzqlatmiyhwP8DAABkF4yWMgB7rjyR/9sYcCLxw4cPqUuXLnTmzBm5PWLECJo9ezZW8gYAgGyH4EbPBYbHyJILrG0lww1uBgwYIIENz1GwatUq+vDDD3VdJAAAMFLoltJz+64GEU+zWNMzD7nnMdw5X3hmaW9vb7p06RICGwAA0CoEN3ruyM1Xc9u0KJfxKun65v79+zJXjUrJkiVlnahixYrptFwAAGD80C2lx55HxdMZv1ddUi3Kv1pryxBs27ZNZhjmqbJ5cVNusQEAAMgpaLnRYwf/HSVVrmBu8sjnQPouNjaWhg8fLoua8tofderUoVKlSum6WAAAYGIQ3Oix332eqNeS0nd3796levXq0aJFi+Q2D/n+888/ycPDQ9dFAwAAE4NuKT0VFB5LZ+4/k+vtKun3xH1btmyRbqjIyEjKly8f/fLLL9SmTRtdFwsAAEwUghs9te9aoIySquGRh4rk1e9RUi9fvpTApmHDhrR+/Xpyd3fXdZEAAMCEIbjRU3uuvFpLqrWeTtzHi17ySt6sT58+lCtXLvrggw/U9wEAAOgKcm700OOwGPrn4Qu53lYPg5tff/2VKlWqJEsoMF7I85NPPkFgAwAAegHBjR763eex/F+7WF5yc7IlfREVFUX9+vWjXr160Y0bN2jhwoW6LhIAAMBr0Iegh36/9GqU1AdVC5O+uH79OnXq1Il8fX2lpWby5Mn09ddf67pYAAAAr0Fwo2duBEbQreBIsrIwo9YVdN8lpSgKrVmzhoYNG0YxMTHk5uYmScNNmzbVddEAAADShG4pPbPz3y6pZl4u5GRvpevi0OLFi6UrigOb5s2bk4+PDwIbAADQawhu9EhiUjJtu/BYr7qkunfvLutCffvtt7R//35ydTWcZSAAAMA0oVtKj5y694yevoyjPPZW9F5ZV511Qx0+fFjWg+LcGmdnZ7p69SrZ2upPYjMAAEBG0HKjh6Ok2lYqSFYWOb9peKHLbt26UYsWLWj58uXq+xHYAACAIUHLjZ6ITUiiA9eC5HrHKjnfJXXp0iUZDcVrRPFEfJxjAwAAYIgQ3OiJQ77BFBWfRO557Ki6R54c7YbipOExY8ZQfHw8FS1alDZu3Eh169bNsTIAAABkJwQ3etYlxa02nOuSE8LCwmjAgAG0bds2ud2+fXtavXo15c2bN0feHwAAQBuQc6MHOIn42K1Qud6xas6tAM6Jwjt27CArKyuaN28e7dy5E4ENAAAYPLTc6IF914IoKVmhSu5OVNLFMcfel1fx/umnn6hGjRpUs2bNHHtfAAAAbULLjR7YeuGR/N+uknZbbZ4/fy6joW7duqW+b8iQIQhsAADAqKDlRsf8Ql/S5YAwsjA3ow+qaW+U1OnTp6lLly7k7+8vI6LOnj2bY7k9AAAAOQktNzq26/KrRTLrl8xP+XPZZPvfT05OptmzZ1OjRo0ksClRogQtXboUgQ0AABgttNzoEA/D3nlJNUoq+7uknj59Sr1796a9e/fK7c6dO9OyZcsod+7c2f5eAAAA+gLBjQ5dfRxOD55Fk52VBbUs75atf5u7npo0aUKPHz+WGYYXLFhAAwcORIsNAAAYPQQ3OrTnSqD839SrADnYZO+m8PDwkEuuXLlo8+bNVKlSpWz9+wAAAPoKwY2O8NDvnf9O3Jddo6RCQ0PJycmJrK2tZe6arVu3kqOjowQ4AAAApgIJxTpy+t4zCo6Io9y2ltSsrMs7/71jx45J68yECRPU9xUsWBCBDQAAmBwENzqy499E4vcrFyIbS4u3/jtJSUk0ZcoU8vb2pqCgINq/fz9FR0dnY0kBAAAMC4IbHYiJT6KD1999BfDAwEBq0aIFffPNNzLku1+/fnTu3Dmyt7fPxtICAAAYFuTc6MDhG8EUGZdIhZ3tqMZbrgB+6NAh6tGjB4WEhJCDgwMtWbKEevbsme1lBQAAMDQIbnTgd58n6kUyzc3N3mo1708++YTCw8OpYsWKMhrKy8tLCyUFAAAwPAhuclhUXCKduPNqBfD333KUlLOzs8wyzEnE8+fPJzs7u2wuJQAAgOFCcJPDjtwMofjEZPLMZ09ebplfAXzfvn0yGV/Tpk3lNq8TxRcAAABICQnFOloBvG2lgpmaLTghIYHGjh1Lbdq0oa5du1JwcHAOlBIAAMBwoeUmBwWGx9DJf7ukOtUo8sbn80KX3DrDK3qzjz/+WCbpAwAAgPQhuMlBm88/omSFqFaxvOSRzyHD5+7atYv69OlDL168kIBm5cqV9NFHH+VYWQEAAAwVuqVycLmFTef95XrXWkUynJRvzJgx1KFDBwlsatasSRcvXkRgAwAAkEkIbnLIX3dC6Ul4LDnbW1HrCgXT3yDm5jJ3DRs1ahSdPHmSihcvnlPFBAAAMHjolsoh68/6q2cktrV6fbmFxMREsrS0lCRjnpCve/fu1Lp165wqHgAAgNFAy00OeBIWI7MSs+61i6Z4LC4ujkaMGCHdToqiyH28kjcCGwAAgLeDlpscsOGcvyQS1ymel0q5/je3zd27d6lz586SU8O4C6phw4Y5USQAAACjhZYbLYtNSFJ3SfWo46G+f9OmTVStWjUJbPLly0e7d+9GYAMAAJANENxo2ZYLj+hZVDwVcrKlluXdKCYmhgYPHizz10RGRlKDBg3Ix8eH2rZtq+2iAAAAmAQEN1oUl5hES47dleuDGhUnKwtzCWp+/vlnSRyeMGGCrA/l7u6uzWIAAACYFOTcaNG2C49l+LeLow11qfUqkZgDmgsXLtCqVauoRYsW2nx7AAAAk4TgRkueR8XT3EO3KDkhlprkjlQP/65duzbdu3ePbGxstPXWAAAAJg3dUlrAQ7on/X6Nnjy4S8/Xf04/ftmXrly5on4cgQ0AAICRBzeLFi0iT09PsrW1lZaNc+fOZfj8LVu2kJeXlzy/YsWKtHfvXtInPDpq42+/UNDa0RQV9ICcnZ0pIiJC18UCAAAwCToPbnhINK+lNHnyZBkWXblyZWrZsqV6CYLU/v77b+ratSv179+fLl26RB07dpTLtWvXSB8cuvyAhgzsR8/2LSAlMY6aN28uo6F4VBQAAABon5mimhZXR7ilhheH/Omnn+R2cnIyFSlSRGbtHTdu3GvP50nvoqKiZF4YlTp16lCVKlVo6dKlb3w/bkHhVbbDw8Mpd+7c2fpZNh04Sb17dKO4pwFkZmZOU6dOkQRiXi8KAAAA3l5Wjt86PerGx8fLyCFvb+//CmRuLrdPnz6d5mv4fs3nM27pSe/5vLwBV4jmRRsO+QbTsBk/S2Bj65SfDh45TF9//TUCGwAAgBym0+Dm6dOnlJSURK6urinu59tBQUFpvobvz8rzZ86cKZGe6sKtQtpQrlBuKti4K1Vu149uXLtC3k2bauV9AAAAIGNG318yfvx4acJSXQICArTyPoWd7ej3EY3o4s4V5OleUCvvAQAAAHo+z03+/PnJwsKCgoNfrZitwrfd3NzSfA3fn5Xn87DrnBp6XbxArhx5HwAAANDTlhtra2uqXr06HTlyRH0fJxTz7bp166b5Gr5f8/ns0KFD6T4fAAAATIvOZyjmYeC9e/emGjVqUK1atWj+/PkyGqpv377yeK9evahw4cKSO8NGjhxJjRs3ph9++EEWm9y4cSP9888/tGzZMh1/EgAAANAHOg9ueGh3aGgoTZo0SZKCeUj3/v371UnD/v7+KUYc1atXj9avXy8jkXiYdalSpWjnzp1UoUIFHX4KAAAA0Bc6n+cmp2lznhsAAAAw8XluAAAAALIbghsAAAAwKghuAAAAwKgguAEAAACjguAGAAAAjAqCGwAAADAqCG4AAADAqCC4AQAAAKOC4AYAAACMis6XX8hpqgmZeaZDAAAAMAyq43ZmFlYwueAmMjJS/i9SpIiuiwIAAABvcRznZRgyYnJrSyUnJ9OTJ0/I0dGRzMzMsj2q5KApICAA61ZpEeo5Z6CeUc/GBvu0Ydczhysc2BQqVCjFgtppMbmWG64Qd3d3rb4Hb0wsyql9qOecgXpGPRsb7NOGW89varFRQUIxAAAAGBUENwAAAGBUENxkIxsbG5o8ebL8D9qDes4ZqGfUs7HBPm069WxyCcUAAABg3NByAwAAAEYFwQ0AAAAYFQQ3AAAAYFQQ3AAAAIBRQXCTRYsWLSJPT0+ytbWl2rVr07lz5zJ8/pYtW8jLy0ueX7FiRdq7d++7bC+TkZV6Xr58OTVs2JDy5MkjF29v7zduF8h6PWvauHGjzPDdsWNHVGU2788sLCyMhg0bRgULFpQRJ6VLl8Zvhxbqef78+VSmTBmys7OTGXVHjx5NsbGx2KczcOLECWrXrp3MEsy/ATt37qQ3OX78OFWrVk325ZIlS9KaNWtI63i0FGTOxo0bFWtra2XVqlXK9evXlYEDByrOzs5KcHBwms8/deqUYmFhoXz//feKr6+v8vXXXytWVlbK1atXUeXZWM/dunVTFi1apFy6dEm5ceOG0qdPH8XJyUl59OgR6jkb61nl/v37SuHChZWGDRsqHTp0QB1ncz3HxcUpNWrUUNq0aaOcPHlS6vv48eOKj48P6job63ndunWKjY2N/M91fODAAaVgwYLK6NGjUc8Z2Lt3r/LVV18p27dv55HWyo4dOzJ6uuLn56fY29srY8aMkePgjz/+KMfF/fv3K9qE4CYLatWqpQwbNkx9OykpSSlUqJAyc+bMNJ/fqVMnpW3btinuq127tvLpp5++7fYyCVmt59QSExMVR0dHZe3atVospWnWM9dtvXr1lBUrVii9e/dGcKOFel6yZIlSvHhxJT4+Pmsb1MRltZ75uc2aNUtxHx+A69evr/WyGgvKRHDz5ZdfKuXLl09xX+fOnZWWLVtqtWzolsqk+Ph4unDhgnR5aK5TxbdPnz6d5mv4fs3ns5YtW6b7fHi7ek4tOjqaEhISKG/evKjSbNyf2dSpU8nFxYX69++PutVSPe/atYvq1q0r3VKurq5UoUIFmjFjBiUlJaHOs7Ge69WrJ69RdV35+flJ11+bNm1Qz9lIV8dBk1s48209ffpUflz4x0YT375582aarwkKCkrz+Xw/ZF89pzZ27FjpD079hYJ3q+eTJ0/SypUrycfHB1WpxXrmg+zRo0epe/fucrC9e/cuDR06VAJ2nvUVsqeeu3XrJq9r0KCBrDadmJhIgwcPpgkTJqCKs1F6x0FeOTwmJkbynbQBLTdgVL777jtJdt2xY4ckFUL2iIyMpJ49e0rydv78+VGtWpScnCytY8uWLaPq1atT586d6auvvqKlS5ei3rMRJ7lyi9jixYvp4sWLtH37dtqzZw9NmzYN9WwE0HKTSfyDbmFhQcHBwSnu59tubm5pvobvz8rz4e3qWWXOnDkS3Bw+fJgqVaqE6szG/fnevXv04MEDGSWheRBmlpaWdOvWLSpRogTq/B3rmfEIKSsrK3mdStmyZeUMmLtfrK2tUc/ZUM8TJ06UgH3AgAFym0ezRkVF0aBBgySY5G4teHfpHQdz586ttVYbhq2XSfyDwmdRR44cSfHjzre5fzwtfL/m89mhQ4fSfT68XT2z77//Xs649u/fTzVq1EBVZvP+zNMZXL16VbqkVJf27dtT06ZN5ToPo4V3r2dWv3596YpSBY/s9u3bEvQgsMme/VmVm5c6gFEFlFhyMfvo7Dio1XRlIxxqyEMH16xZI0PaBg0aJEMNg4KC5PGePXsq48aNSzEU3NLSUpkzZ44MUZ48eTKGgmuhnr/77jsZArp161YlMDBQfYmMjMz+ncCE6zk1jJbSTj37+/vLaL/hw4crt27dUnbv3q24uLgo06dPf8ctbtyyWs/8e8z1vGHDBhmufPDgQaVEiRIyyhXSx7+rPO0GXziEmDt3rlx/+PChPM51zHWdeij4F198IcdBnrYDQ8H1EI/RL1q0qBxMeejhmTNn1I81btxYfvA1bd68WSldurQ8n4fD7dmzRwelNu569vDwkC9Z6gv/eEH21XNqCG60sz+zv//+W6aN4IM1Dwv/9ttvZRg+ZF89JyQkKN98840ENLa2tkqRIkWUoUOHKi9evEA1Z+DYsWNp/t6q6pb/57pO/ZoqVarIduH9efXq1Yq2mfE/2m0bAgAAAMg5yLkBAAAAo4LgBgAAAIwKghsAAAAwKghuAAAAwKgguAEAAACjguAGAAAAjAqCGwAAADAqCG4AAADAqCC4AQC916dPHzIzM3vtwmswaT7GawyVLFmSpk6dSomJierVnzVfU6BAAWrTpo2slQUAxgnBDQAYhFatWlFgYGCKS7FixVI8dufOHfrss8/om2++odmzZ6d4Pa9czs85cOAAxcXFUdu2bWWVbQAwPghuAMAg2NjYkJubW4qLahVn1WMeHh40ZMgQ8vb2pl27dqV4vYuLizynWrVqNGrUKAoICKCbN2/q6NMAgDYhuAEAo2NnZ5duq0x4eDht3LhRrnM3FgAYH0tdFwAAIDN2795NuXLlUt9u3bo1bdmyJcVzeB3gI0eOSNfTiBEjUjzm7u4u/0dFRcn/7du3Jy8vL1Q+gBFCcAMABqFp06a0ZMkS9W0HB4fXAp+EhARKTk6mbt26Sd6Npr/++ovs7e3pzJkzNGPGDFq6dGmOlh8Acg6CGwAwCBzM8EiojAIf7mYqVKgQWVq+/tPGycfOzs5UpkwZCgkJoc6dO9OJEydyoOQAkNOQcwMARhP4FC1aNM3AJrVhw4bRtWvXaMeOHTlSPgDIWQhuAMDkcPfUwIEDafLkyZKnAwDGBcENAJik4cOH040bN15LSgYAw2em4LQFAAAAjAhabgAAAMCoILgBAAAAo4LgBgAAAIwKghsAAAAwKghuAAAAwKgguAEAAACjguAGAAAAjAqCGwAAADAqCG4AAADAqCC4AQAAAKOC4AYAAACMCoIbAAAAIGPyf/mzukhR7Is/AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Build a test set of real data background vs. data-like MC background\n", + "# to check whether the classifier has learned MC/data artefacts.\n", + "df_mc_bkg_test_diag = df_mc_bkg_test.copy()\n", + "df_data_bkg_test_diag = df_bigpreselectblind_test.copy()\n", + "df_mc_bkg_test_diag ['MC_label'] = 1.0\n", + "df_data_bkg_test_diag['MC_label'] = 0.0\n", + "\n", + "df_dm_diag = pd.concat([df_data_bkg_test_diag, df_mc_bkg_test_diag],\n", + " ignore_index=True, sort=False)\n", + "X_dm_diag = df_dm_diag.drop(columns=['InvM','PhiKK','MC_label'])\n", + "y_dm_diag = df_dm_diag['MC_label'].to_numpy()\n", + "X_dm_diag = pd.DataFrame(scaler.transform(X_dm_diag),\n", + " columns=X_dm_diag.columns, index=X_dm_diag.index)\n", + "\n", + "dm_diag_loader = DataLoader(\n", + " TensorDataset(torch.from_numpy(X_dm_diag.to_numpy().astype(np.float32))),\n", + " batch_size=bsize, shuffle=False)\n", + "\n", + "scores_diag = mytools.test_clas(dm_diag_loader, final_clas_adv, device)\n", + "scores_diag = torch.sigmoid(torch.tensor(scores_diag)).numpy()\n", + "\n", + "scores_data_diag = scores_diag[y_dm_diag == 0]\n", + "scores_mc_diag = scores_diag[y_dm_diag == 1]\n", + "\n", + "# Unit-normalised score histograms + MC/data ratio\n", + "bins = np.linspace(0, 1, 51)\n", + "bin_centers = 0.5 * (bins[:-1] + bins[1:])\n", + "bin_widths = np.diff(bins)\n", + "\n", + "data_counts, _ = np.histogram(scores_data_diag, bins=bins)\n", + "mc_counts, _ = np.histogram(scores_mc_diag, bins=bins)\n", + "\n", + "data_total = np.sum(data_counts)\n", + "mc_total = np.sum(mc_counts)\n", + "\n", + "data_density = data_counts / (data_total * bin_widths)\n", + "mc_density = mc_counts / (mc_total * bin_widths)\n", + "\n", + "ratio = np.divide(mc_density, data_density,\n", + " out=np.full_like(mc_density, np.nan, dtype=float),\n", + " where=data_density > 0)\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(\n", + " 2, 1, figsize=(7, 7), sharex=True,\n", + " gridspec_kw={'height_ratios': [3, 1], 'hspace': 0.05}\n", + ")\n", + "\n", + "ax1.step(bin_centers, data_density, where='mid',\n", + " label='Real data', color='steelblue', linewidth=1.5)\n", + "ax1.step(bin_centers, mc_density, where='mid',\n", + " label='Data-like MC (wab+tritrig)', color='tomato', linewidth=1.5)\n", + "ax1.set_ylabel('Normalised counts (a.u.)')\n", + "ax1.set_yscale('log')\n", + "ax1.set_title('Score distribution: data vs. data-like MC\\n(final_clas_adv)')\n", + "ax1.legend()\n", + "\n", + "ax2.step(bin_centers, ratio, where='mid', color='black', linewidth=1.2)\n", + "ax2.axhline(1.0, linestyle='--', color='gray', linewidth=1.0)\n", + "ax2.set_xlabel('Classifier score')\n", + "ax2.set_ylabel('MC/data')\n", + "ax2.set_ylim(0.0, 4.0)\n", + "\n", + "plt.show()\n", + "\n", + "# ROC curve for data vs. MC (diagnostic — should be close to random)\n", + "fpr_dm, tpr_dm, _ = sklearn.metrics.roc_curve(y_dm_diag, scores_diag, pos_label=1)\n", + "auc_dm = sklearn.metrics.auc(fpr_dm, tpr_dm)\n", + "print(f'Data-vs-MC AUROC (ideally ~0.5): {auc_dm:.4f}')\n", + "\n", + "plt.figure()\n", + "plt.plot(fpr_dm, tpr_dm, label=f'Data vs MC AUROC: {auc_dm:.4f}')\n", + "plt.plot([0,1],[0,1],'k--', label='Random')\n", + "plt.xlabel('FPR')\n", + "plt.ylabel('TPR')\n", + "plt.title('ROC – data vs. data-like MC (diagnostic)')\n", + "plt.legend()\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tools/simp-search-tools/ann-training/data_process_branches_only.ipynb b/tools/simp-search-tools/ann-training/data_process_branches_only.ipynb new file mode 100755 index 00000000..e2b26075 --- /dev/null +++ b/tools/simp-search-tools/ann-training/data_process_branches_only.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "id": "ef738aaa", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pylab as plt\n", + "#import ROOT\n", + "import uproot\n", + "\n", + "mass = 45\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffe21612", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "mass = 45" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b2496581", + "metadata": {}, + "outputs": [], + "source": [ + "def make_df(file_path):\n", + " df = pd.DataFrame()\n", + "\n", + " tritrig_file = uproot.open(file_path)\n", + "\n", + " # Open ROOT tree\n", + " tree = tritrig_file['preselection']\n", + "\n", + " # Access branches\n", + " B_vertex = tree['vertex.']\n", + " B_ele = tree['ele.']\n", + " B_pos = tree['pos.']\n", + "\n", + " arr = B_vertex['vertex.pos_'].array()\n", + " df['vertex_pos_x'] = np.array(arr['fX'])\n", + " df['vertex_pos_y'] = np.array(arr['fY'])\n", + " df['vertex_pos_z'] = np.array(arr['fZ'])\n", + "\n", + " df['InvM'] = np.array(B_vertex['vertex.invM_'].array())\n", + " df['psum'] = np.array(tree['psum'].array())\n", + "\n", + " df['ele_track_n_hits'] = np.array(B_ele['ele.track_.n_hits_'].array())\n", + " df['ele_track_d0'] = np.array(B_ele['ele.track_.d0_'].array())\n", + " df['ele_track_phi0'] = np.array(B_ele['ele.track_.phi0_'].array())\n", + " df['ele_track_z0'] = np.array(B_ele['ele.track_.z0_'].array())\n", + " df['ele_track_tan_lambda'] = np.array(B_ele['ele.track_.tan_lambda_'].array())\n", + " df['ele_track_px'] = np.array(B_ele['ele.track_.px_'].array())\n", + " df['ele_track_py'] = np.array(B_ele['ele.track_.py_'].array())\n", + " df['ele_track_pz'] = np.array(B_ele['ele.track_.pz_'].array())\n", + " df['ele_track_chi2'] = np.array(B_ele['ele.track_.chi2_'].array())\n", + " df['ele_track_x_at_ecal'] = np.array(B_ele['ele.track_.x_at_ecal_'].array())\n", + " df['ele_track_y_at_ecal'] = np.array(B_ele['ele.track_.y_at_ecal_'].array())\n", + " df['ele_track_z_at_ecal'] = np.array(B_ele['ele.track_.z_at_ecal_'].array())\n", + "\n", + " df['pos_track_n_hits'] = np.array(B_pos['pos.track_.n_hits_'].array())\n", + " df['pos_track_d0'] = np.array(B_pos['pos.track_.d0_'].array())\n", + " df['pos_track_phi0'] = np.array(B_pos['pos.track_.phi0_'].array())\n", + " df['pos_track_z0'] = np.array(B_pos['pos.track_.z0_'].array())\n", + " df['pos_track_tan_lambda'] = np.array(B_pos['pos.track_.tan_lambda_'].array())\n", + " df['pos_track_px'] = np.array(B_pos['pos.track_.px_'].array())\n", + " df['pos_track_py'] = np.array(B_pos['pos.track_.py_'].array())\n", + " df['pos_track_pz'] = np.array(B_pos['pos.track_.pz_'].array())\n", + " df['pos_track_chi2'] = np.array(B_pos['pos.track_.chi2_'].array())\n", + " df['pos_track_x_at_ecal'] = np.array(B_pos['pos.track_.x_at_ecal_'].array())\n", + " df['pos_track_y_at_ecal'] = np.array(B_pos['pos.track_.y_at_ecal_'].array())\n", + " df['pos_track_z_at_ecal'] = np.array(B_pos['pos.track_.z_at_ecal_'].array())\n", + "\n", + " df['vertex_chi2'] = np.array(B_vertex['vertex.chi2_'].array())\n", + " df['vtx_proj_sig'] = np.array(tree['vtx_proj_sig'].array())\n", + " df['vtx_proj_x_sig'] = np.array(tree['vtx_proj_x_sig'].array())\n", + " df['vtx_proj_y_sig'] = np.array(tree['vtx_proj_y_sig'].array())\n", + " df['ele_L1_iso_significance'] = np.array(tree['ele_L1_iso_significance'].array())\n", + " df['pos_L1_iso_significance'] = np.array(tree['pos_L1_iso_significance'].array())\n", + "\n", + " return df\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87300203", + "metadata": {}, + "outputs": [], + "source": [ + "#df_simp150pres = make_df('simp'+str(mass)+'pres.root')\n", + "#df_bigpreselectblind = make_df('bigpreselectblind.root')\n", + "df_wab = make_df('wab_HPS_Run2021Pass1_v9_14272_hitSmearKill_2000files.root')\n", + "df_trident = make_df('tritrig_HPS_Run2021Pass1_v9_14272_hitSmearKill_1000files.root')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e38af337", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rows containing NaN: 1\n", + "Rows containing NaN: 9\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "## count rows containing NaN\n", + "#nan_mask = df_simp150pres.isna().any(axis=1)\n", + "#n_bad = nan_mask.sum()\n", + "\n", + "#print(\"Rows containing NaN:\", n_bad)\n", + "\n", + "## drop them before writing pickle\n", + "#df_simp150pres = df_simp150pres[~nan_mask]\n", + "\n", + "## count rows containing NaN\n", + "#nan_mask = df_bigpreselectblind.isna().any(axis=1)\n", + "#n_bad = nan_mask.sum()\n", + "\n", + "#print(\"Rows containing NaN:\", n_bad)\n", + "\n", + "## drop them before writing pickle\n", + "#df_bigpreselectblind = df_bigpreselectblind[~nan_mask]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32bd7123", + "metadata": {}, + "outputs": [], + "source": [ + "#df_simp150pres.to_pickle('simp'+str(mass)+'pres.pk')\n", + "#df_bigpreselectblind.to_pickle('bigpreselectblind.pk')\n", + "df_wab.to_pickle('wab_HPS_Run2021Pass1_v9_14272_hitSmearKill_2000files.pk')\n", + "df_trident.to_pickle('tritrig_HPS_Run2021Pass1_v9_14272_hitSmearKill_1000files.pk')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tools/simp-search-tools/ann-training/mytools.py b/tools/simp-search-tools/ann-training/mytools.py new file mode 100755 index 00000000..46bfa91b --- /dev/null +++ b/tools/simp-search-tools/ann-training/mytools.py @@ -0,0 +1,547 @@ +import torch +from torch import nn, optim +import torch.nn.functional as F +from torch.utils.data import TensorDataset, DataLoader +import numpy as np +import pandas as pd + +# Mass of Kaon in GeV +m_K = 0.493677 + +# compute adversary labels from dataframe +def get_InvM(df): + + # Create temporary dataframe and compute invariant mass + df_temp = pd.DataFrame() + df_temp['e_Ptot'] = np.sqrt(df['ele_Px']**2 + df['ele_Py']**2 + df['ele_Pz']**2) + df_temp['p_Ptot'] = np.sqrt(df['pos_Px']**2 + df['pos_Py']**2 + df['pos_Pz']**2) + df_temp['e_SVT_E_K'] = m_K / np.sqrt ( 1 - ( df_temp.e_Ptot**2 / (df_temp.e_Ptot**2 + m_K**2) ) ) + df_temp['p_SVT_E_K'] = m_K / np.sqrt ( 1 - ( df_temp.p_Ptot**2 / (df_temp.p_Ptot**2 + m_K**2) ) ) + df_temp['M'] = np.sqrt((df_temp.e_SVT_E_K+df_temp.p_SVT_E_K)**2 - (df.ele_Px+df.pos_Px)**2 - (df.ele_Py+df.pos_Py)**2 - (df.ele_Pz+df.pos_Pz)**2) + + # Return a one-hot encoding of the quantile bins + return df_temp.M.to_numpy() + +def get_one_hot_edges(vals, n_bins=5): + vals = np.asarray(vals) + + # compute percentiles from 0..100 in (n_bins+1) steps -> n_bins intervals + quantiles = np.linspace(0, 100, n_bins + 1) + edges = np.percentile(vals, quantiles) # length = n_bins + 1 + + return edges + +def get_adv_labels(vals, edges): + vals = np.asarray(vals) + + bin_idx = np.searchsorted(edges, vals, side='right') - 1 + bin_idx = np.clip(bin_idx, 0, len(edges) - 2).astype(int) + + one_hot = np.eye(len(edges)-1)[bin_idx] # shape (N, n_bins) + return one_hot.astype(np.float32) + +# Training Loop +def train_clas(dataloader, model, loss_fn, optimizer, scheduler_clas, device, print_results = True): + size = len(dataloader.dataset) + num_batches = len(dataloader) + model.train() + train_loss = 0 + + for batch, (X, y) in enumerate(dataloader): + + X, y = X.to(device), y.to(device) + + # Compute prediction error + pred = model(X) + loss = loss_fn(pred, y) + + # Backpropagation + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler_clas.step() + + train_loss += loss.item() + + # if batch % 100 == 0: + # loss, current = loss.item(), batch * len(X) + # print(f"Current batch training loss: {loss:>7f} [{current:>5d}/{size:>5d}]") + + train_loss /= num_batches + if print_results: + print(f"Training loss: {train_loss:>7f}") + return(train_loss) + + +# Validation Loop +def validate_clas(dataloader, model, loss_fn, device, print_results = True): + num_batches = len(dataloader) + model.eval() + val_loss = 0 + with torch.no_grad(): + for X, y in dataloader: + + X, y = X.to(device), y.to(device) + pred = model(X) + val_loss += loss_fn(pred, y).item() + + val_loss /= num_batches + if print_results: + print(f"Validation loss: {val_loss:>7f} \n") + return(val_loss) + + +# Testing Loop +def test_clas(dataloader, model, device): + test_labels = np.array([]) + model.eval() + with torch.no_grad(): + for X in dataloader: + + X = X[0] + X = X.to(device) + pred = model(X) + test_labels = np.append( test_labels , pred.squeeze(1).cpu().numpy() ) + + return(test_labels) + + +# Training Loop +# Training Loop +def train_adv(dataloader, classifier, adv, loss_fn, optimizer, scheduler_adv, device, print_results=True): + size = len(dataloader.dataset) + num_batches = len(dataloader) + classifier.eval() + adv.train() + train_loss = 0 + + for batch, (X, y, w) in enumerate(dataloader): + + X, y, w = X.to(device), y.to(device), w.to(device) + + #Ensure not all weights are zero + if w.sum().item() == 0: continue + + # Forward pass through classifier + with torch.no_grad(): + p = classifier(X) + + # Compute prediction error + pred = adv(p) + sample_losses = loss_fn(pred, y) # since we set reduction='none', this is now a vector of losses, one per sample in the batch + + # Weighted mean loss over non-zero weights + nonzero = w > 0 + loss = (sample_losses[nonzero] * w[nonzero]).sum() / w[nonzero].sum() + + # Backpropagation + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler_adv.step() + + train_loss += loss.item() + + # if batch % 100 == 0: + # loss, current = loss.item(), batch * len(X) + # print(f"Current batch training loss: {loss:>7f} [{current:>5d}/{size:>5d}]") + + train_loss /= num_batches + if print_results: + print(f"Training loss: {train_loss:>7f}") + return(train_loss) + + +# Training Loop +def validate_adv(dataloader, classifier, adv, loss_fn, device, print_results=True): + num_batches = len(dataloader) + classifier.eval() + adv.eval() + val_loss = 0 + + with torch.no_grad(): + for batch, (X, y, w) in enumerate(dataloader): + + X, y, w = X.to(device), y.to(device), w.to(device) + + #Ensure not all weights are zero + if w.sum().item() == 0: continue + + p = classifier(X) + + pred = adv(p) + sample_losses = loss_fn(pred, y) # since we set reduction='none', this is now a vector of losses, one per sample in the batch + + # Weighted mean loss over non-zero weights + nonzero = w > 0 + loss = (sample_losses[nonzero] * w[nonzero]).sum() / w[nonzero].sum() + + val_loss += loss.item() + + val_loss /= num_batches + if print_results: + print(f"Validation loss: {val_loss:>7f} \n") + return(val_loss) + +# Testing Loop +def test_adv(dataloader, clas, adv, n_classes, device): + adv_pred = np.empty(n_classes) + clas.eval() + adv.eval() + with torch.no_grad(): + for X, y in dataloader: + X, y = X.to(device), y.to(device) + pred = adv(clas(X)) + adv_pred = np.vstack([adv_pred,pred.cpu().numpy()]) + + return(adv_pred[1:]) + + +def set_requires_grad(model, requires_grad): + for p in model.parameters(): + p.requires_grad = requires_grad + + +def train_full(dataloader, classifier, adv, loss_fn_clas, lambda_, loss_fn_adv, optimizer_clas, optimizer_adv, scheduler_clas, scheduler_adv, device, print_results=True): + size = len(dataloader.dataset) + num_batches = len(dataloader) + train_loss_clas = 0 + train_loss_adv = 0 + classifier.train() + adv.train() + + for batch, (X, y_clas, y_adv, w) in enumerate(dataloader): + + X, y_clas, y_adv, w = X.to(device), y_clas.to(device), y_adv.to(device), w.to(device) + + #Ensure not all weights are zero + if w.sum().item() == 0: continue + + # ------------------------- + # 1) Classifier update (adv parameters frozen) + # ------------------------- + set_requires_grad(adv, False) # freeze adv params + set_requires_grad(classifier, True) # ensure classifier grads enabled + + # forward classifier + class_out = classifier(X) + + # forward adversary using classifier outputs + # do NOT detach adv input here because we want gradients to flow into classifier + adv_out_for_clas = adv(class_out) + + # compute classifier loss + loss_clas = loss_fn_clas(class_out, adv_out_for_clas, y_clas, y_adv, w, lambda_) + + # backprop and update classifier + optimizer_clas.zero_grad() + loss_clas.backward() + optimizer_clas.step() + scheduler_clas.step() + + train_loss_clas += loss_clas.item() + + # ------------------------- + # 2) Adversary update (classifier parameters frozen) + # ------------------------- + + set_requires_grad(classifier, False) # freeze classifier params + set_requires_grad(adv, True) # ensure adv grads enabled + + # forward classifier and detach outputs so gradients do NOT flow into classifier + # another forward pass through classifier is needed here to catch updated classifier weights + class_out_detached = classifier(X).detach() + + # forward adv using detached classifier outputs + adv_out = adv(class_out_detached) + + # compute adversary loss + sample_losses_adv = loss_fn_adv(adv_out, y_adv) # since we set reduction='none', this is now a vector of losses, one per sample in the batch + + # Weighted mean loss over non-zero weights + nonzero = w > 0 + loss_adv = (sample_losses_adv[nonzero] * w[nonzero]).sum() / w[nonzero].sum() + + optimizer_adv.zero_grad() + loss_adv.backward() + optimizer_adv.step() + scheduler_adv.step() + + train_loss_adv += loss_adv.item() + + + train_loss_clas /= num_batches + train_loss_adv /= num_batches + + if print_results: + print(f"Classifier training loss: {train_loss_clas:>7f}") + print(f"Adversary training loss: {train_loss_adv:>7f}") + + return(train_loss_clas, train_loss_adv) + + +def train_full_orig(dataloader, classifier, adv, loss_fn_clas, lambda_, loss_fn_adv, optimizer_clas, optimizer_adv, scheduler_clas, scheduler_adv, adv_steps, device, grad_clip = False, print_results=True): + num_batches = len(dataloader) + train_loss_clas = 0 + train_loss_adv = 0 + classifier.train() + adv.train() + + for batch, (X, y_clas, y_adv, w) in enumerate(dataloader): + + X, y_clas, y_adv, w = X.to(device), y_clas.to(device), y_adv.to(device), w.to(device) + + #Ensure not all weights are zero + if w.sum().item() == 0: continue + + # ------------------------- + # 1) Classifier update (adv parameters frozen) + # ------------------------- + set_requires_grad(adv, False) # freeze adv params + set_requires_grad(classifier, True) # ensure classifier grads enabled + + # forward classifier + class_out = classifier(X) + + # forward adversary using classifier outputs + # do NOT detach adv input here because we want gradients to flow into classifier + adv_out_for_clas = adv(class_out) + + # compute classifier loss + loss_clas = loss_fn_clas(class_out, adv_out_for_clas, y_clas, y_adv, w, lambda_) + + # backprop and update classifier + optimizer_clas.zero_grad() + loss_clas.backward() + if grad_clip: + torch.nn.utils.clip_grad_norm_(classifier.parameters(), max_norm=0.1) + optimizer_clas.step() + scheduler_clas.step() + + train_loss_clas += loss_clas.item() + + # ------------------------- + # 2) Adversary update (classifier parameters frozen) + # ------------------------- + + set_requires_grad(classifier, False) # freeze classifier params + set_requires_grad(adv, True) # ensure adv grads enabled + + # This follows the original prescription where the adv training loop is nested inside the classifier training loop + # adv_steps specifies how many batches to use for adv training per classifier batch + for batch, (X_i, y_clas_i, y_adv_i, w_i) in enumerate(dataloader): + + # forward classifier and detach outputs so gradients do NOT flow into classifier + class_out_detached = classifier(X_i).detach() + + # forward adv using detached classifier outputs + adv_out = adv(class_out_detached) + + # compute adversary loss + sample_losses_adv = loss_fn_adv(adv_out, y_adv_i) # since we set reduction='none', this is now a vector of losses, one per sample in the batch + + # Weighted mean loss over non-zero weights + nonzero = w_i > 0 + loss_adv = (sample_losses_adv[nonzero] * w_i[nonzero]).sum() / w_i[nonzero].sum() + + optimizer_adv.zero_grad() + loss_adv.backward() + optimizer_adv.step() + + train_loss_adv += loss_adv.item() + + if batch == adv_steps-1: + break + + scheduler_adv.step() + + train_loss_clas /= num_batches + train_loss_adv /= (num_batches*adv_steps) + + if print_results: + print(f"Classifier training loss: {train_loss_clas:>7f}") + print(f"Adversary training loss: {train_loss_adv:>7f}") + + return(train_loss_clas, train_loss_adv) + +def validate_full(dataloader, classifier, adv, loss_fn_clas, lambda_, loss_fn_adv, device, print_results = True): + size = len(dataloader.dataset) + num_batches = len(dataloader) + val_loss_clas = 0 + val_loss_adv = 0 + classifier.eval() + adv.eval() + + with torch.no_grad(): + + for batch, (X, y_clas, y_adv, w) in enumerate(dataloader): + + X, y_clas, y_adv, w = X.to(device), y_clas.to(device), y_adv.to(device), w.to(device) + + #Ensure not all weights are zero + if w.sum().item() == 0: continue + + # forward classifier + class_out = classifier(X) + + # forward adversary + adv_out = adv(class_out) + + # compute classifier loss + loss_clas = loss_fn_clas(class_out, adv_out, y_clas, y_adv, w, lambda_) + val_loss_clas += loss_clas.item() + + # compute adversary loss + sample_losses_adv = loss_fn_adv(adv_out, y_adv) # since we set reduction='none', this is now a vector of losses, one per sample in the batch + + # Weighted mean loss over non-zero weights + nonzero = w > 0 + loss_adv = (sample_losses_adv[nonzero] * w[nonzero]).sum() / w[nonzero].sum() + + val_loss_adv += loss_adv.item() + + + val_loss_clas /= num_batches + val_loss_adv /= num_batches + + if print_results: + print(f"Classifier validation loss: {val_loss_clas:>7f}") + print(f"Adversary validation loss: {val_loss_adv:>7f}") + + return(val_loss_clas, val_loss_adv) + + +# Testing Loop +def test_final(dataloader, model, device): + test_labels = np.array([]) + model.eval() + with torch.no_grad(): + for X in dataloader: + + X = X[0].to(device) + pred = model(X) + test_labels = np.append( test_labels , pred.squeeze(1).cpu().numpy() ) + + return(test_labels) + + +def get_diff_score(x1,x2): + + N, edges = np.histogram(x1, bins=np.arange(0,300,1), density=False) + M, edges = np.histogram(x2, bins=np.arange(0,300,1), density=False) + + N2 = N[(N > 0) & (M > 0)] + M2 = M[(N > 0) & (M > 0)] + + diff_score = np.mean( np.abs( N2/np.sum(N) - M2/np.sum(M) ) / np.sqrt( ( np.sqrt(N2)/np.sum(N) )**2 + ( np.sqrt(M2)/np.sum(M) )**2 ) ) + + return diff_score + + +# simple MLP for binary classification (single logit output) +class Classifier(nn.Module): + def __init__(self, in_features, hidden1=264, hidden2=264, hidden3=128, hidden4=128, hidden5=64): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, hidden1,bias=False), + nn.BatchNorm1d(hidden1), + nn.ReLU(), + nn.Linear(hidden1, hidden2,bias=False), + nn.BatchNorm1d(hidden2), + nn.ReLU(), + nn.Linear(hidden2, hidden3,bias=False), + nn.BatchNorm1d(hidden3), + nn.ReLU(), + nn.Linear(hidden3, hidden4,bias=False), + nn.BatchNorm1d(hidden4), + nn.ReLU(), + nn.Linear(hidden4, hidden5,bias=False), + nn.BatchNorm1d(hidden5), + nn.ReLU(), + nn.Linear(hidden5, 1), + ) + def forward(self, x): + return self.net(x) + + +class Adversary(nn.Module): + def __init__(self, n_classes, input_dim=1, hidden1=64, hidden2=128, hidden3=64): + super().__init__() + self.net = nn.Sequential( + nn.Linear(input_dim, hidden1,bias=False), + nn.BatchNorm1d(hidden1), + nn.ReLU(), + nn.Linear(hidden1, hidden2,bias=False), + nn.BatchNorm1d(hidden2), + nn.ReLU(), + nn.Linear(hidden2, hidden3,bias=False), + nn.BatchNorm1d(hidden3), + nn.ReLU(), + nn.Linear(hidden3, n_classes) # outputs raw logits for CrossEntropyLoss + ) + + def forward(self, x): + # make the shape (N,1) in a way that is robust for squeezed / unsqueezed classifier outputs + x = x.view(x.size(0), -1) + return self.net(x) # returns shape (N, n_classes) + +class Adversary_small(nn.Module): + def __init__(self, n_classes, input_dim=1, hidden1=128): + super().__init__() + self.net = nn.Sequential( + nn.Linear(input_dim, hidden1), + nn.ReLU(), + nn.Linear(hidden1, n_classes) # outputs raw logits for CrossEntropyLoss + ) + + def forward(self, x): + # make the shape (N,1) in a way that is robust for squeezed / unsqueezed classifier outputs + x = x.view(x.size(0), -1) + return self.net(x) # returns shape (N, n_classes) + +# simple MLP for binary classification (single logit output) +class Classifier_2021(nn.Module): + def __init__(self, in_features, hidden1=264, hidden2=264, hidden3=128, hidden4=128, hidden5=128, hidden6=64): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, hidden1,bias=False), + nn.BatchNorm1d(hidden1), + nn.ReLU(), + nn.Linear(hidden1, hidden2,bias=False), + nn.BatchNorm1d(hidden2), + nn.ReLU(), + nn.Linear(hidden2, hidden3,bias=False), + nn.BatchNorm1d(hidden3), + nn.ReLU(), + nn.Linear(hidden3, hidden4,bias=False), + nn.BatchNorm1d(hidden4), + nn.ReLU(), + nn.Linear(hidden4, hidden5,bias=False), + nn.BatchNorm1d(hidden5), + nn.ReLU(), + nn.Linear(hidden5, hidden6,bias=False), + nn.BatchNorm1d(hidden6), + nn.ReLU(), + nn.Linear(hidden6, 1), + ) + def forward(self, x): + return self.net(x) + + + +QualCuts = { + 'pos_E_Ecal_low': 0.5, + 'pos_Pz_low': 0.65, + 'pos_Px_low': -0.06, + 'pos_Py_low': -0.12, + 'pos_Py_high': 0.13, + 'ele_Px_low': -0.12, + 'ele_Py_low': -0.14, + 'ele_Py_high': 0.16, + 'pos_Ecal_x_low': 100.0, + 'pos_Ecal_y_low': -85.0, + 'pos_Ecal_y_high': 90.0, + 'pos_Ecal_z_low': 1448.6, + 'ele_Ecal_x_high': 10.0, + 'ele_Ecal_z_low': 1448.6 +} \ No newline at end of file diff --git a/tools/simp-search-tools/ann-training/scalershifting.py b/tools/simp-search-tools/ann-training/scalershifting.py new file mode 100755 index 00000000..b019cb97 --- /dev/null +++ b/tools/simp-search-tools/ann-training/scalershifting.py @@ -0,0 +1,11 @@ +import joblib +import numpy as np + +MASS=210 +scaler = joblib.load("scaler_2021_v9_pass5_run42_QualCuts_"+str(MASS)+"_v3.pkl") + +np.savez( + "scaler_arrays_"+str(MASS)+"_v3.npz", + mean=scaler.mean_.astype(np.float32), + scale=scaler.scale_.astype(np.float32) +) \ No newline at end of file diff --git a/tools/simp-search-tools/ann-training/temp.ipynb b/tools/simp-search-tools/ann-training/temp.ipynb new file mode 100755 index 00000000..9904818d --- /dev/null +++ b/tools/simp-search-tools/ann-training/temp.ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "ef738aaa", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:29.215286Z", + "iopub.status.busy": "2026-04-01T07:57:29.214888Z", + "iopub.status.idle": "2026-04-01T07:57:30.201375Z", + "shell.execute_reply": "2026-04-01T07:57:30.200384Z" + }, + "papermill": { + "duration": 0.990473, + "end_time": "2026-04-01T07:57:30.202215+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:29.211742+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pylab as plt\n", + "#import ROOT\n", + "import uproot\n", + "\n", + "mass = 45\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ffe21612", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:30.207379Z", + "iopub.status.busy": "2026-04-01T07:57:30.206922Z", + "iopub.status.idle": "2026-04-01T07:57:30.210083Z", + "shell.execute_reply": "2026-04-01T07:57:30.209286Z" + }, + "papermill": { + "duration": 0.006605, + "end_time": "2026-04-01T07:57:30.210858+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:30.204253+00:00", + "status": "completed" + }, + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "mass = 45" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "eb81de59", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:30.214577Z", + "iopub.status.busy": "2026-04-01T07:57:30.214299Z", + "iopub.status.idle": "2026-04-01T07:57:30.217115Z", + "shell.execute_reply": "2026-04-01T07:57:30.216312Z" + }, + "papermill": { + "duration": 0.005501, + "end_time": "2026-04-01T07:57:30.217825+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:30.212324+00:00", + "status": "completed" + }, + "tags": [ + "injected-parameters" + ] + }, + "outputs": [], + "source": [ + "# Parameters\n", + "mass = 210\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b2496581", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:30.222061Z", + "iopub.status.busy": "2026-04-01T07:57:30.221779Z", + "iopub.status.idle": "2026-04-01T07:57:30.229708Z", + "shell.execute_reply": "2026-04-01T07:57:30.228859Z" + }, + "papermill": { + "duration": 0.010917, + "end_time": "2026-04-01T07:57:30.230340+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:30.219423+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def make_df(file_path):\n", + " df = pd.DataFrame()\n", + "\n", + " tritrig_file = uproot.open(file_path)\n", + "\n", + " # Open ROOT tree\n", + " tree = tritrig_file['preselection']\n", + "\n", + " # Access branches\n", + " B_vertex = tree['vertex.']\n", + " B_ele = tree['ele.']\n", + " B_pos = tree['pos.']\n", + "\n", + " arr = B_vertex['vertex.pos_'].array()\n", + " df['vertex_pos_x'] = np.array(arr['fX'])\n", + " df['vertex_pos_y'] = np.array(arr['fY'])\n", + " df['vertex_pos_z'] = np.array(arr['fZ'])\n", + "\n", + " df['InvM'] = np.array(B_vertex['vertex.invM_'].array())\n", + " df['psum'] = np.array(tree['psum'].array())\n", + "\n", + " df['ele_track_n_hits'] = np.array(B_ele['ele.track_.n_hits_'].array())\n", + " df['ele_track_d0'] = np.array(B_ele['ele.track_.d0_'].array())\n", + " df['ele_track_phi0'] = np.array(B_ele['ele.track_.phi0_'].array())\n", + " df['ele_track_z0'] = np.array(B_ele['ele.track_.z0_'].array())\n", + " df['ele_track_tan_lambda'] = np.array(B_ele['ele.track_.tan_lambda_'].array())\n", + " df['ele_track_px'] = np.array(B_ele['ele.track_.px_'].array())\n", + " df['ele_track_py'] = np.array(B_ele['ele.track_.py_'].array())\n", + " df['ele_track_pz'] = np.array(B_ele['ele.track_.pz_'].array())\n", + " df['ele_track_chi2'] = np.array(B_ele['ele.track_.chi2_'].array())\n", + " df['ele_track_x_at_ecal'] = np.array(B_ele['ele.track_.x_at_ecal_'].array())\n", + " df['ele_track_y_at_ecal'] = np.array(B_ele['ele.track_.y_at_ecal_'].array())\n", + " df['ele_track_z_at_ecal'] = np.array(B_ele['ele.track_.z_at_ecal_'].array())\n", + "\n", + " df['pos_track_n_hits'] = np.array(B_pos['pos.track_.n_hits_'].array())\n", + " df['pos_track_d0'] = np.array(B_pos['pos.track_.d0_'].array())\n", + " df['pos_track_phi0'] = np.array(B_pos['pos.track_.phi0_'].array())\n", + " df['pos_track_z0'] = np.array(B_pos['pos.track_.z0_'].array())\n", + " df['pos_track_tan_lambda'] = np.array(B_pos['pos.track_.tan_lambda_'].array())\n", + " df['pos_track_px'] = np.array(B_pos['pos.track_.px_'].array())\n", + " df['pos_track_py'] = np.array(B_pos['pos.track_.py_'].array())\n", + " df['pos_track_pz'] = np.array(B_pos['pos.track_.pz_'].array())\n", + " df['pos_track_chi2'] = np.array(B_pos['pos.track_.chi2_'].array())\n", + " df['pos_track_x_at_ecal'] = np.array(B_pos['pos.track_.x_at_ecal_'].array())\n", + " df['pos_track_y_at_ecal'] = np.array(B_pos['pos.track_.y_at_ecal_'].array())\n", + " df['pos_track_z_at_ecal'] = np.array(B_pos['pos.track_.z_at_ecal_'].array())\n", + "\n", + " df['vertex_chi2'] = np.array(B_vertex['vertex.chi2_'].array())\n", + " df['vtx_proj_sig'] = np.array(tree['vtx_proj_sig'].array())\n", + " df['vtx_proj_x_sig'] = np.array(tree['vtx_proj_x_sig'].array())\n", + " df['vtx_proj_y_sig'] = np.array(tree['vtx_proj_y_sig'].array())\n", + " df['ele_L1_iso_significance'] = np.array(tree['ele_L1_iso_significance'].array())\n", + " df['pos_L1_iso_significance'] = np.array(tree['pos_L1_iso_significance'].array())\n", + "\n", + " return df\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "87300203", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:30.234815Z", + "iopub.status.busy": "2026-04-01T07:57:30.234521Z", + "iopub.status.idle": "2026-04-01T07:57:38.523884Z", + "shell.execute_reply": "2026-04-01T07:57:38.522869Z" + }, + "papermill": { + "duration": 8.292663, + "end_time": "2026-04-01T07:57:38.524579+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:30.231916+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "df_simp150pres = make_df('simp'+str(mass)+'pres.root')\n", + "df_bigpreselectblind = make_df('bigpreselectblind.root')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e38af337", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:38.529558Z", + "iopub.status.busy": "2026-04-01T07:57:38.529316Z", + "iopub.status.idle": "2026-04-01T07:57:39.615276Z", + "shell.execute_reply": "2026-04-01T07:57:39.614331Z" + }, + "papermill": { + "duration": 1.089323, + "end_time": "2026-04-01T07:57:39.615958+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:38.526635+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rows containing NaN: 0\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rows containing NaN: 9\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "# count rows containing NaN\n", + "nan_mask = df_simp150pres.isna().any(axis=1)\n", + "n_bad = nan_mask.sum()\n", + "\n", + "print(\"Rows containing NaN:\", n_bad)\n", + "\n", + "# drop them before writing pickle\n", + "df_simp150pres = df_simp150pres[~nan_mask]\n", + "\n", + "# count rows containing NaN\n", + "nan_mask = df_bigpreselectblind.isna().any(axis=1)\n", + "n_bad = nan_mask.sum()\n", + "\n", + "print(\"Rows containing NaN:\", n_bad)\n", + "\n", + "# drop them before writing pickle\n", + "df_bigpreselectblind = df_bigpreselectblind[~nan_mask]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "32bd7123", + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-01T07:57:39.620964Z", + "iopub.status.busy": "2026-04-01T07:57:39.620684Z", + "iopub.status.idle": "2026-04-01T07:57:44.680763Z", + "shell.execute_reply": "2026-04-01T07:57:44.679754Z" + }, + "papermill": { + "duration": 5.063671, + "end_time": "2026-04-01T07:57:44.681658+00:00", + "exception": false, + "start_time": "2026-04-01T07:57:39.617987+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "df_simp150pres.to_pickle('simp'+str(mass)+'pres.pk')\n", + "df_bigpreselectblind.to_pickle('bigpreselectblind.pk')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + }, + "papermill": { + "default_parameters": {}, + "duration": 17.941503, + "end_time": "2026-04-01T07:57:45.343634+00:00", + "environment_variables": {}, + "exception": null, + "input_path": "data_process_branches_only.ipynb", + "output_path": "temp.ipynb", + "parameters": { + "mass": 210 + }, + "start_time": "2026-04-01T07:57:27.402131+00:00", + "version": "2.7.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/tools/simp-search-tools/bdt-training/train_bdt_classifier.py b/tools/simp-search-tools/bdt-training/train_bdt_classifier.py new file mode 100644 index 00000000..143d50db --- /dev/null +++ b/tools/simp-search-tools/bdt-training/train_bdt_classifier.py @@ -0,0 +1,200 @@ +import uproot +import awkward as ak +import numpy as np +from sklearn.model_selection import train_test_split +from sklearn.ensemble import GradientBoostingClassifier +from sklearn.metrics import roc_auc_score, accuracy_score +import joblib + +# --- 1. File paths and branch setup --- +# Specify the input ROOT files for background and signal. +# (Fill these paths in with the actual file locations before running.) +#background_file = "/sdf/group/hps/users/rodwyer1/run/reach_curves/datafiles/pres/bigpreselect.root" +#signal_file = "/sdf/data/hps/users/rodwyer1/SIMPS/allmassesD/signalMeVpres.root" +background_file = "/sdf/group/hps/users/rodwyer1/run/reach_curves/datafiles/pres2/bigpreselectblind.root" +signal_file = "/sdf/group/hps/users/rodwyer1/run/BigSIMPCollection2021/PRESELECTION/simpSIGpres.root" + +tree_name = "preselection" # Tree name to load from the ROOT files + +# Define the branches to read, matching those used in write_final_yields_v2.py. +# These include all features needed for the BDT. +branches = [ + "psum", "vertex.pos_", # invariant mass, p(sum), and vertex position (with fX,fY,fZ) + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele_L1_iso_significance", "pos_L1_iso_significance" + # Note: We omit "ele.track_.hit_layers_" and "pos.track_.hit_layers_" as features, + # because they are array-like (lists of hit layers) rather than simple numeric variables. + # These were included in the yield script for selection logic, not for BDT training. +] +#"vertex.invM_", +#"vertex.invMerr_", + +# Prepare containers for data and labels +all_data = [] # will hold feature arrays for each file +all_labels = [] # will hold class labels for each file (0 for background, 1 for signal) +flattened_names = [] # will capture the final feature names (for importance ranking) + +# Define file list with labels: 0 = background, 1 = signal +files_and_labels = [ + (background_file, 0), + (signal_file, 1) +] + +# --- 2. Data loading and flattening --- +for filepath, label in files_and_labels: + # Open the ROOT file and get the specified TTree + with uproot.open(f"{filepath}:{tree_name}") as tree: + # Read all desired branches at once using uproot + arrays = tree.arrays(branches) + arrays2 = tree.arrays(["vertex.invM_"]) + + # Convert to numpy and apply any preselection masks similar to the BDT script: + invM = ak.to_numpy(arrays2["vertex.invM_"]) # Invariant mass of the vertex (GeV) + # vertex.pos_ is a struct (e.g., with fields fX, fY, fZ). Convert to numpy structured array: + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + z_coords = vertex_pos["fZ"] # Extract the z-coordinate (displacement along beam) + psum_values = ak.to_numpy(arrays["psum"]) # Sum of track momenta (GeV) + + # Apply the same loose preselection cuts as in the BDT training: + # - Invariant mass window: invM < 0.18 GeV (and > -100 as a trivial lower bound) + # - Vertex displacement: z > 10.0 mm (selecting displaced vertices far from the target) + cut_mask = (invM > -100.0) & (invM < 0.18) & (z_coords > 10.0) + # If no events pass this preselection, skip this file to avoid empty arrays + if np.count_nonzero(cut_mask) == 0: + continue + + # Flatten the data: build a 2D array of shape (num_events, num_features) + # We'll iterate over each branch and handle structured or multi-dimensional data. + data_parts = [] # list to accumulate column arrays + feature_names = [] # names for each flattened feature + for br in branches: + # Convert branch to a NumPy array and apply the mask + arr = ak.to_numpy(arrays[br])[cut_mask] + if arr.dtype.fields is not None: + # This branch is structured (e.g., vertex.pos_ has fields fX,fY,fZ) + for field in arr.dtype.names: # iterate over subfields + subarr = arr[field] + # Only include numeric subfields + if not np.issubdtype(subarr.dtype, np.number): + continue # skip non-numeric types + data_parts.append(subarr.reshape(-1, 1)) + feature_names.append(f"{br}.{field}") # e.g., "vertex.pos_.fZ" + elif np.issubdtype(arr.dtype, np.number): + # This branch is a simple numeric array (or possibly a fixed-size vector) + if arr.ndim == 1: + # 1D array: directly use as one feature + data_parts.append(arr.reshape(-1, 1)) + feature_names.append(br) + elif arr.ndim == 2: + # 2D array: flatten each column as a separate feature (e.g., if any branch had a fixed-length array per event) + for i in range(arr.shape[1]): + data_parts.append(arr[:, i].reshape(-1, 1)) + feature_names.append(f"{br}[{i}]") + else: + # Higher-dimensional data not expected; skip if encountered + continue + else: + # Non-numeric and non-structured data (e.g., object dtype) are skipped + continue + + # Concatenate all feature columns horizontally to form the feature matrix for this file + if data_parts: + file_data = np.hstack(data_parts) # shape: (N_events_file, N_features_total) + else: + file_data = np.empty((np.count_nonzero(cut_mask), 0)) + file_labels = np.full(file_data.shape[0], label, dtype=int) # assign the class label (0 or 1) to all events from this file + + # Append to the global lists + all_data.append(file_data) + all_labels.append(file_labels) + # Capture feature names from the first non-empty dataset (they should be the same for both signal and background) + if not flattened_names: + flattened_names = feature_names + +# Combine data from both files into one dataset +X = np.vstack(all_data) # Feature matrix (num_total_events x num_features) +y = np.concatenate(all_labels) # Labels array (num_total_events,) + +# --- 3. Split into training and testing sets --- +# We use stratified splitting to maintain the same signal/background ratio in train and test sets. +X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, random_state=42, stratify=y +) +# Now X_train, y_train will be used to train the model, and X_test, y_test for evaluation. + +# --- 4. Train the Gradient Boosting classifier --- +# Initialize the classifier. We choose a moderate tree depth (e.g., 4) to balance complexity and generalization. +clf = GradientBoostingClassifier(max_depth=4, n_estimators=100, learning_rate=0.1, random_state=42) +# - max_depth=4 allows each decision tree in the ensemble to have up to 4 levels (default is 3; 4 can capture more complex patterns). +# - n_estimators=100 and learning_rate=0.1 are default settings (100 trees). You can adjust these for tuning. +# - random_state=42 ensures reproducibility of the training process. + +# Train the model on the training dataset +clf.fit(X_train, y_train) + +# --- 5. Evaluate model performance on the test set --- +# Use the trained model to predict probabilities and classes for the test set. +y_pred_proba = clf.predict_proba(X_test)[:, 1] # Probability of class 1 (signal) for each test event +y_pred_label = clf.predict(X_test) # Predicted class labels (0 or 1) + +# Compute evaluation metrics: +roc_auc = roc_auc_score(y_test, y_pred_proba) +accuracy = accuracy_score(y_test, y_pred_label) + +# Print out the performance metrics +print(f"Test ROC AUC = {roc_auc:.3f}") +print(f"Test Accuracy = {accuracy:.3f}") + +# These metrics give an idea of how well the classifier is separating signal and background. +# ROC AUC (Area Under the ROC Curve) is threshold-independent and 1.0 would be a perfect classifier (0.5 is random guessing). +# Accuracy is the fraction of correct classifications at the default 0.5 threshold. + +# --- 6. Save the trained model to disk --- +model_filename = "bdt_model.joblib" +joblib.dump(clf, model_filename) +print(f"Saved trained model to '{model_filename}'") + +# The model is saved using joblib, which preserves the sklearn model object (including its learned decision trees). +# This file can be loaded later in write_final_yields_v2.py using joblib.load, to apply the model for scoring events. + +# --- 7. Write feature importance rankings to a file --- +importances = clf.feature_importances_ +# feature_importances_ gives the relative importance of each feature in the trained model (based on reduction in impurity). +# We will rank these importances to see which features were most useful in the classification. + +sorted_idx = np.argsort(importances)[::-1] # indices of features sorted by importance (descending) +with open("feature_importances.txt", "w") as f: + for rank, idx in enumerate(sorted_idx, start=1): + feat_name = flattened_names[idx] if idx < len(flattened_names) else f"feature_{idx}" + f.write(f"{rank}. {feat_name}: {importances[idx]:.6f}\n") +print("Wrote feature importances to 'feature_importances.txt'") + +# --- Plot: BDT score distribution for signal and background --- +import matplotlib.pyplot as plt + +# Extract scores for signal and background in the test set +signal_scores = y_pred_proba[y_test == 1] +background_scores = y_pred_proba[y_test == 0] + +# Create the plot +plt.figure(figsize=(8, 5)) +plt.hist(signal_scores, bins=50, alpha=0.5, density=True, label='Signal', histtype='stepfilled') +plt.hist(background_scores, bins=50, alpha=0.5, density=True, label='Background', histtype='stepfilled') +plt.xlabel('BDT Score') +plt.ylabel('Density') +plt.title('BDT Response for Signal vs Background') +plt.legend(loc='upper center') +plt.grid(True) +plt.tight_layout() +plt.savefig("bdt_score_distribution.png") +print("Saved BDT response plot to 'bdt_score_distribution.png'") + diff --git a/tools/simp-search-tools/bdt-training/train_bdt_classifier_mass_dependent.py b/tools/simp-search-tools/bdt-training/train_bdt_classifier_mass_dependent.py new file mode 100644 index 00000000..58c961e2 --- /dev/null +++ b/tools/simp-search-tools/bdt-training/train_bdt_classifier_mass_dependent.py @@ -0,0 +1,199 @@ +import uproot +import awkward as ak +import numpy as np +from sklearn.model_selection import train_test_split +from sklearn.ensemble import GradientBoostingClassifier +from sklearn.metrics import roc_auc_score, accuracy_score +import joblib +import argparse +import matplotlib.pyplot as plt + +parser = argparse.ArgumentParser() +parser.add_argument("RUN", type=int) +args = parser.parse_args() + +# --- 1. File paths and mass-point setup --- +background_file = "/sdf/group/hps/users/rodwyer1/run/reach_curves/datafiles/pres2/bigpreselectblind.root" + +RUN = args.RUN +runs = [30 + i * 15 for i in range(13)] + +if RUN < 0 or RUN >= len(runs): + raise ValueError("RUN must be between 0 and " + str(len(runs) - 1)) + +signal_file = "/sdf/group/hps/users/rodwyer1/run/BigSIMPCollection2021/PRESELECTION/simp" + str(runs[RUN]) + "pres.root" +tree_name = "preselection" + +# --- 2. Branch setup --- +# Keep the newer branch set, including the two newer isolation-significance features +branches = [ + "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele_L1_iso_significance", "pos_L1_iso_significance" +] + +all_data = [] +all_labels = [] +flattened_names = [] + +files_and_labels = [ + (background_file, 0), + (signal_file, 1) +] + +# --- 3. Data loading and flattening --- +for filepath, label in files_and_labels: + with uproot.open(f"{filepath}:{tree_name}") as tree: + arrays = tree.arrays(branches) + arrays2 = tree.arrays(["vertex.invM_"]) + + invM = ak.to_numpy(arrays2["vertex.invM_"]) + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + z_coords = vertex_pos["fZ"] + # Mass-dependent window copied from the older mass-dependent script + center_geV = float(runs[RUN])/1000.0 #1.8 * float(runs[RUN]) / (1000.0 * 3.0) + print(center_geV) + print("Training mass point (MeV):", runs[RUN]) + print("Mass-window center (GeV):", center_geV) + print("Mass window:", center_geV - 0.002, "to", center_geV + 0.002) + + cut_mask = ( + (invM > -100.0) + & (invM < 100.0) + #& (z_coords > 10.0) + & (invM > (center_geV - 0.002)) + & (invM < (center_geV + 0.002)) + ) + + if np.count_nonzero(cut_mask) == 0: + print("No events passed cut_mask for file:", filepath) + continue + + data_parts = [] + feature_names = [] + + for br in branches: + arr = ak.to_numpy(arrays[br])[cut_mask] + + if arr.dtype.fields is not None: + for field in arr.dtype.names: + subarr = arr[field] + if not np.issubdtype(subarr.dtype, np.number): + continue + data_parts.append(subarr.reshape(-1, 1)) + feature_names.append(f"{br}.{field}") + + elif np.issubdtype(arr.dtype, np.number): + if arr.ndim == 1: + data_parts.append(arr.reshape(-1, 1)) + feature_names.append(br) + elif arr.ndim == 2: + for i in range(arr.shape[1]): + data_parts.append(arr[:, i].reshape(-1, 1)) + feature_names.append(f"{br}[{i}]") + else: + continue + else: + continue + + if data_parts: + file_data = np.hstack(data_parts) + else: + file_data = np.empty((np.count_nonzero(cut_mask), 0)) + + file_labels = np.full(file_data.shape[0], label, dtype=int) + + all_data.append(file_data) + all_labels.append(file_labels) + + if not flattened_names: + flattened_names = feature_names + +print(len(file_data)) +if len(all_data) == 0: + raise RuntimeError("No data survived selection in either file.") + +X = np.vstack(all_data) +y = np.concatenate(all_labels) + +# --- Remove rows with NaN/inf before splitting --- +finite_mask = np.all(np.isfinite(X), axis=1) + +print("Total events before finite cleaning:", X.shape[0]) +print("Events with all-finite features:", np.count_nonzero(finite_mask)) +print("Events removed for NaN/inf:", np.count_nonzero(~finite_mask)) + +X = X[finite_mask] +y = y[finite_mask] + +if X.shape[0] == 0: + raise RuntimeError("No fully finite events remain after NaN/inf cleaning.") + +# --- 4. Train/test split --- +X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, random_state=42, stratify=y +) + +# --- 5. Train classifier --- +clf = GradientBoostingClassifier( + max_depth=4, + n_estimators=100, + learning_rate=0.1, + random_state=42 +) + +clf.fit(X_train, y_train) + +# --- 6. Evaluate --- +y_pred_proba = clf.predict_proba(X_test)[:, 1] +y_pred_label = clf.predict(X_test) + +roc_auc = roc_auc_score(y_test, y_pred_proba) +accuracy = accuracy_score(y_test, y_pred_label) + +print(f"Test ROC AUC = {roc_auc:.3f}") +print(f"Test Accuracy = {accuracy:.3f}") + +# --- 7. Save model --- +model_filename = "bdt_model_" + str(runs[RUN]) + ".joblib" +joblib.dump(clf, model_filename) +print(f"Saved trained model to '{model_filename}'") + +# --- 8. Save feature importances --- +importances = clf.feature_importances_ +sorted_idx = np.argsort(importances)[::-1] + +feature_importance_filename = "feature_importances_" + str(runs[RUN]) + ".txt" +with open(feature_importance_filename, "w") as f: + for rank, idx in enumerate(sorted_idx, start=1): + feat_name = flattened_names[idx] if idx < len(flattened_names) else f"feature_{idx}" + f.write(f"{rank}. {feat_name}: {importances[idx]:.6f}\n") + +print(f"Wrote feature importances to '{feature_importance_filename}'") + +# --- 9. Plot score distributions --- +signal_scores = y_pred_proba[y_test == 1] +background_scores = y_pred_proba[y_test == 0] + +plt.figure(figsize=(8, 5)) +plt.hist(signal_scores, bins=50, alpha=0.5, density=True, label="Signal", histtype="stepfilled") +plt.hist(background_scores, bins=50, alpha=0.5, density=True, label="Background", histtype="stepfilled") +plt.xlabel("BDT Score") +plt.ylabel("Density") +plt.title("BDT Response for Signal vs Background") +plt.legend(loc="upper center") +plt.grid(True) +plt.tight_layout() + +plot_filename = "bdt_score_distribution_" + str(runs[RUN]) + ".png" +plt.savefig(plot_filename) +print(f"Saved BDT response plot to '{plot_filename}'") From 08bd84adbaeb434e85b3c93febbef0c443be5197 Mon Sep 17 00:00:00 2001 From: rodwyer100 Date: Tue, 7 Apr 2026 15:31:52 -0700 Subject: [PATCH 3/3] Adding the rest of the miscellaneous plotters with a read me file --- tools/simp-search-tools/README.txt | 2 +- .../plot-making/misc/README.txt | 71 ++ .../misc/plot_all_featured_and_2d_debug.py | 635 +++++++++++++ .../misc/plot_all_features_and_do_tables.py | 427 +++++++++ .../misc/plot_all_features_and_do_tables_2.py | 764 ++++++++++++++++ .../misc/plot_all_features_and_do_tables_3.py | 848 +++++++++++++++++ ...plot_all_features_and_do_tables_3_MULTI.py | 751 +++++++++++++++ ...atures_and_do_tables_3_MULTI_just_z_fit.py | 805 +++++++++++++++++ .../misc/plot_all_features_and_do_tables_4.py | 852 ++++++++++++++++++ ...plot_all_features_and_do_tables_4_MULTI.py | 749 +++++++++++++++ ...atures_and_do_tables_4_MULTI_just_z_fit.py | 810 +++++++++++++++++ .../plot_all_features_and_do_tables_BDT.py | 709 +++++++++++++++ ...ot_all_features_and_do_tables_BK_w_CUTs.py | 429 +++++++++ .../plot_all_features_normed_and_2d_debug.py | 559 ++++++++++++ .../misc/plot_all_together_vals.py | 195 ++++ .../plot-making/misc/plot_ann_vs_bdt.py | 432 +++++++++ .../plot_bg_shell_feature_diffs_ann_bdt.py | 505 +++++++++++ .../plot_bg_shell_feature_diffs_ann_bdt2.py | 583 ++++++++++++ .../plot-making/misc/plot_features_normed.py | 244 +++++ .../plot-making/misc/plot_psum_compare.py | 94 ++ .../plot-making/misc/plot_psum_from_vertex.py | 94 ++ .../misc/plot_psum_from_vertex_only.py | 97 ++ .../plot-making/misc/plot_sig_vs_mass.py | 178 ++++ .../plot-making/misc/plot_sig_vs_val.py | 213 +++++ .../misc/plot_significance_surfaces.py | 205 +++++ .../plot-making/misc/plot_z0_dist.py | 122 +++ .../plot-making/misc/plot_z0_signal_v_back.py | 204 +++++ 27 files changed, 11576 insertions(+), 1 deletion(-) create mode 100644 tools/simp-search-tools/plot-making/misc/README.txt create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_featured_and_2d_debug.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_2.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI_just_z_fit.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI_just_z_fit.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BDT.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BK_w_CUTs.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_features_normed_and_2d_debug.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_all_together_vals.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_ann_vs_bdt.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt2.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_features_normed.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_psum_compare.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex_only.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_sig_vs_mass.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_sig_vs_val.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_significance_surfaces.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_z0_dist.py create mode 100644 tools/simp-search-tools/plot-making/misc/plot_z0_signal_v_back.py diff --git a/tools/simp-search-tools/README.txt b/tools/simp-search-tools/README.txt index 223e2fb2..4d4a1f81 100644 --- a/tools/simp-search-tools/README.txt +++ b/tools/simp-search-tools/README.txt @@ -16,7 +16,7 @@ bk_eff_selection.y: contains any tools deemed entirely corresponding to backgrou plot-making -This file contains many/most of the scripts required for plotting tight optimization estimated yields (after optimizing things) as well as roc curves for individual scans (ann,bdt,miny0) to compare things relatively. The scripts contained therein are: ann_score_data_mc_overlay.py make_maxZbi_grid_worker_v2.py submit_maxZbi_grid.sh write_roc_overlay_all3.py +This file contains many/most of the scripts required for plotting tight optimization estimated yields (after optimizing things) as well as roc curves for individual scans (ann,bdt,miny0) to compare things relatively. The scripts contained therein are: ann_score_data_mc_overlay.py make_maxZbi_grid_worker_v2.py submit_maxZbi_grid.sh write_roc_overlay_all3.py. This directory also includes misc, which is a ton of miscellaneous python ploting files. The README.txt file there gives a brief description of each. Some are legacy code, in that they rely on older versions of things included in this directory. Enough is supplied so that Claude should be able to infer how to fix it ;) ann_score_data_mc_overlay.py: Plots the ann response curve for data and data-like MC on top of eachother given the locations of ann npz scaling files and classifier pickle files. Is used as a final test to establish that indeed it is not learning data MC discriminating features rather than signal background discriminating features. diff --git a/tools/simp-search-tools/plot-making/misc/README.txt b/tools/simp-search-tools/plot-making/misc/README.txt new file mode 100644 index 00000000..2b944e51 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/README.txt @@ -0,0 +1,71 @@ +plot_all_featured_and_2d_debug.py +Plot unit normalized distributions for all variables; now needs signal and background normalization. + +plot_all_features_and_do_tables_2.py +Plots some accurately normalized distributions. First script to obtain accurate yields in signal and background, an iteration that worked is included in STUFF_WORKS. + +plot_all_features_and_do_tables_3_MULTI_just_z_fit.py +Does the above task but only for the z distribution, allowing the z profile to be fit and checked against the expected post-resampling behavior. +Dependencies: plot_all_features_and_do_tables_3_MULTI.py + +plot_all_features_and_do_tables_3_MULTI.py +Does the tables with the additional constraint that multiple signals are overlaid for differing pairs of masses and epsilon. + +plot_all_features_and_do_tables_3.py +Performs tight selection and operation of previous scripts, but with two val cuts, one for significance and one for min y0. +Dependencies: decayLength7sel.py + +plot_all_features_and_do_tables_4_MULTI_just_z_fit.py +Same as the 3_MULTI_just_z_fit version, but for the version 4 workflow, which uses reweighting rather than down-sampling, with no cutoffs and true_vd. +Dependencies: plot_all_features_and_do_tables_4_MULTI.py + +plot_all_features_and_do_tables_4_MULTI.py +Same as the version 3 MULTI workflow, but uses reweighting rather than down-sampling, with no cutoffs and true_vd. + +plot_all_features_and_do_tables_4.py +Same as the version 3 workflow, but uses reweighting rather than down-sampling, with no cutoffs and true_vd. + +plot_all_features_and_do_tables_BDT.py +Does the tables for the BDT to compare its performance against the selection-based cuts. +Dependencies: decayLength8sel.py, /bdt_trainer/train_bdt_classifier.py + +plot_all_features_and_do_tables_BK_w_CUTs.py +Plots the invM of the background as selection cuts are increased. + +plot_all_features_and_do_tables.py + +plot_all_together_vals.py +Plots the contour levels for a given value for all vals, useful for checking whether one cut is better than the rest. + +plot_ann_vs_bdt.py + +plot_bg_shell_feature_diffs_ann_bdt2.py + +plot_bg_shell_feature_diffs_ann_bdt.py + +plot_features_normed.py +Plots histograms for cut variables, weighted by their actual physical values after tight selection. + +plot_psum_compare.py +Compares the psum for different interpretations. + +plot_psum_from_vertex_only.py +Plots the psum from quantities calculated independently, rather than using the psum already stored in the ROOT file. + +plot_psum_from_vertex.py +Plots the psum, used to evaluate whether it had the desired shape. + +plot_significance_surfaces.py +Makes 25 contour plots for the various cuts, with 2D histograms and color contours for significance as a function of cut value. + +plot_sig_vs_mass.py +Plots the significance as a function of mass, i.e. cut val, for various values of epsilon and val that are fed in. + +plot_sig_vs_val.py +Plots the significance as a function of val, i.e. cut val, for various values of mass and epsilon that are fed in. + +plot_z0_dist.py +Plots unit normalized distributions of cut variables; they are not weighted by physical weighting. + +plot_z0_signal_v_back.py +Plots unit normalized distributions of cut variables; at this point it is min y0. diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_featured_and_2d_debug.py b/tools/simp-search-tools/plot-making/misc/plot_all_featured_and_2d_debug.py new file mode 100644 index 00000000..e808acf5 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_featured_and_2d_debug.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import numpy as np +import matplotlib.pyplot as plt +from pathlib import Path +import uproot, awkward as ak # for data reading +import math + +# Attempt to import background efficiency module +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection as bg: {e}.\n") + bg = None + +# Constants and global settings +N_B_TOTAL_RECO = 3.2e9 # total number of reconstructed background events (baseline) + +# Define Zbi significance calculation (from combine_zbi.py) +try: + from scipy.special import betainc, erfinv +except ImportError: + betainc = erfinv = None + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for given signal (S) and background (B) yields.""" + if B <= 0: + # No background events: significance is infinite if S>0, or 0 if S=0 + return float('inf') if S > 0 else 0.0 + if betainc is None or erfinv is None: + # Fallback to approximate significance if SciPy not available + return S / math.sqrt(B) if B > 0 else 0.0 + # Compute the one-sided tail probability using the beta incomplete function + p = betainc(S + B, 1.0 + B, 0.5) + # Convert tail probability to significance + z = math.sqrt(2.0) * erfinv(1.0 - 2.0 * p) + # Cap extremely large significances at 9.0 for display purposes + if p < 1e-16: + z = 9.0 + return float(z) + +def import_signal_base(module_name: str = "decayLength5sel"): + """Dynamically import the signal base module (e.g., decayLength5sel).""" + try: + base = __import__(module_name) + return base + except SystemExit: + sys.stderr.write(f"[fatal] Importing '{module_name}' triggered SystemExit (argparse at top-level?). " + f"Please guard CLI in that module with if __name__ == '__main__'.\n") + raise + except Exception: + # Fallback: try loading from a file in the current directory + import importlib.util + here = os.path.dirname(os.path.abspath(__file__)) + candidate = os.path.join(here, module_name + ".py") + if os.path.exists(candidate): + spec = importlib.util.spec_from_file_location(module_name, candidate) + mod = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(mod) + return mod + except SystemExit: + sys.stderr.write(f"[fatal] Executing '{candidate}' still triggered argparse at import.\n") + raise + else: + raise + +def _ensure_dir(directory: str): + """Create the directory if it does not exist.""" + if directory and not os.path.isdir(directory): + os.makedirs(directory, exist_ok=True) + +def load_signal_events(base_module, mass_mev: float, Val: int): + """Load signal events after tight selection, returning a dict of feature->np.array.""" + mkey = base_module._mass_key(mass_mev) # find closest available mass key + events = base_module._events_cache(mkey) # load cached events (numerator file) + mask = base_module.tight_selection(events, Val) # apply tight selection mask + out = {} + # Filter each numeric 1D branch with the selection mask + for k, v in events.items(): + if isinstance(v, np.ndarray) and v.ndim == 1 and len(v) == len(mask): + vv = v[mask] + if np.issubdtype(vv.dtype, np.number): + vv = vv[np.isfinite(vv)] # remove any non-finite values + out[k] = vv + # Ensure 'vertex.pos_.fZ' is present (use alias if needed) + if "vertex.pos_.fZ" not in out and "vertex.pos_.z" in out: + out["vertex.pos_.fZ"] = out["vertex.pos_.z"] + return out + +def _map_desired_to_available(avail_names, desired_list): + """ + Map canonical desired branch names (e.g. 'vertex.invM_') to actual branch names in file. + Strategy: + 1) exact match + 2) suffix match on '/{desired}' or '.{desired}' or end match + 3) match by last token after '/' or '.' + """ + avail = list(avail_names) + mapping = {} + def last_token(s): + part = s.split('/')[-1] + return part.split('.')[-1] + for d in desired_list: + if d in avail: + mapping[d] = d + continue + # Try suffix matches + cand = [a for a in avail if a.endswith("/" + d) or a.endswith("." + d) or a.endswith(d)] + if not cand: + dtok = last_token(d) + cand = [a for a in avail if last_token(a) == dtok] + if cand: + # choose the shortest path match (most likely direct branch name) + mapping[d] = sorted(cand, key=len)[0] + return mapping + +def _get_array_by_canonical(arrays, canonical, name_map): + """Fetch array corresponding to canonical branch name from Awkward arrays using the mapping.""" + real = name_map.get(canonical) + if real is None and canonical in getattr(arrays, "fields", []): + real = canonical + if real is None: + # Try suffix matches in available fields + for f in getattr(arrays, "fields", []): + if f.endswith("/" + canonical) or f.endswith("." + canonical) or f.endswith(canonical): + real = f + break + if real is None: + return None + try: + return np.asarray(ak.to_numpy(arrays[real])) + except Exception: + return None + +def load_background_events(mass_mev: float, Val: int, desired_keys=None, debug=False): + """ + Load background events after tight selection (within mass window) from the background ROOT file. + Returns a dict of feature->np.array for selected background events. + """ + if bg is None: + return {} + # Resolve background file path(s) + paths = [] + cand = getattr(bg, "BACKGROUND_PATH", None) + if cand is None: + return {} + # Helper to add candidate paths (file, dir, pattern, or list of such) + def add_candidate(c): + if c is None: + return + if isinstance(c, (list, tuple)): + for cc in c: + add_candidate(cc) + return + c_str = str(c) + p = Path(c_str) + if any(ch in c_str for ch in ["*", "?", "["]): + paths.extend(sorted(Path(x) for x in Path().glob(c_str))) + elif p.is_dir(): + paths.extend(sorted(p.rglob("*.root"))) + elif p.exists(): + paths.append(p) + else: + # if path not found, ignore silently (debug can print) + pass + add_candidate(cand) + paths = [str(pp) for pp in paths] + if not paths: + if debug: + sys.stderr.write(f"[debug] BACKGROUND_PATH: {cand}, resolved to 0 files.\n") + return {} + # Open the first background file (expected to contain the tree) + file_path = paths[0] + with uproot.open(file_path) as f: + # If module provides a helper to get the tree, use it; otherwise pick the first TTree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # Find first TTree key + tree_keys = [k for k in f.keys() if ";" in k] + tname = tree_keys[0] if tree_keys else None + if tname is None: + if debug: + sys.stderr.write(f"[debug] no TTree found in: {file_path}\n") + return {} + tree = f[tname] + # Determine available branch keys and map requested ones + try: + avail_keys = list(tree.keys()) + except Exception: + avail_keys = [] + base_needed = { + "vertex.invM_", "psum", "vtx_proj_sig", "vertex.pos_", + "vertex.pos_.fX", "vertex.pos_.fY", "vertex.pos_.fZ", + "vertex.pos_.X", "vertex.pos_.Y", "vertex.pos_.Z", + "vertex.pos_.x", "vertex.pos_.y", "vertex.pos_.z", + } + req_all = set(desired_keys or []) | base_needed | set(getattr(bg, "BRANCHES", [])) + name_map = _map_desired_to_available(avail_keys, sorted(req_all)) + branch_req = sorted(set(name_map.values())) + # Read the requested branches into an Awkward arrays object + arrays = tree.arrays(branch_req, library="ak") + # Build events dictionary with canonical keys + events_bg = {} + # Extract Z positions (if custom extractor exists in bg module, use it) + if hasattr(bg, "_extract_z_from_arrays"): + try: + zvals = bg._extract_z_from_arrays(arrays) + events_bg["vertex.pos_.fZ"] = np.asarray(zvals, dtype=float) + except Exception: + pass + # If Z not set, attempt robust extraction + if "vertex.pos_.fZ" not in events_bg: + z = _get_array_by_canonical(arrays, "vertex.pos_.fZ", name_map) + if z is None: + for alias in ["vertex.pos_.Z", "vertex.pos_.z"]: + z = _get_array_by_canonical(arrays, alias, name_map) + if z is not None: + break + if z is not None: + events_bg["vertex.pos_.fZ"] = z + # Transfer desired keys (features) from arrays to events_bg using mapping + for k in (desired_keys or []): + arr = _get_array_by_canonical(arrays, k, name_map) + if arr is not None and np.issubdtype(arr.dtype, np.number): + events_bg[k] = arr + # Ensure essential branches are present (psum and vtx_proj_sig) + if "psum" not in events_bg: + tmp = None + # Try common aliases for psum + for nm in ["psum", "vertex.psum", "p_sum", "pSum", "sumP"]: + if nm in getattr(arrays, "fields", []): + tmp_arr = ak.to_numpy(arrays[nm]) + if tmp_arr is not None: + tmp = np.asarray(tmp_arr) + break + if tmp is None: + tmp = _get_array_by_canonical(arrays, "psum", name_map) + if tmp is not None: + events_bg["psum"] = np.asarray(tmp) + if "vtx_proj_sig" not in events_bg: + tmp = None + for nm in ["vtx_proj_sig", "vertex.projSig", "vtxProjSig", "vtx_sigma_proj"]: + if nm in getattr(arrays, "fields", []): + tmp_arr = ak.to_numpy(arrays[nm]) + if tmp_arr is not None: + tmp = np.asarray(tmp_arr) + break + if tmp is None: + tmp = _get_array_by_canonical(arrays, "vtx_proj_sig", name_map) + if tmp is not None: + events_bg["vtx_proj_sig"] = np.asarray(tmp) + # Determine mass window mask (±10 MeV around mass_mev) + invM = None + for inv_alias in ["vertex.invM_", "invM_", "invMass", "m_inv", "mInv", "vtxInvM", "vtx.invM"]: + invM = _get_array_by_canonical(arrays, inv_alias, name_map) + if invM is not None: + break + if invM is not None and hasattr(bg, "_mass_window_mask"): + mwin_mask = bg._mass_window_mask(invM, mass_mev) + else: + mwin_mask = np.ones(len(events_bg.get(next(iter(events_bg.keys())), [])), dtype=bool) + if debug and invM is None: + sys.stderr.write("[debug] invariant mass branch not found; skipping mass window filter.\n") + # Apply tight selection mask + if hasattr(bg, "_tight_selection_mask"): + try: + tight_mask = bg._tight_selection_mask(events_bg, Val) + except Exception as e: + if debug: + sys.stderr.write(f"[debug] bg._tight_selection_mask failed: {e}\n") + tight_mask = None + else: + tight_mask = None + if tight_mask is None: + # If no selection mask available, default to requiring finite z + zvals = events_bg.get("vertex.pos_.fZ") + tight_mask = np.isfinite(zvals) if zvals is not None else np.ones_like(mwin_mask, dtype=bool) + # Combine mass window and tight selection + mask = (tight_mask.astype(bool) & mwin_mask.astype(bool)) + # Filter each numeric 1D branch by the combined mask + out = {} + for k, v in events_bg.items(): + if isinstance(v, np.ndarray) and v.ndim == 1 and len(v) == len(mask) and np.issubdtype(v.dtype, np.number): + vv = v[mask] + vv = vv[np.isfinite(vv)] + out[k] = vv + return out + +def pick_1d_numeric_features(sig_dict, bg_dict): + """Return a sorted list of feature names present in both signal and background (with at least 2 entries each).""" + common = [] + for k in sig_dict.keys(): + if k in bg_dict and sig_dict[k].size >= 2 and bg_dict[k].size >= 2: + common.append(k) + prioritise = [ + "vertex.pos_.fZ", "ele.track_.z0_", "pos.track_.z0_", + "ele.track_.d0_", "pos.track_.d0_", "vertex.invM_", "psum", "vtx_proj_sig", + "ele.track_.chi2_", "pos.track_.chi2_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", + ] + ordered = [] + seen = set() + # Add prioritized features first (if present) + for p in prioritise: + if p in common and p not in seen: + ordered.append(p) + seen.add(p) + # Add remaining common features in sorted order + for k in sorted(common): + if k not in seen: + ordered.append(k) + seen.add(k) + return ordered + +def sanitize(name: str) -> str: + """Sanitize a string to be used in file names (replace special characters with underscore).""" + bad_chars = [" ", "/", "\\", "(", ")", "[", "]", "{", "}", ":", ";", ",", "|", "<", ">", "?", "*", "'", '"', "."] + out = name + for b in bad_chars: + out = out.replace(b, "_") + # Collapse multiple underscores to single + while "__" in out: + out = out.replace("__", "_") + return out.strip("_") + +def plot_1d_overlaid(sig_arr, bg_arr, feat_name, outdir, mass_mev, epsilon, Val, bins, S_yield, B_yield, Zbi): + """Plot a 1D histogram of a given feature, overlaying signal and background, normalized to expected yields.""" + fig, ax = plt.subplots(figsize=(7.8, 4.8)) + # Determine a suitable range for the histogram (focus between 0.5% and 99.5% quantiles of combined data) + if bg_arr.size > 0: + both = np.concatenate([sig_arr, bg_arr]) + else: + both = sig_arr + q1, q99 = np.quantile(both, [0.005, 0.995]) if both.size > 0 else (0, 1) + pad = 0.05 * (q99 - q1 + 1e-12) + hist_range = (q1 - pad, q99 + pad) + # Compute per-event weights so that total area equals expected yields + sig_weights = None + bg_weights = None + if sig_arr.size > 0: + sig_weights = np.full(sig_arr.shape, S_yield / sig_arr.size) + if bg_arr.size > 0: + bg_weights = np.full(bg_arr.shape, B_yield / bg_arr.size) + # Plot signal and background histograms with weights (no density normalization) + ax.hist(sig_arr, bins=bins, range=hist_range, weights=sig_weights, histtype="step", + linewidth=1.8, label="signal") + if bg_arr.size > 0: + ax.hist(bg_arr, bins=bins, range=hist_range, weights=bg_weights, histtype="step", + linewidth=1.8, label="background") + # Annotate the plot with yields and significance + title_line1 = f"{feat_name}" + title_line2 = f"S={S_yield:.2g}, B={B_yield:.2g}, Zbi={Zbi:.2f}; mass={mass_mev:g} MeV, " + title_line2 += f"epsilon={epsilon}" if epsilon is not None else "epsilon=None" + title_line2 += f", Val={Val}" + ax.set_title(f"{title_line1}\n{title_line2}") + ax.set_xlabel(feat_name) + ax.set_ylabel("Expected events") + ax.grid(True, alpha=0.3) + ax.legend() + + ax.set_yscale("log") + ymin, ymax = ax.get_ylim() + ax.set_ylim(bottom=max(ymin, 1e-6), top=ymax) + + # Save plot to file + fname = f"oned_{sanitize(feat_name)}_{int(round(mass_mev))}MeV" + if epsilon is not None: + # Include epsilon in filename (sanitized) + eps_str = str(epsilon).replace('.', 'p').replace('-', 'm') + fname += f"_eps{eps_str}" + fname += f"_V{Val}.png" + fpath = os.path.join(outdir, fname) + fig.tight_layout() + fig.savefig(fpath, dpi=160, bbox_inches="tight") + plt.close(fig) + return fpath + +def plot_2d_single(data_x, data_y, feat_x, feat_y, label, outdir, mass_mev, epsilon, Val, bins2d, weight_per_event): + """Plot a single 2D histogram for either signal or background data for two features.""" + fig, ax = plt.subplots(figsize=(6.6, 5.8)) + # Determine ranges for x and y (1% to 99% quantiles) + def compute_range(a): + if a.size == 0: + return (0, 1) + q1, q99 = np.quantile(a, [0.01, 0.99]) + pad = 0.05 * (q99 - q1 + 1e-12) + return (q1 - pad, q99 + pad) + rx = compute_range(data_x) + ry = compute_range(data_y) + weights = None + if weight_per_event is not None: + # Create weights array matching data length + weights = np.full(data_x.shape, weight_per_event) + # Plot 2D histogram with weights (if provided) + H, xedges, yedges, img = ax.hist2d(data_x, data_y, bins=bins2d, range=[rx, ry], weights=weights) + cb = fig.colorbar(img, ax=ax) + cb.set_label("counts" if weights is None else "Expected events") + title = f"{label} 2D: {feat_x} vs {feat_y}\nmass={mass_mev:g} MeV" + if epsilon is not None: + title += f", epsilon={epsilon}" + title += f", Val={Val}" + ax.set_title(title) + ax.set_xlabel(feat_x) + ax.set_ylabel(feat_y) + ax.grid(True, alpha=0.15) + # Save to file + fname = f"twoD_{label}_{sanitize(feat_x)}__{sanitize(feat_y)}_{int(round(mass_mev))}MeV" + if epsilon is not None: + eps_str = str(epsilon).replace('.', 'p').replace('-', 'm') + fname += f"_eps{eps_str}" + fname += f"_V{Val}.png" + fpath = os.path.join(outdir, fname) + fig.tight_layout() + fig.savefig(fpath, dpi=160, bbox_inches="tight") + plt.close(fig) + return fpath + +def main(): + ap = argparse.ArgumentParser(description="Plot overlaid 1D and 2D histograms for signal/background with yield normalization.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV (used to select signal files and mass window).") + ap.add_argument("--epsilon", type=float, default=None, help="Kinetic mixing parameter (epsilon) for signal yield weighting.") + ap.add_argument("--Val", type=int, default=25, help="Tight selection parameter (e.g., z-threshold index). Default 25.") + ap.add_argument("--outdir", type=str, default="plots_all_features", help="Output directory for plots.") + ap.add_argument("--bins", type=int, default=80, help="Number of bins for 1D plots.") + ap.add_argument("--bins2d", type=int, default=80, help="Number of bins per axis for 2D plots.") + ap.add_argument("--features", type=str, nargs="*", default=None, help="Optional subset of feature names to include.") + ap.add_argument("--max-pairs", type=int, default=None, help="Optional cap on number of 2D feature pairs (per sample).") + ap.add_argument("--base-module", type=str, default="decayLength5sel", help="Signal base module name (default uses decayLength5sel.py).") + ap.add_argument("--debug", action="store_true", help="Print debug information about background loading.") + args = ap.parse_args() + + _ensure_dir(args.outdir) + + # Import the signal base module and load signal events + base = import_signal_base(args.base_module) + sig = load_signal_events(base, args.mass, args.Val) + if not sig: + sys.stderr.write("[error] No signal features loaded after selection.\n") + sys.exit(2) + + # Determine all signal feature names that have sufficient entries + all_sig_feats = sorted([k for k, v in sig.items() if isinstance(v, np.ndarray) and v.ndim == 1 and v.size >= 2]) + # Ensure 'vertex.pos_.fZ' is included if only alias present + if "vertex.pos_.fZ" not in all_sig_feats and "vertex.pos_.z" in all_sig_feats: + all_sig_feats.insert(0, "vertex.pos_.fZ") + + # Load background events (after tight selection and within ±10 MeV mass window) + bg_dict = load_background_events(args.mass/1.8, args.Val, desired_keys=set(all_sig_feats), debug=args.debug) + + # Filter features if user provided a specific list + if args.features: + selected_feats = [f for f in args.features if f in sig and (not bg_dict or f in bg_dict)] + if not selected_feats: + sys.stderr.write("[warn] --features list produced no usable features; falling back to common set.\n") + selected_feats = None + else: + selected_feats = None + + # Determine the final set of features to plot (common to both signal and background if available, else all signal features) + common_feats = pick_1d_numeric_features(sig, bg_dict) if bg_dict else sorted(list(sig.keys())) + features_1d = selected_feats if selected_feats else common_feats + + # If epsilon is not provided, we cannot compute actual yields; inform the user and exit + if args.epsilon is None: + sys.stderr.write("[error] --epsilon must be specified to compute signal/background yields.\n") + sys.exit(1) + + # Compute expected signal and background yields, and Zbi significance + epsilon = args.epsilon + mass_mev = float(args.mass) + # Signal acceptance and yield: + # Compute acceptance-weighted probability (sR, sP) for signal decays in acceptance using getSum + try: + sR, sP = base.getSum(epsilon, mass_mev, args.Val) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute signal acceptance fraction: {e}\n") + sys.exit(1) + signal_acceptance_frac = float(sR + sP) # total accepted fraction of decays for one A' + # Compute theoretical yield factor using aprime_yield logic (scale_const * ratio * mA * eps^2) + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) # from decayLength5sel (parralel_aprime) + ratio_val = base.ratio(mass_mev) # ratio(mA) includes polynomial m(x) fraction and cross-section factors + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) # expected number of A' produced per baseline (per N_B events baseline) + signal_fraction = signal_acceptance_frac * core # fraction of baseline events that result in an accepted signal + S_yield = N_B_TOTAL_RECO * signal_fraction # expected signal yield (S) + # Background yield: + # Compute polynomial mass-bin fraction m(x) for this mass (from combine_zbi.py) + x_gev = mass_mev / 1000.0 + poly_num = (-6860.03 + 299358.0 * x_gev - 4087220.0 * (x_gev ** 2) + + 25209900.0 * (x_gev ** 3) - 73485900.0 * (x_gev ** 4) + + 82579800.0 * (x_gev ** 5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # polynomial fraction m(x) + # Determine background selection efficiency (fraction of background in mass window that passes tight selection) + bg_fraction = 0.0 + if bg_dict: + # Numerator: number of background events after selection (within mass window) + numer = len(next(iter(bg_dict.values()))) if bg_dict else 0 + # Denominator: total background events in mass window (no selection). We need to compute this by reading invM from file. + denom = 0 + try: + if bg and hasattr(bg, "BACKGROUND_PATH"): + # Use background file path from module to count events in mass window + bg_paths = [] + cand = getattr(bg, "BACKGROUND_PATH", None) + if cand: + if isinstance(cand, (list, tuple)): + for cc in cand: + bg_paths.append(str(cc)) + else: + bg_paths.append(str(cand)) + if bg_paths: + # Use first background file (same as used in bg_dict) + bg_file = bg_paths[0] + with uproot.open(bg_file) as f: + t = None + if hasattr(bg, "_open_first_tree"): + t = bg._open_first_tree(f) + else: + # fallback: pick first TTree + keys = [k for k in f.keys() if ";" in k] + t = f[keys[0]] if keys else None + if t is not None: + invM_array = ak.to_numpy(t["vertex.invM_"].array(library="ak")) + center = mass_mev / (1000.0*1.8) # center of mass window in GeV + low, high = center - 0.01, center + 0.01 + mask_mass = (invM_array > low) & (invM_array < high) + denom = int(np.sum(mask_mass)) + except Exception as e: + if args.debug: + sys.stderr.write(f"[debug] Unable to compute background mass-window count: {e}\n") + if denom > 0: + bg_fraction = float(numer) / float(denom) + # Compute expected background yield in the ±10 MeV mass window after selection + # N_b_massbin = N_B_TOTAL_RECO * m_fraction * (mass window width in units of 0.1 GeV) + # The factor 10 corresponds to 0.01 GeV * 10 = 0.1 GeV (since poly_m_of_x returned fraction per 0.1 GeV) + N_b_massbin = N_B_TOTAL_RECO * m_fraction * 10.0 + B_yield = N_b_massbin * bg_fraction # expected background yield (B) after selection + # Compute Zbi significance + Zbi_value = zbi_significance(S_yield, B_yield) + + # 1D overlaid plots + out_1d_files = [] + if bg_dict: + # Overlay signal and background for each feature + for feat in features_1d: + try: + fpath = plot_1d_overlaid(sig[feat], bg_dict[feat], feat, args.outdir, + mass_mev, epsilon, args.Val, args.bins, + S_yield, B_yield, Zbi_value) + out_1d_files.append(fpath) + except Exception as e: + sys.stderr.write(f"[warn] 1D overlay failed for {feat}: {e}\n") + else: + # If no background available, plot signal only (weighted by S yield) + for feat in features_1d: + try: + arr = sig[feat] + # Use a simplified version of the 1D plotting for signal-only + fig, ax = plt.subplots(figsize=(7.8, 4.8)) + q1, q99 = np.quantile(arr, [0.005, 0.995]) if arr.size > 0 else (0, 1) + pad = 0.05 * (q99 - q1 + 1e-12) + rrange = (q1 - pad, q99 + pad) + weights = np.full(arr.shape, S_yield / arr.size) if arr.size > 0 else None + ax.hist(arr, bins=args.bins, range=rrange, weights=weights, histtype="step", + linewidth=1.8, label="signal") + ax.set_title(f"{feat}\nS={S_yield:.2g}, mass={args.mass:g} MeV, epsilon={epsilon}, Val={args.Val}") + ax.set_xlabel(feat) + ax.set_ylabel("Expected events") + ax.grid(True, alpha=0.3) + ax.legend() + fname = f"oned_signal_{sanitize(feat)}_{int(round(args.mass))}MeV" + if epsilon is not None: + eps_str = str(epsilon).replace('.', 'p').replace('-', 'm') + fname += f"_eps{eps_str}" + fname += f"_V{args.Val}.png" + f = os.path.join(args.outdir, fname) + fig.tight_layout() + fig.savefig(f, dpi=160, bbox_inches="tight") + plt.close(fig) + out_1d_files.append(f) + except Exception as e: + sys.stderr.write(f"[warn] 1D signal-only failed for {feat}: {e}\n") + + # 2D histograms for signal and background separately + feats_for_2d = features_1d + pairs = list(itertools.combinations(feats_for_2d, 2)) + if args.max_pairs is not None: + pairs = pairs[:int(args.max_pairs)] + out_2d_sig = [] + out_2d_bg = [] + # Signal 2D plots (each pair) + for fx, fy in pairs: + try: + x = sig[fx] + y = sig[fy] + if x.size >= 10 and y.size >= 10: + # Weight per signal event + w_sig = S_yield / x.size if x.size > 0 else None + fpath = plot_2d_single(x, y, fx, fy, "signal", args.outdir, + mass_mev, epsilon, args.Val, args.bins2d, w_sig) + out_2d_sig.append(fpath) + # If fewer than 10 events, skip plotting to avoid sparse meaningless plots + except Exception as e: + sys.stderr.write(f"[warn] 2D signal failed for {fx} vs {fy}: {e}\n") + # Background 2D plots (each pair) + if bg_dict: + for fx, fy in pairs: + try: + x = bg_dict[fx] + y = bg_dict[fy] + if x.size >= 10 and y.size >= 10: + w_bg = B_yield / x.size if x.size > 0 else None + fpath = plot_2d_single(x, y, fx, fy, "background", args.outdir, + mass_mev, epsilon, args.Val, args.bins2d, w_bg) + out_2d_bg.append(fpath) + except Exception as e: + sys.stderr.write(f"[warn] 2D background failed for {fx} vs {fy}: {e}\n") + + # Print summary of outputs + print(f"[done] 1D plots written: {len(out_1d_files)}") + print(f"[done] 2D signal plots written: {len(out_2d_sig)}") + print(f"[done] 2D background plots written: {len(out_2d_bg)}") + if out_1d_files: + print("Example output files:") + for p in out_1d_files[:min(5, len(out_1d_files))]: + print(" ", p) + +if __name__ == "__main__": + import itertools # import here to use combinations for 2D pairs + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables.py new file mode 100644 index 00000000..154af4c2 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +import argparse, sys, os, math +import numpy as np +import uproot +import awkward as ak + +from scipy.special import betainc, erfinv + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +def sanitize(name: str) -> str: + """Sanitize a string to use in file names by replacing special characters with underscore:contentReference[oaicite:11]{index=11}.""" + bad_chars = [" ", "/", "\\", "(", ")", "[", "]", "{", "}", ":", ";", ",", "|", "<", ">", "?", "*", "'", '"', "."] + out = name + for b in bad_chars: + out = out.replace(b, "_") + while "__" in out: + out = out.replace("__", "_") + return out.strip("_") + +def main(): + ap = argparse.ArgumentParser(description="Compute yields, significance, and produce plots for signal and background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV.") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter (epsilon).") + ap.add_argument("--Val", type=int, default=25, help="Tight selection parameter (e.g., z0 threshold index):contentReference[oaicite:12]{index=12}.") + ap.add_argument("--outdir", type=str, default="output_plots", help="Output directory for plots and tables:contentReference[oaicite:13]{index=13}.") + ap.add_argument("--bins", type=int, default=80, help="Number of bins for 1D plots:contentReference[oaicite:14]{index=14}.") + ap.add_argument("--bins2d", type=int, default=80, help="Number of bins per axis for 2D plots:contentReference[oaicite:15]{index=15}.") + ap.add_argument("--signal-file", type=str, default=None, help="Path to signal ROOT file for the given mass (preselection events).") + ap.add_argument("--base-module", type=str, default="decayLength5sel", help="Name of signal base module (default 'decayLength5sel'):contentReference[oaicite:16]{index=16}.") + ap.add_argument("--debug", action="store_true", help="Enable debug output.") + args = ap.parse_args() + + # Ensure output directory exists + os.makedirs(args.outdir, exist_ok=True) + + # Dynamically import the signal base module for physics calculations:contentReference[oaicite:17]{index=17}. + try: + base = __import__(args.base_module) + except ImportError as e: + sys.stderr.write(f"[error] Unable to import base module '{args.base_module}': {e}\n") + sys.exit(1) + # Dynamically import or use background module for selection functions + try: + import bk_eff_selection as bg + except ImportError as e: + sys.stderr.write(f"[error] Unable to import background selection module: {e}\n") + sys.exit(1) + + mass_mev = float(args.mass) + epsilon = float(args.epsilon) + Val = int(args.Val) + x_gev = mass_mev / 1000.0 + # Compute polynomial mass fraction m(x) for background yield:contentReference[oaicite:18]{index=18}. + m_fraction = 0.0 + try: + # Use poly_m_of_x from combine_zbi if available + from combine_zbi import poly_m_of_x as _poly_m + m_fraction = _poly_m(x_gev) + except Exception: + # Fallback to polynomial formula:contentReference[oaicite:19]{index=19} + num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + 25209900.0*(x_gev**3) + - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = num / (82.9268041667*1000.0) + # Ensure m_fraction is non-negative and zero outside valid range:contentReference[oaicite:20]{index=20}. + if x_gev < 0.05 or x_gev > 0.25: + m_fraction = 0.0 + N_B_TOTAL = 3.0e9 + + # Load background events from file (preselection tree) using the background module's config. + bg_file = None + cand = getattr(bg, "BACKGROUND_PATH", None) + if cand: + if isinstance(cand, (list, tuple)): + bg_paths = [str(p) for p in cand] + else: + bg_paths = [str(cand)] + if bg_paths: + bg_file = bg_paths[0] + if not bg_file or not os.path.exists(bg_file): + sys.stderr.write(f"[error] Background file not found: {bg_file}\n") + sys.exit(1) + if args.debug: + print(f"[debug] Loading background file: {bg_file}") + with uproot.open(bg_file) as f: + # Use custom tree opener if available:contentReference[oaicite:23]{index=23}. + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Read necessary branches into Awkward arrays:contentReference[oaicite:24]{index=24}. + branches = ["vertex.invM_", "vtx_proj_sig", "ele.track_.z0_", "pos.track_.z0_", "psum"] + # include hit_layers for L0L1 calculation + branches += ["ele.track_.hit_layers_", "pos.track_.hit_layers_"] + arrays = tree.arrays(branches, library="ak", how=dict) + # Convert Awkward arrays to numpy for convenience + invM_bg = ak.to_numpy(arrays.get("vertex.invM_")) + proj_sig_bg = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_z0_bg = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0_bg = ak.to_numpy(arrays.get("pos.track_.z0_")) + psum_bg = ak.to_numpy(arrays.get("psum")) + # Determine L0L1 hit flags for each track:contentReference[oaicite:25]{index=25}:contentReference[oaicite:26]{index=26}. + ele_layers = arrays.get("ele.track_.hit_layers_") + pos_layers = arrays.get("pos.track_.hit_layers_") + if ele_layers is not None: + ele_hasL0 = ak.to_numpy(ak.any(ele_layers == 0, axis=-1)) + ele_hasL1 = ak.to_numpy(ak.any(ele_layers == 1, axis=-1)) + ele_hasL0L1 = np.asarray(ele_hasL0 & ele_hasL1, dtype=bool) + else: + ele_hasL0L1 = np.ones_like(invM_bg, dtype=bool) + if pos_layers is not None: + pos_hasL0 = ak.to_numpy(ak.any(pos_layers == 0, axis=-1)) + pos_hasL1 = ak.to_numpy(ak.any(pos_layers == 1, axis=-1)) + pos_hasL0L1 = np.asarray(pos_hasL0 & pos_hasL1, dtype=bool) + else: + pos_hasL0L1 = np.ones_like(invM_bg, dtype=bool) + # Compute mass window mask (±10 MeV around m/1.8):contentReference[oaicite:27]{index=27}:contentReference[oaicite:28]{index=28}. + center = mass_mev / (1000.0 * 1.8) + low_edge, high_edge = center - 0.01, center + 0.01 # 10 MeV window in GeV:contentReference[oaicite:29]{index=29}. + mask_mass_bg = (invM_bg > low_edge) & (invM_bg < high_edge) + # Sequentially apply cuts: psum, L1L1, proj_sig, z0. + total_events = len(invM_bg) + # Baseline (no tight cuts, only preselection baseline): + inside0 = np.sum(mask_mass_bg) + outside0 = np.sum(~mask_mass_bg) + # psum cut (1.5 <= psum <= 3.0 GeV):contentReference[oaicite:30]{index=30}. + mask_psum = (psum_bg >= 1.5) & (psum_bg <= 3.0) + inside1 = np.sum(mask_mass_bg & mask_psum) + outside1 = np.sum(~mask_mass_bg & mask_psum) + # L1L1 cut (both tracks have L0 & L1 hits):contentReference[oaicite:31]{index=31}. + mask_L1 = ele_hasL0L1 & pos_hasL0L1 + inside2 = np.sum(mask_mass_bg & mask_psum & mask_L1) + outside2 = np.sum(~mask_mass_bg & mask_psum & mask_L1) + # proj_sig cut (vertex projection significance < 1.6):contentReference[oaicite:32]{index=32}. + mask_proj = proj_sig_bg < 1.6 + inside3 = np.sum(mask_mass_bg & mask_psum & mask_L1 & mask_proj) + outside3 = np.sum(~mask_mass_bg & mask_psum & mask_L1 & mask_proj) + # z0 cut (both tracks |z0| > zthr):contentReference[oaicite:33]{index=33}:contentReference[oaicite:34]{index=34}. + zthr = 0.5 * (Val * (1.0/25.0)) + mask_z0 = (np.abs(ele_z0_bg) > zthr) & (np.abs(pos_z0_bg) > zthr) + inside4 = np.sum(mask_mass_bg & mask_psum & mask_L1 & mask_proj & mask_z0) + outside4 = np.sum(~mask_mass_bg & mask_psum & mask_L1 & mask_proj & mask_z0) + # Calculate expected background yields at each stage using polynomial scaling:contentReference[oaicite:35]{index=35}:contentReference[oaicite:36]{index=36}. + N_b_massbin = N_B_TOTAL * m_fraction * 10.0 # expected BG count in ±0.1 GeV mass bin:contentReference[oaicite:37]{index=37}. + B0 = N_b_massbin * (inside0 / inside0 if inside0>0 else 0) # should equal N_b_massbin + B1 = N_b_massbin * (inside1 / inside0 if inside0>0 else 0) + B2 = N_b_massbin * (inside2 / inside0 if inside0>0 else 0) + B3 = N_b_massbin * (inside3 / inside0 if inside0>0 else 0) + B4 = N_b_massbin * (inside4 / inside0 if inside0>0 else 0) + # Compute signal yields at various stages. + # Theoretical production rate core = const * ratio(m) * mA * eps^2:contentReference[oaicite:38]{index=38}:contentReference[oaicite:39]{index=39}. + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0/137.0459991)) # from decayLength5sel (parallel aprime calculation):contentReference[oaicite:40]{index=40}. + ratio_val = base.ratio(mass_mev) if hasattr(base, "ratio") else 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + # Branching ratio to e+e- (visible decays). For m_A above 2*m_mu, include muon channel. + mA_GeV = mass_mev / 1000.0 + # Expected number of A' produced (no acceptance cuts) + S_produced = N_B_TOTAL * core + # Signal acceptance fraction without tight selection (Val=0):contentReference[oaicite:41]{index=41}:contentReference[oaicite:42]{index=42}. + try: + sR0, sP0 = base.getSum(epsilon, mass_mev, 0) + except Exception as e: + sR0, sP0 = (0.0, 0.0) + if args.debug: + sys.stderr.write(f"[debug] getSum with Val=0 failed: {e}\n") + total_accept_frac = float(sR0 + sP0) + # Signal acceptance fraction with tight selection (Val):contentReference[oaicite:43]{index=43}:contentReference[oaicite:44]{index=44}. + try: + sR, sP = base.getSum(epsilon, mass_mev, Val) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute signal acceptance fraction: {e}\n") + sys.exit(1) + signal_accept_frac = float(sR + sP) + # Expected signal yields: + S_accept = S_produced * total_accept_frac # after geometric acceptance (pre-selection) + S_final = S_produced * signal_accept_frac # after tight selection cuts + # Compute Zbi significance at each cut stage (using S_accept for stages before z0, and S_final after z0). + Z0 = zbi_significance(S_accept, B0) + Z1 = zbi_significance(S_accept, B1) + Z2 = zbi_significance(S_accept, B2) + Z3 = zbi_significance(S_accept, B3) + Z4 = zbi_significance(S_final, B4) + # Prepare LaTeX tables content. + tex_lines = [] + tex_lines.append("Entries for mass = %.1f MeV, epsilon = %.2g, Val = %d" % (mass_mev, epsilon, Val)) + # Background cutflow table + tex_lines.append("\\begin{table}[h]\\centering") + tex_lines.append("\\begin{tabular}{lccc} \\hline") + tex_lines.append("Cut stage & In-window & Out-of-window & $B_\\text{exp}$ \\\\ \\hline") + tex_lines.append(f"No cuts & {inside0} & {outside0} & {B0:.2g} \\\\") + tex_lines.append(f"Psum cut & {inside1} & {outside1} & {B1:.2g} \\\\") + tex_lines.append(f"L1L1 cut & {inside2} & {outside2} & {B2:.2g} \\\\") + tex_lines.append(f"Pointing cut & {inside3} & {outside3} & {B3:.2g} \\\\") + tex_lines.append(f"Min $|z0|$ cut & {inside4} & {outside4} & {B4:.2g} \\\\ \\hline") + tex_lines.append("\\end{tabular}") + tex_lines.append("\\caption{Background cutflow: counts inside/outside mass window and expected yield in signal region.}") + tex_lines.append("\\end{table}") + tex_lines.append("") + # Signal yield table + tex_lines.append("\\begin{table}[h]\\centering") + tex_lines.append("\\begin{tabular}{lc} \\hline") + tex_lines.append("Signal stage & Expected yield \\\\ \\hline") + tex_lines.append(f"Produced $A'$ & {S_produced:.2g} \\\\") + tex_lines.append(f"Visible $e^+e^-$ decays & {S_visible:.2g} \\\\") + tex_lines.append(f"In acceptance (pre-selection) & {S_accept:.2g} \\\\") + tex_lines.append(f"After tight cuts & {S_final:.2g} \\\\ \\hline") + tex_lines.append("\\end{tabular}") + tex_lines.append("\\caption{Expected signal yields at each stage for $m_{A'}=%g$ MeV, $\\epsilon=%g$.}" % (mass_mev, epsilon)) + tex_lines.append("\\end{table}") + tex_lines.append("") + # Significance table + tex_lines.append("\\begin{table}[h]\\centering") + tex_lines.append("\\begin{tabular}{lc} \\hline") + tex_lines.append("Selection stage & $Z_{Bi}$ \\\\ \\hline") + tex_lines.append(f"No selection & {Z0:.2f} \\\\") + tex_lines.append(f"Psum cut & {Z1:.2f} \\\\") + tex_lines.append(f"L1L1 cut & {Z2:.2f} \\\\") + tex_lines.append(f"Pointing cut & {Z3:.2f} \\\\") + tex_lines.append(f"After $|z0|$ cut & {Z4:.2f} \\\\ \\hline") + tex_lines.append("\\end{tabular}") + tex_lines.append("\\caption{Calculated $Z_{Bi}$ significance after each cut stage.}") + tex_lines.append("\\end{table}") + # Write LaTeX output to file + tex_filename = os.path.join(args.outdir, f"results_{int(round(mass_mev))}MeV_V{Val}.tex") + with open(tex_filename, "w") as fout: + fout.write("\n".join(tex_lines)) + if args.debug: + print(f"[debug] Wrote LaTeX tables to {tex_filename}") + # Now load signal events for plotting if available + sig_events = {} + if args.signal_file: + sig_file_path = args.signal_file + else: + sig_file_path = getattr(base, "SIGNAL_PATH", None) + if sig_file_path: + if args.debug: + print(f"[debug] Loading signal file: {sig_file_path}") + try: + with uproot.open(sig_file_path) as sf: + # Use same tree opening logic as background (preselection) + if hasattr(bg, "_open_first_tree"): + tree_s = bg._open_first_tree(sf) + else: + keys = [k for k in sf.keys() if ";" in k] + tree_s = sf[keys[0]] if keys else None + arrays_s = tree_s.arrays(branches, library="ak", how=dict) + invM_sig = ak.to_numpy(arrays_s.get("vertex.invM_")) + proj_sig = ak.to_numpy(arrays_s.get("vtx_proj_sig")) + ele_z0_sig = ak.to_numpy(arrays_s.get("ele.track_.z0_")) + pos_z0_sig = ak.to_numpy(arrays_s.get("pos.track_.z0_")) + psum_sig = ak.to_numpy(arrays_s.get("psum")) + ele_layers_s = arrays_s.get("ele.track_.hit_layers") or arrays_s.get("ele.track_.hit_layers_") + pos_layers_s = arrays_s.get("pos.track_.hit_layers") or arrays_s.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + mask_mass_sig = (invM_sig > low_edge) & (invM_sig < high_edge) + mask_tight_sig = (psum_sig >= 1.5) & (psum_sig <= 3.0) & ele_L1L1_s & pos_L1L1_s & (proj_sig < 1.6) & ((np.abs(ele_z0_sig) > zthr) & (np.abs(pos_z0_sig) > zthr)) + # Apply selection mask and mass window + mask_final_sig = mask_mass_sig & mask_tight_sig + # Build signal events dictionary for features after selection + for key, arr in arrays_s.items(): + arr_np = ak.to_numpy(arr) + if arr_np is None or arr_np.ndim != 1: + continue + sig_events[key] = arr_np[mask_final_sig] + except Exception as e: + sys.stderr.write(f"[warn] Failed to load or process signal file: {e}\n") + sig_events = {} + else: + if args.debug: + print("[debug] No signal file provided or configured; skipping signal event plots.") + # Build background events dict after selection (already applied sequentially above) + bg_events = {} + # We have masks for final selection: + mask_final_bg = mask_mass_bg & mask_psum & mask_L1 & mask_proj & mask_z0 + if total_events and np.sum(mask_final_bg) > 0: + # Filter numeric arrays + for arr_name, arr_val in [("vertex.invM_", invM_bg), ("vtx_proj_sig", proj_sig_bg), + ("ele.track_.z0_", ele_z0_bg), ("pos.track_.z0_", pos_z0_bg), + ("psum", psum_bg)]: + if arr_val is not None and len(arr_val) == total_events: + bg_events[arr_name] = arr_val[mask_final_bg] + # Determine common features for plotting:contentReference[oaicite:45]{index=45}:contentReference[oaicite:46]{index=46}. + features_1d = [] + if sig_events: + for feat in sig_events.keys(): + if feat in bg_events and sig_events[feat].size >= 2 and bg_events[feat].size >= 2: + features_1d.append(feat) + if not features_1d: + # If no common features (or no background), use all signal features + features_1d = sorted([k for k,v in sig_events.items() if v.size >= 2]) + elif bg_events: + features_1d = sorted([k for k,v in bg_events.items() if v.size >= 2]) + else: + features_1d = [] + # Plot 1D overlaid histograms for each feature:contentReference[oaicite:47]{index=47}:contentReference[oaicite:48]{index=48}. + for feat in features_1d: + sig_arr = sig_events.get(feat, np.array([])) + bg_arr = bg_events.get(feat, np.array([])) + if sig_arr.size == 0 and bg_arr.size == 0: + continue + # Determine histogram range between 0.5% and 99.5% quantiles:contentReference[oaicite:49]{index=49}. + if bg_arr.size > 0: + combined = np.concatenate([sig_arr, bg_arr]) if sig_arr.size>0 else bg_arr + else: + combined = sig_arr + q1, q99 = np.quantile(combined, [0.005, 0.995]) if combined.size > 0 else (0, 1) + pad = 0.05 * (q99 - q1 + 1e-12) + hist_range = (q1 - pad, q99 + pad) + # Compute weights so that total area equals expected yields:contentReference[oaicite:50]{index=50}. + sig_weights = np.full(sig_arr.shape, S_final/ sig_arr.size) if sig_arr.size > 0 else None + B_final = B4 # expected B yield after final selection + bg_weights = np.full(bg_arr.shape, B_final/ bg_arr.size) if bg_arr.size > 0 else None + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(7.8, 4.8)) + if sig_arr.size > 0: + ax.hist(sig_arr, bins=args.bins, range=hist_range, weights=sig_weights, histtype="step", linewidth=1.8, label="signal") + if bg_arr.size > 0: + ax.hist(bg_arr, bins=args.bins, range=hist_range, weights=bg_weights, histtype="step", linewidth=1.8, label="background") + title_line1 = f"{feat}" + title_line2 = f"S={S_final:.2g}, B={B4:.2g}, Zbi={Z4:.2f}; mass={mass_mev:g} MeV, " + title_line2 += f"epsilon={epsilon}, Val={Val}" + ax.set_title(f"{title_line1}\n{title_line2}") + ax.set_xlabel(feat) + ax.set_ylabel("Expected events") + ax.grid(True, alpha=0.3) + ax.legend() + ax.set_yscale("log") + ymin, ymax = ax.get_ylim() + ax.set_ylim(bottom=max(ymin, 1e-6), top=ymax) + fname = f"oned_{sanitize(feat)}_{int(round(mass_mev))}MeV" + eps_str = str(epsilon).replace('.', 'p').replace('-', 'm') + fname += f"_eps{eps_str}_V{Val}.png" + out_path = os.path.join(args.outdir, fname) + fig.tight_layout() + fig.savefig(out_path, dpi=160, bbox_inches="tight") + plt.close(fig) + # Plot 2D histograms for each feature pair (signal and background separately):contentReference[oaicite:51]{index=51}:contentReference[oaicite:52]{index=52}. + if features_1d: + feats_for_2d = features_1d + pairs = [(feats_for_2d[i], feats_for_2d[j]) for i in range(len(feats_for_2d)) for j in range(i+1, len(feats_for_2d))] + if args.bins2d < len(pairs): + pairs = pairs[:args.bins2d] + import matplotlib.pyplot as plt + for fx, fy in pairs: + # Signal 2D + x_sig = sig_events.get(fx, np.array([])) + y_sig = sig_events.get(fy, np.array([])) + if x_sig.size >= 10 and y_sig.size >= 10: + weight_sig = S_final / x_sig.size if x_sig.size>0 else None + fig, ax = plt.subplots(figsize=(6.6, 5.8)) + rx = np.quantile(x_sig, [0.01, 0.99]); ry = np.quantile(y_sig, [0.01, 0.99]) + pad_x = 0.05 * (rx[1] - rx[0] + 1e-12); pad_y = 0.05 * (ry[1] - ry[0] + 1e-12) + range2d = [(rx[0]-pad_x, rx[1]+pad_x), (ry[0]-pad_y, ry[1]+pad_y)] + H, xedges, yedges, img = ax.hist2d(x_sig, y_sig, bins=args.bins2d, range=range2d, weights=(np.full(x_sig.shape, weight_sig) if weight_sig is not None else None)) + cb = fig.colorbar(img, ax=ax) + cb.set_label("Expected events") + ax.set_title(f"Signal 2D: {fx} vs {fy}\\nmass={mass_mev:g} MeV, epsilon={epsilon}, Val={Val}") + ax.set_xlabel(fx); ax.set_ylabel(fy) + ax.grid(True, alpha=0.15) + fname2d = f"twoD_signal_{sanitize(fx)}__{sanitize(fy)}_{int(round(mass_mev))}MeV" + fname2d += f"_eps{eps_str}_V{Val}.png" + fpath2d = os.path.join(args.outdir, fname2d) + fig.tight_layout(); fig.savefig(fpath2d, dpi=160, bbox_inches="tight"); plt.close(fig) + # Background 2D + x_bg = bg_events.get(fx, np.array([])) + y_bg = bg_events.get(fy, np.array([])) + if x_bg.size >= 10 and y_bg.size >= 10: + weight_bg = B4 / x_bg.size if x_bg.size>0 else None + fig, ax = plt.subplots(figsize=(6.6, 5.8)) + rx = np.quantile(x_bg, [0.01, 0.99]); ry = np.quantile(y_bg, [0.01, 0.99]) + pad_x = 0.05 * (rx[1] - rx[0] + 1e-12); pad_y = 0.05 * (ry[1] - ry[0] + 1e-12) + range2d = [(rx[0]-pad_x, rx[1]+pad_x), (ry[0]-pad_y, ry[1]+pad_y)] + H, xedges, yedges, img = ax.hist2d(x_bg, y_bg, bins=args.bins2d, range=range2d, weights=(np.full(x_bg.shape, weight_bg) if weight_bg is not None else None)) + cb = fig.colorbar(img, ax=ax) + cb.set_label("Expected events") + ax.set_title(f"Background 2D: {fx} vs {fy}\\nmass={mass_mev:g} MeV, epsilon={epsilon}, Val={Val}") + ax.set_xlabel(fx); ax.set_ylabel(fy) + ax.grid(True, alpha=0.15) + fname2d = f"twoD_background_{sanitize(fx)}__{sanitize(fy)}_{int(round(mass_mev))}MeV" + fname2d += f"_eps{eps_str}_V{Val}.png" + fpath2d = os.path.join(args.outdir, fname2d) + fig.tight_layout(); fig.savefig(fpath2d, dpi=160, bbox_inches="tight"); plt.close(fig) + # Print summary of outputs + print(f"[done] LaTeX tables written to: {tex_filename}") + # Optionally print some plot file examples + # (List up to 5 example output files) + out_images = [] + for root, dirs, files in os.walk(args.outdir): + for fname in files: + if fname.endswith(".png"): + out_images.append(os.path.join(root, fname)) + if out_images: + print(f"[done] {len(out_images)} plots saved. Example files:") + for p in out_images[:min(5, len(out_images))]: + print(" ", p) + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_2.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_2.py new file mode 100644 index 00000000..07faab4c --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_2.py @@ -0,0 +1,764 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength5sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength5sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + args = ap.parse_args() + + mass_mev = args.mass + epsilon = args.epsilon + Val = args.Val + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength5sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = 1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + s_proj_mask = (s_proj < 1.6) + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength5sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val) + zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted decay length: "+str(rho_length)) + print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + #for i in range(len(s_x)): + # #print("I get to "+str(i)) + # z = s_vertex_z[i] + # #print(z) + # if not (den_edges[0] <= z < den_edges[-1]): + # continue + # mk = base._mass_key(m_V_D) + # mask = base.tight_selection(events,Val) + # zvals = events["vertex.pos_.fZ"][mask] + # num_vals, _ = np.histogram(zvals, bins=den_edges) + # I = np.searchsorted(den_edges, z, side="right") - 1 + # #counts[I]+=1.0 + # Ngen = float(den_vals[I]) + # Nacc = float(num_vals[I]) + # #Nacc = Ngen + # rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + # phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + # rho_length=(1000*HBAR_C*10.0)/rho_width + # phi_length=(1000*HBAR_C*10.0)/phi_width + # valuesr[I]=valuesr[I]+(Nacc/Ngen)*np.exp(-s_x[i]/rho_length)/(rho_length*s_gamma[i]) + # valuesp[I]=valuesp[I]+(Nacc/Ngen)*np.exp(-s_x[i]/phi_length)/(phi_length*s_gamma[i]) + # #print(counts) + # #print(valuesr) + # #print(rho_width) + # #print(phi_width) + # #print(s_x[i]) + # #print(s_gamma[i]) + # #print(rho_length) + # #print(phi_length) + # #print("Done here: "+str(np.exp(-s_x[i]/rho_length)/(rho_length*s_gamma[i]))) + # #print(pphi) + #prho=sum([valuesr[I]/(counts[I]+1.0*(counts[I]==0)) for I in range(len(den_vals))]) + #pphi=sum([valuesp[I]/(counts[I]+1.0*(counts[I]==0)) for I in range(len(den_vals))]) + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + file1 = open(args.outdir+"/latek.tex",'w') + # ------------------------------ + # Output LaTeX-formatted tables + # ------------------------------ + # Background cutflow table + + file1.write("\\documentclass{article}\n") + file1.write("\\usepackage{graphicx} % Required for inserting images\n") + file1.write("\\usepackage{amsmath}\n") + #file1.write("\\pagecolor{white}\n") + file1.write("\\usepackage{xcolor}\n") + file1.write("\\pagecolor[rgb]{1,1,1}\n") + file1.write("\\title{TablesForBackgroundRates}\n") + file1.write("\\author{rodwyer100 }\n") + file1.write("\\date{November 2025}\n") + file1.write("\\begin{document}\n") + file1.write("\\maketitle\n") + file1.write("\\begin{tabular}{l|r}\n") + file1.write("Original Core & "+str(core)+"\\\\\n") + file1.write("Total Num Events & 3e9\\\\\n") + file1.write("Area Under Curve At Point & "+str(m_fraction * 4.0)+"\\\\\n") + file1.write("Events At Mass & "+str(format_sci(N_b_massbin))+"\\\\\n") + file1.write("A Prime Yield &"+str(format_sci(prod_yield))+"\\\\\n") + file1.write("Rho BR and Phi BR &"+str(np.round(rho_fraction,4))+","+str(np.round(phi_fraction,4))+"\\\\\n") + file1.write("Visible Yield &"+ str(format_sci(vis_yield))+"\\\\\n") + file1.write("Rho Acc and Phi Acc &"+str(np.round(prho,6))+","+str(np.round(pphi,6))+"\\\\\n") + file1.write("Rho Yield and Phi Yield &"+str(format_sci(prod_yield*prho*rho_fraction))+","+str(format_sci(prod_yield*pphi*phi_fraction))+"\\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\\textbf{Cut 0}\n") + if bg is not None: + file1.write("\n%% Background cutflow table\n") + file1.write("\\begin{tabular}{l|rr|rr|r}\n") + file1.write("\\textbf{Cut stage} & $N_{\\text{in}}$ & $N_{\\text{out}}$ & In~(\\% total) & Out~(\\% total) & $B_{\\text{yield}}$ \\\\ \\hline\n") + for stage, n_in, n_out, pct_in, pct_out, B_yield, _ in bg_cutflow: + # Format counts as integers and yields in scientific notation + count_in_str = f"{int(n_in)}" + count_out_str = f"{int(n_out)}" + pct_in_str = f"{pct_in:.2f}\\%" + pct_out_str = f"{pct_out:.2f}\\%" + yield_str = format_sci(B_yield) if B_yield is not None else "--" + file1.write(f"{stage} & {count_in_str} & {count_out_str} & {pct_in_str} & {pct_out_str} & ${yield_str}$ \\\\\n") + file1.write("\\end{tabular}\n") + # Signal yield cutflow table + file1.write("%% Signal yield table\n") + file1.write("\\begin{tabular}{l|r@{~}r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & (cum.\\,eff\\%) \\\\ \\hline\n") + # We include production and visible as additional rows for clarity + prod_str = format_sci(prod_yield) + vis_str = format_sci(vis_yield) + # Visible fraction percent = rho_fraction*100 + vis_frac_pct = rho_fraction * 100.0 + file1.write(f"Production (total) & ${prod_str}$ & (100\\%) \\\\\n") + file1.write(f"Visible decays & ${vis_str}$ & ({vis_frac_pct:.2f}\\%) \\\\\n") + # Acceptance stage is already in sig_cutflow[0] + for stage, S_yield, eff in sig_cutflow: + yield_str = format_sci(S_yield) + file1.write(f"{stage} & ${yield_str}$ & ({eff:.2f}\\%) \\\\\n") + file1.write("\\end{tabular}\n") + # Significance per step table + file1.write("\hline") + file1.write("%% Significance per selection stage\n") + file1.write("\\begin{tabular}{l|r r r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & $B_{\\text{yield}}$ & $Z_{\\text{Bi}}$ \\\\ \\hline\n") + for stage, S_yield, B_yield, Zbi_val in sig_table: + S_str = format_sci(S_yield) + B_str = format_sci(B_yield) + Z_str = f"{Zbi_val:.2f}" if math.isfinite(Zbi_val) else "$\\infty$" + file1.write(f"{stage} & ${S_str}$ & ${B_str}$ & {Z_str} \\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\n") # blank line after tables + file1.write("\\end{document}\n") + file1.close() + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + # Choose features to plot: use common features present in both samples + features_to_plot = ["vertex.pos_.fZ", "ele.track_.z0_", "pos.track_.z0_", "psum", "vtx_proj_sig"] + # Ensure those exist in events and arrays + features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + # Plot 1D histograms overlay + for feat in features_to_plot: + s_data = np.asarray(events[feat])[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size) + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + plt.figure(figsize=(7.5, 4.5)) + bins = args.bins + plt.hist(s_data, bins=bins, range=hist_range, weights=s_w, histtype="step", linewidth=1.8, label="Signal") + if b_data.size > 0: + plt.hist(b_data, bins=bins, range=hist_range, weights=b_w, histtype="step", linewidth=1.8, label="Background") + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + # Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + if args.debug: + print(f"[debug] Saved 1D plot: {outfile}") + # Plot 2D histograms for each pair of features (for signal and background separately) + feat_pairs = [(features_to_plot[i], features_to_plot[j]) for i in range(len(features_to_plot)) for j in range(i+1, len(features_to_plot))] + # Optionally limit number of pairs + max_pairs = getattr(args, "max_pairs", None) + if max_pairs: + feat_pairs = feat_pairs[:int(max_pairs)] + for fx, fy in feat_pairs: + # Prepare data + s_x = np.asarray(events[fx])[sig_final_mask] + s_y = np.asarray(events[fy])[sig_final_mask] + if bg_final_mask is not None and fx in arrays.keys() and fy in arrays.keys(): + b_x = ak.to_numpy(arrays[fx])[bg_final_mask] + b_y = ak.to_numpy(arrays[fy])[bg_final_mask] + else: + b_x = np.array([]); b_y = np.array([]) + # Plot for signal + if s_x.size >= 2 and s_y.size >= 2: + plt.figure(figsize=(6.5, 5.5)) + H, xedges, yedges = np.histogram2d(s_x, s_y, bins=args.bins2d) + plt.pcolormesh(xedges, yedges, H.T, cmap='Blues') + plt.colorbar(label="Signal count") + plt.xlabel(fx); plt.ylabel(fy) + plt.title(f"Signal {fx} vs {fy} (after cuts)") + outfile = os.path.join(args.outdir, f"plot2D_signal_{fx.replace('.', '_')}_vs_{fy.replace('.', '_')}.png") + plt.tight_layout(); plt.savefig(outfile, dpi=150); plt.close() + # Plot for background + if b_x.size >= 2 and b_y.size >= 2: + plt.figure(figsize=(6.5, 5.5)) + H, xedges, yedges = np.histogram2d(b_x, b_y, bins=args.bins2d) + plt.pcolormesh(xedges, yedges, H.T, cmap='Oranges') + plt.colorbar(label="Background count") + plt.xlabel(fx); plt.ylabel(fy) + plt.title(f"Background {fx} vs {fy} (after cuts)") + outfile = os.path.join(args.outdir, f"plot2D_background_{fx.replace('.', '_')}_vs_{fy.replace('.', '_')}.png") + plt.tight_layout(); plt.savefig(outfile, dpi=150); plt.close() + + + plotRates(args.outdir) + if args.debug: + print("[done] All tables generated and plots saved.") + compile_latex(args.outdir+"/latek.tex") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3.py new file mode 100644 index 00000000..107bfa23 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3.py @@ -0,0 +1,848 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + mass_mev = args.mass + epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = 1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + ####REWEIGHTING SHIT HAPPENS HERE + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print(z_temp) + z_temp=z_temp*(z_temp>=0.0) + print(z_temp) + #print(gamma_temp) + #print(z_temp/(gamma_temp*rho_length)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + p_accept_temp/= max(p_accept_temp) + + rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print(u) + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + #events = events[mask] + #print("GOT HERE") + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + + print(Val2) + projval=10.0*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + + + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val,Val2) + zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted decay length: "+str(rho_length)) + print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + if(z_cent<0): + z_cent=0 + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + #for i in range(len(s_x)): + # #print("I get to "+str(i)) + # z = s_vertex_z[i] + # #print(z) + # if not (den_edges[0] <= z < den_edges[-1]): + # continue + # mk = base._mass_key(m_V_D) + # mask = base.tight_selection(events,Val) + # zvals = events["vertex.pos_.fZ"][mask] + # num_vals, _ = np.histogram(zvals, bins=den_edges) + # I = np.searchsorted(den_edges, z, side="right") - 1 + # #counts[I]+=1.0 + # Ngen = float(den_vals[I]) + # Nacc = float(num_vals[I]) + # #Nacc = Ngen + # rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + # phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + # rho_length=(1000*HBAR_C*10.0)/rho_width + # phi_length=(1000*HBAR_C*10.0)/phi_width + # valuesr[I]=valuesr[I]+(Nacc/Ngen)*np.exp(-s_x[i]/rho_length)/(rho_length*s_gamma[i]) + # valuesp[I]=valuesp[I]+(Nacc/Ngen)*np.exp(-s_x[i]/phi_length)/(phi_length*s_gamma[i]) + # #print(counts) + # #print(valuesr) + # #print(rho_width) + # #print(phi_width) + # #print(s_x[i]) + # #print(s_gamma[i]) + # #print(rho_length) + # #print(phi_length) + # #print("Done here: "+str(np.exp(-s_x[i]/rho_length)/(rho_length*s_gamma[i]))) + # #print(pphi) + #prho=sum([valuesr[I]/(counts[I]+1.0*(counts[I]==0)) for I in range(len(den_vals))]) + #pphi=sum([valuesp[I]/(counts[I]+1.0*(counts[I]==0)) for I in range(len(den_vals))]) + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + file1 = open(args.outdir+"/latek.tex",'w') + # ------------------------------ + # Output LaTeX-formatted tables + # ------------------------------ + # Background cutflow table + + file1.write("\\documentclass{article}\n") + file1.write("\\usepackage{graphicx} % Required for inserting images\n") + file1.write("\\usepackage{amsmath}\n") + #file1.write("\\pagecolor{white}\n") + file1.write("\\usepackage{xcolor}\n") + file1.write("\\pagecolor[rgb]{1,1,1}\n") + file1.write("\\title{TablesForBackgroundRates}\n") + file1.write("\\author{rodwyer100 }\n") + file1.write("\\date{November 2025}\n") + file1.write("\\begin{document}\n") + file1.write("\\maketitle\n") + file1.write("\\begin{tabular}{l|r}\n") + file1.write("Original Core & "+str(core)+"\\\\\n") + file1.write("Total Num Events & 3e9\\\\\n") + file1.write("Area Under Curve At Point & "+str(m_fraction * 4.0)+"\\\\\n") + file1.write("Events At Mass & "+str(format_sci(N_b_massbin))+"\\\\\n") + file1.write("A Prime Yield &"+str(format_sci(prod_yield))+"\\\\\n") + file1.write("Rho BR and Phi BR &"+str(np.round(rho_fraction,4))+","+str(np.round(phi_fraction,4))+"\\\\\n") + file1.write("Visible Yield &"+ str(format_sci(vis_yield))+"\\\\\n") + file1.write("Rho Acc and Phi Acc &"+str(np.round(prho,6))+","+str(np.round(pphi,6))+"\\\\\n") + file1.write("Rho Yield and Phi Yield &"+str(format_sci(prod_yield*prho*rho_fraction))+","+str(format_sci(prod_yield*pphi*phi_fraction))+"\\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\\textbf{Cut 0}\n") + if bg is not None: + file1.write("\n%% Background cutflow table\n") + file1.write("\\begin{tabular}{l|rr|rr|r}\n") + file1.write("\\textbf{Cut stage} & $N_{\\text{in}}$ & $N_{\\text{out}}$ & In~(\\% total) & Out~(\\% total) & $B_{\\text{yield}}$ \\\\ \\hline\n") + for stage, n_in, n_out, pct_in, pct_out, B_yield, _ in bg_cutflow: + # Format counts as integers and yields in scientific notation + count_in_str = f"{int(n_in)}" + count_out_str = f"{int(n_out)}" + pct_in_str = f"{pct_in:.2f}\\%" + pct_out_str = f"{pct_out:.2f}\\%" + yield_str = format_sci(B_yield) if B_yield is not None else "--" + file1.write(f"{stage} & {count_in_str} & {count_out_str} & {pct_in_str} & {pct_out_str} & ${yield_str}$ \\\\\n") + file1.write("\\end{tabular}\n") + # Signal yield cutflow table + file1.write("%% Signal yield table\n") + file1.write("\\begin{tabular}{l|r@{~}r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & (cum.\\,eff\\%) \\\\ \\hline\n") + # We include production and visible as additional rows for clarity + prod_str = format_sci(prod_yield) + vis_str = format_sci(vis_yield) + # Visible fraction percent = rho_fraction*100 + vis_frac_pct = rho_fraction * 100.0 + file1.write(f"Production (total) & ${prod_str}$ & (100\\%) \\\\\n") + file1.write(f"Visible decays & ${vis_str}$ & ({vis_frac_pct:.2f}\\%) \\\\\n") + # Acceptance stage is already in sig_cutflow[0] + for stage, S_yield, eff in sig_cutflow: + yield_str = format_sci(S_yield) + file1.write(f"{stage} & ${yield_str}$ & ({eff:.2f}\\%) \\\\\n") + file1.write("\\end{tabular}\n") + # Significance per step table + file1.write("\hline") + file1.write("%% Significance per selection stage\n") + file1.write("\\begin{tabular}{l|r r r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & $B_{\\text{yield}}$ & $Z_{\\text{Bi}}$ \\\\ \\hline\n") + for stage, S_yield, B_yield, Zbi_val in sig_table: + S_str = format_sci(S_yield) + B_str = format_sci(B_yield) + Z_str = f"{Zbi_val:.2f}" if math.isfinite(Zbi_val) else "$\\infty$" + file1.write(f"{stage} & ${S_str}$ & ${B_str}$ & {Z_str} \\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\n") # blank line after tables + file1.write("\\end{document}\n") + file1.close() + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + # Choose features to plot: use common features present in both samples + features_to_plot = ["psum","vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + #"vertex.pos_.fZ", "ele.track_.z0_", "pos.track_.z0_", "psum", "vtx_proj_sig"] + # Ensure those exist in events and arrays + print(features_to_plot) + features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + print(features_to_plot) + # Plot 1D histograms overlay + for feat in features_to_plot: + s_data = np.asarray(events[feat])[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + + if(normalized): + S_final = 1.0 + B_final = 1.0 + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size) + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + plt.figure(figsize=(7.5, 4.5)) + bins = args.bins + plt.hist(s_data, bins=bins, range=hist_range, weights=s_w, histtype="step", linewidth=1.8, label="Signal") + if b_data.size > 0: + plt.hist(b_data, bins=bins, range=hist_range, weights=b_w, histtype="step", linewidth=1.8, label="Background") + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + # Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + if args.debug: + print(f"[debug] Saved 1D plot: {outfile}") + # Plot 2D histograms for each pair of features (for signal and background separately) + feat_pairs = [(features_to_plot[i], features_to_plot[j]) for i in range(len(features_to_plot)) for j in range(i+1, len(features_to_plot))] + # Optionally limit number of pairs + max_pairs = getattr(args, "max_pairs", None) + if max_pairs: + feat_pairs = feat_pairs[:int(max_pairs)] + for fx, fy in feat_pairs: + # Prepare data + s_x = np.asarray(events[fx])[sig_final_mask] + s_y = np.asarray(events[fy])[sig_final_mask] + if bg_final_mask is not None and fx in arrays.keys() and fy in arrays.keys(): + b_x = ak.to_numpy(arrays[fx])[bg_final_mask] + b_y = ak.to_numpy(arrays[fy])[bg_final_mask] + else: + b_x = np.array([]); b_y = np.array([]) + # Plot for signal + if s_x.size >= 2 and s_y.size >= 2: + plt.figure(figsize=(6.5, 5.5)) + H, xedges, yedges = np.histogram2d(s_x, s_y, bins=args.bins2d) + plt.pcolormesh(xedges, yedges, H.T, cmap='Blues') + plt.colorbar(label="Signal count") + plt.xlabel(fx); plt.ylabel(fy) + plt.title(f"Signal {fx} vs {fy} (after cuts)") + outfile = os.path.join(args.outdir, f"plot2D_signal_{fx.replace('.', '_')}_vs_{fy.replace('.', '_')}.png") + plt.tight_layout(); plt.savefig(outfile, dpi=150); plt.close() + # Plot for background + if b_x.size >= 2 and b_y.size >= 2: + plt.figure(figsize=(6.5, 5.5)) + H, xedges, yedges = np.histogram2d(b_x, b_y, bins=args.bins2d) + plt.pcolormesh(xedges, yedges, H.T, cmap='Oranges') + plt.colorbar(label="Background count") + plt.xlabel(fx); plt.ylabel(fy) + plt.title(f"Background {fx} vs {fy} (after cuts)") + outfile = os.path.join(args.outdir, f"plot2D_background_{fx.replace('.', '_')}_vs_{fy.replace('.', '_')}.png") + plt.tight_layout(); plt.savefig(outfile, dpi=150); plt.close() + + + plotRates(args.outdir) + if args.debug: + print("[done] All tables generated and plots saved.") + compile_latex(args.outdir+"/latek.tex") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI.py new file mode 100644 index 00000000..f5e0f29c --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI.py @@ -0,0 +1,751 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + #ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + #ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + #mass_mev = args.mass + #epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + #"vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_", + features_to_plot = ["psum","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + features_to_plot += ["vertex.pos_.fX", "vertex.pos_.fY", "vertex.pos_.fZ"] + + param_list = [[90,1e-2],[90,8e-3],[90,5e-3],[90,2e-3]] + plotarray_data = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_bins = [[0 for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_weights = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_ranges = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + + + ##WHAT I WILL WANT TO DO IS LOOP OVER THE MASS AND EPSILON LIST (AND ONE FOR BACKGROUND], FOR EACH, STORE SDATA INTO THE RELEVANT FEATURE POSITION + ##SO PLOTARRAY SHOULD BE DIMENSION [len(param_list)+1]X[feature_list] + count=1 + for mass_mev,epsilon in param_list: + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + #center_geV = 1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > -1)#(invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + ####REWEIGHTING SHIT HAPPENS HERE + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted cut length: "+str(rho_length)) + z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print(z_temp) + z_temp=z_temp*(z_temp>=0.0) + print(z_temp) + #print(gamma_temp) + #print(z_temp/(gamma_temp*rho_length)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + p_accept_temp/= max(p_accept_temp) + + rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print(u) + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + #events = events[mask] + #print("GOT HERE") + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + + print(Val2) + projval=10.0*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + + + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val,Val2) + zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #print("unboosted decay length: "+str(rho_length)) + #print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + if(z_cent<0): + z_cent=0 + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + #features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + + # Plot 1D histograms overlay + count2 = 0 + for feat in features_to_plot: + + + '''s_data = np.asarray(events[feat])[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background''' + + if feat in events: + #print("Features: ") + #print(np.asarray(events[feat])) + s_data = np.asarray(events[feat])[sig_final_mask] + elif feat.startswith("vertex.pos_.") and ("vertex.pos_" in events): + _fld = feat.split(".")[-1] + s_data = np.asarray(events["vertex.pos_"][_fld])[sig_final_mask] + else: + continue # feature not present in signal, skip + + # Background feature fetch (supports either split keys or record "vertex.pos_") + + if bg_final_mask is not None and (feat in arrays.keys()): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + elif bg_final_mask is not None and feat.startswith("vertex.pos_.") and ("vertex.pos_" in arrays.keys()): + _fld = feat.split(".")[-1] + b_data = ak.to_numpy(arrays["vertex.pos_"][_fld])[bg_final_mask] + else: + b_data = np.array([]) + + + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + + if(normalized): + S_final = 1.0 + B_final = 1.0 + print("Data then Signal shape") + print(b_data.shape) + print(s_data.shape) + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size) + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + bins = args.bins + if((count==1)): + plotarray_data[0][count2].extend(b_data) + plotarray_bins[0][count2]=bins + print("I am printing background shape") + print(b_w) + plotarray_weights[0][count2].extend(b_w) + plotarray_ranges[0][count2].extend(hist_range) + plotarray_data[count][count2].extend(s_data) + plotarray_bins[count][count2]=bins + plotarray_weights[count][count2].extend(s_w) + plotarray_ranges[count][count2].extend(hist_range) + count2+=1 + count+=1 + #NOW I CAN PLOT WITH THE ARRAYS + for I in range(len(features_to_plot)): + feat=features_to_plot[I] + plt.hist(plotarray_data[0][I],bins=plotarray_bins[0][I],range=plotarray_ranges[0][I], weights=plotarray_weights[0][I],histtype="step",linewidth=1.8,label="Background") + for J in range(len(param_list)): + plt.hist(plotarray_data[1+J][I],bins=plotarray_bins[1+J][I],range=plotarray_ranges[1+J][I], weights=plotarray_weights[1+J][I],histtype="step",linewidth=1.8,label="Mass: "+str(param_list[J][0])+", eps: "+str(param_list[J][1])) + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + ## Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI_just_z_fit.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI_just_z_fit.py new file mode 100644 index 00000000..ac84d5de --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_3_MULTI_just_z_fit.py @@ -0,0 +1,805 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + #ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + #ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + #mass_mev = args.mass + #epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + #"vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_", + features_to_plot = ["psum","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + features_to_plot += ["vertex.pos_.fX", "vertex.pos_.fY", "vertex.pos_.fZ"] + + param_list = [[90,1e-2],[90,9e-3],[90,8e-3],[90,7e-3],[90,6e-3],[90,5e-3],[90,4e-3],[90,3e-3],[90,2e-3],[90,1e-3],[90,9e-4],[90,8e-4],[90,7e-4]] + plotarray_data = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_bins = [[0 for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_weights = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_ranges = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + + + ##WHAT I WILL WANT TO DO IS LOOP OVER THE MASS AND EPSILON LIST (AND ONE FOR BACKGROUND], FOR EACH, STORE SDATA INTO THE RELEVANT FEATURE POSITION + ##SO PLOTARRAY SHOULD BE DIMENSION [len(param_list)+1]X[feature_list] + count=1 + for mass_mev,epsilon in param_list: + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + #center_geV = 1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > -1)#(invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + ####REWEIGHTING SHIT HAPPENS HERE + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted cut length: "+str(rho_length)) + z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print(z_temp) + z_temp=z_temp*(z_temp>=0.0) + print(z_temp) + #print(gamma_temp) + #print(z_temp/(gamma_temp*rho_length)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + p_accept_temp/= max(p_accept_temp) + + rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print(u) + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + #events = events[mask] + #print("GOT HERE") + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + + print(Val2) + projval=10.0*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + + + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val,Val2) + zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #print("unboosted decay length: "+str(rho_length)) + #print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + if(z_cent<0): + z_cent=0 + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + + #features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + + # Plot 1D histograms overlay + count2 = 0 + for feat in features_to_plot: + + + '''s_data = np.asarray(events[feat])[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background''' + + if feat in events: + s_data = np.asarray(events[feat])[sig_final_mask] + elif feat.startswith("vertex.pos_.") and ("vertex.pos_" in events): + _fld = feat.split(".")[-1] + s_data = np.asarray(events["vertex.pos_"][_fld])[sig_final_mask] + else: + continue # feature not present in signal, skip + + # Background feature fetch (supports either split keys or record "vertex.pos_") + if bg_final_mask is not None and (feat in arrays.keys()): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + elif bg_final_mask is not None and feat.startswith("vertex.pos_.") and ("vertex.pos_" in arrays.keys()): + _fld = feat.split(".")[-1] + b_data = ak.to_numpy(arrays["vertex.pos_"][_fld])[bg_final_mask] + else: + b_data = np.array([]) + + + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + + if(normalized): + S_final = 1.0 + B_final = 1.0 + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size) + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + bins = args.bins + if((count==1)): + plotarray_data[0][count2].extend(b_data) + plotarray_bins[0][count2]=bins + plotarray_weights[0][count2].extend(b_w) + plotarray_ranges[0][count2].extend(hist_range) + plotarray_data[count][count2].extend(s_data) + plotarray_bins[count][count2]=bins + plotarray_weights[count][count2].extend(s_w) + plotarray_ranges[count][count2].extend(hist_range) + count2+=1 + count+=1 + #NOW I CAN PLOT WITH THE ARRAYS + I=len(features_to_plot)-1 + feat=features_to_plot[I] + xmax_list = [6.0,6.0,6.0,6.0,6.0,6.0,6.0,20.0,20.0,25.0,25.0,25.0,25.0] + slopelist=[] + rho_list=[] + phi_list=[] + phi_frac_list=[] + + plt.hist(plotarray_data[0][I],bins=plotarray_bins[0][I],range=plotarray_ranges[0][I], weights=plotarray_weights[0][I],histtype="step",linewidth=1.8,label="Background") + for J in range(len(param_list)): + counts, edges, _ = plt.hist(plotarray_data[1+J][I],bins=plotarray_bins[1+J][I],range=plotarray_ranges[1+J][I], weights=plotarray_weights[1+J][I],histtype="step",linewidth=1.8,label="Mass: "+str(param_list[J][0])+", eps: "+str(param_list[J][1])) + # --- 2) NOW do the log-linear fit + overlay --- + centers = 0.5 * (edges[:-1] + edges[1:]) + y = counts.astype(float) + + xmax = float(xmax_list[J]) # array aligned with param_list + mask = (centers >= 0.0) & (centers <= xmax) & (y > 0.0) + + if np.count_nonzero(mask) < 2: + continue + + xfit = centers[mask] + yfit = y[mask] + + logy = np.log(np.maximum(yfit, 1e-30)) + slope, intercept = np.polyfit(xfit, logy, 1) + slopelist.append(slope) + xline = np.linspace(0.0, xmax, 200) + yline = np.exp(intercept + slope * xline) + + plt.plot(xline, yline, linestyle="--", linewidth=1.5) + + mass_mev = param_list[J][0] + epsilon = param_list[J][1] + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + psum = 2.0 + gamma = 1000*psum/m_V_D + rho_list.append(rho_length*gamma) + phi_list.append(phi_length*gamma) + phi_frac_list.append(phi_fraction) + + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + ## Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + plt.scatter([a[1] for a in param_list],slopelist,s=10,c="r",marker="o") + plt.scatter([a[1] for a in param_list],[-1/a for a in rho_list],s=10,c="b",marker="o") + plt.scatter([a[1] for a in param_list],[-1/a for a in phi_list],s=10,c="y",marker="o") + plt.title("Slopes as a function of Epsilon") + plt.ylabel("Slope") + plt.xlabel("Epsilon") + plt.savefig(os.path.join(args.outdir,"slopesplot.png")) + plt.close() + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4.py new file mode 100644 index 00000000..c8593c13 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4.py @@ -0,0 +1,852 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + mass_mev = args.mass + epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + Mkey = base._mass_key(1.8*mass_mev/3.0) + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = float(Mkey)/1000.0 + #1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + ####REWEIGHTING SHIT HAPPENS HERE + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + z_temp = np.asarray(events["true_vd.vtx_z_"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print(z_temp) + z_temp=z_temp*(z_temp>=0.0) + print(z_temp) + #print(gamma_temp) + #print(z_temp/(gamma_temp*rho_length)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + p_accept_temp/= max(p_accept_temp) + + '''rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print(u) + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + #events = events[mask] + #print("GOT HERE")''' + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + + print(Val2) + projval=10.0*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + + + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val,Val2) + #zvals = events["vertex.pos_.fZ"][mask] + zvals = events["true_vd.vtx_z_"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted decay length: "+str(rho_length)) + print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + if(z_cent<0): + z_cent=0 + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + #for i in range(len(s_x)): + # #print("I get to "+str(i)) + # z = s_vertex_z[i] + # #print(z) + # if not (den_edges[0] <= z < den_edges[-1]): + # continue + # mk = base._mass_key(m_V_D) + # mask = base.tight_selection(events,Val) + # zvals = events["vertex.pos_.fZ"][mask] + # num_vals, _ = np.histogram(zvals, bins=den_edges) + # I = np.searchsorted(den_edges, z, side="right") - 1 + # #counts[I]+=1.0 + # Ngen = float(den_vals[I]) + # Nacc = float(num_vals[I]) + # #Nacc = Ngen + # rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + # phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + # rho_length=(1000*HBAR_C*10.0)/rho_width + # phi_length=(1000*HBAR_C*10.0)/phi_width + # valuesr[I]=valuesr[I]+(Nacc/Ngen)*np.exp(-s_x[i]/rho_length)/(rho_length*s_gamma[i]) + # valuesp[I]=valuesp[I]+(Nacc/Ngen)*np.exp(-s_x[i]/phi_length)/(phi_length*s_gamma[i]) + # #print(counts) + # #print(valuesr) + # #print(rho_width) + # #print(phi_width) + # #print(s_x[i]) + # #print(s_gamma[i]) + # #print(rho_length) + # #print(phi_length) + # #print("Done here: "+str(np.exp(-s_x[i]/rho_length)/(rho_length*s_gamma[i]))) + # #print(pphi) + #prho=sum([valuesr[I]/(counts[I]+1.0*(counts[I]==0)) for I in range(len(den_vals))]) + #pphi=sum([valuesp[I]/(counts[I]+1.0*(counts[I]==0)) for I in range(len(den_vals))]) + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + file1 = open(args.outdir+"/latek.tex",'w') + # ------------------------------ + # Output LaTeX-formatted tables + # ------------------------------ + # Background cutflow table + + file1.write("\\documentclass{article}\n") + file1.write("\\usepackage{graphicx} % Required for inserting images\n") + file1.write("\\usepackage{amsmath}\n") + #file1.write("\\pagecolor{white}\n") + file1.write("\\usepackage{xcolor}\n") + file1.write("\\pagecolor[rgb]{1,1,1}\n") + file1.write("\\title{TablesForBackgroundRates}\n") + file1.write("\\author{rodwyer100 }\n") + file1.write("\\date{November 2025}\n") + file1.write("\\begin{document}\n") + file1.write("\\maketitle\n") + file1.write("\\begin{tabular}{l|r}\n") + file1.write("Original Core & "+str(core)+"\\\\\n") + file1.write("Total Num Events & 3e9\\\\\n") + file1.write("Area Under Curve At Point & "+str(m_fraction * 1.0)+"\\\\\n") + file1.write("Events At Mass & "+str(format_sci(N_b_massbin))+"\\\\\n") + file1.write("A Prime Yield &"+str(format_sci(prod_yield))+"\\\\\n") + file1.write("Rho BR and Phi BR &"+str(np.round(rho_fraction,4))+","+str(np.round(phi_fraction,4))+"\\\\\n") + file1.write("Visible Yield &"+ str(format_sci(vis_yield))+"\\\\\n") + file1.write("Rho Acc and Phi Acc &"+str(np.round(prho,6))+","+str(np.round(pphi,6))+"\\\\\n") + file1.write("Rho Yield and Phi Yield &"+str(format_sci(prod_yield*prho*rho_fraction))+","+str(format_sci(prod_yield*pphi*phi_fraction))+"\\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\\textbf{Cut 0}\n") + if bg is not None: + file1.write("\n%% Background cutflow table\n") + file1.write("\\begin{tabular}{l|rr|rr|r}\n") + file1.write("\\textbf{Cut stage} & $N_{\\text{in}}$ & $N_{\\text{out}}$ & In~(\\% total) & Out~(\\% total) & $B_{\\text{yield}}$ \\\\ \\hline\n") + for stage, n_in, n_out, pct_in, pct_out, B_yield, _ in bg_cutflow: + # Format counts as integers and yields in scientific notation + count_in_str = f"{int(n_in)}" + count_out_str = f"{int(n_out)}" + pct_in_str = f"{pct_in:.2f}\\%" + pct_out_str = f"{pct_out:.2f}\\%" + yield_str = format_sci(B_yield) if B_yield is not None else "--" + file1.write(f"{stage} & {count_in_str} & {count_out_str} & {pct_in_str} & {pct_out_str} & ${yield_str}$ \\\\\n") + file1.write("\\end{tabular}\n") + # Signal yield cutflow table + file1.write("%% Signal yield table\n") + file1.write("\\begin{tabular}{l|r@{~}r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & (cum.\\,eff\\%) \\\\ \\hline\n") + # We include production and visible as additional rows for clarity + prod_str = format_sci(prod_yield) + vis_str = format_sci(vis_yield) + # Visible fraction percent = rho_fraction*100 + vis_frac_pct = rho_fraction * 100.0 + file1.write(f"Production (total) & ${prod_str}$ & (100\\%) \\\\\n") + file1.write(f"Visible decays & ${vis_str}$ & ({vis_frac_pct:.2f}\\%) \\\\\n") + # Acceptance stage is already in sig_cutflow[0] + for stage, S_yield, eff in sig_cutflow: + yield_str = format_sci(S_yield) + file1.write(f"{stage} & ${yield_str}$ & ({eff:.2f}\\%) \\\\\n") + file1.write("\\end{tabular}\n") + # Significance per step table + file1.write("\hline") + file1.write("%% Significance per selection stage\n") + file1.write("\\begin{tabular}{l|r r r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & $B_{\\text{yield}}$ & $Z_{\\text{Bi}}$ \\\\ \\hline\n") + for stage, S_yield, B_yield, Zbi_val in sig_table: + S_str = format_sci(S_yield) + B_str = format_sci(B_yield) + Z_str = f"{Zbi_val:.2f}" if math.isfinite(Zbi_val) else "$\\infty$" + file1.write(f"{stage} & ${S_str}$ & ${B_str}$ & {Z_str} \\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\n") # blank line after tables + file1.write("\\end{document}\n") + file1.close() + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + # Choose features to plot: use common features present in both samples + features_to_plot = ["psum","vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + #"vertex.pos_.fZ", "ele.track_.z0_", "pos.track_.z0_", "psum", "vtx_proj_sig"] + # Ensure those exist in events and arrays + print(features_to_plot) + features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + print(features_to_plot) + # Plot 1D histograms overlay + for feat in features_to_plot: + s_data = np.asarray(events[feat])[sig_final_mask] + reweight = p_accept_temp[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + if(normalized): + S_final = 1.0 + B_final = 1.0 + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size)*reweight + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + plt.figure(figsize=(7.5, 4.5)) + bins = args.bins + plt.hist(s_data, bins=bins, range=hist_range, weights=s_w, histtype="step", linewidth=1.8, label="Signal") + if b_data.size > 0: + plt.hist(b_data, bins=bins, range=hist_range, weights=b_w, histtype="step", linewidth=1.8, label="Background") + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + # Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + if args.debug: + print(f"[debug] Saved 1D plot: {outfile}") + # Plot 2D histograms for each pair of features (for signal and background separately) + feat_pairs = [(features_to_plot[i], features_to_plot[j]) for i in range(len(features_to_plot)) for j in range(i+1, len(features_to_plot))] + # Optionally limit number of pairs + max_pairs = getattr(args, "max_pairs", None) + if max_pairs: + feat_pairs = feat_pairs[:int(max_pairs)] + for fx, fy in feat_pairs: + # Prepare data + s_x = np.asarray(events[fx])[sig_final_mask] + s_y = np.asarray(events[fy])[sig_final_mask] + if bg_final_mask is not None and fx in arrays.keys() and fy in arrays.keys(): + b_x = ak.to_numpy(arrays[fx])[bg_final_mask] + b_y = ak.to_numpy(arrays[fy])[bg_final_mask] + else: + b_x = np.array([]); b_y = np.array([]) + # Plot for signal + if s_x.size >= 2 and s_y.size >= 2: + plt.figure(figsize=(6.5, 5.5)) + H, xedges, yedges = np.histogram2d(s_x, s_y, bins=args.bins2d) + plt.pcolormesh(xedges, yedges, H.T, cmap='Blues') + plt.colorbar(label="Signal count") + plt.xlabel(fx); plt.ylabel(fy) + plt.title(f"Signal {fx} vs {fy} (after cuts)") + outfile = os.path.join(args.outdir, f"plot2D_signal_{fx.replace('.', '_')}_vs_{fy.replace('.', '_')}.png") + plt.tight_layout(); plt.savefig(outfile, dpi=150); plt.close() + # Plot for background + if b_x.size >= 2 and b_y.size >= 2: + plt.figure(figsize=(6.5, 5.5)) + H, xedges, yedges = np.histogram2d(b_x, b_y, bins=args.bins2d) + plt.pcolormesh(xedges, yedges, H.T, cmap='Oranges') + plt.colorbar(label="Background count") + plt.xlabel(fx); plt.ylabel(fy) + plt.title(f"Background {fx} vs {fy} (after cuts)") + outfile = os.path.join(args.outdir, f"plot2D_background_{fx.replace('.', '_')}_vs_{fy.replace('.', '_')}.png") + plt.tight_layout(); plt.savefig(outfile, dpi=150); plt.close() + + + plotRates(args.outdir) + if args.debug: + print("[done] All tables generated and plots saved.") + compile_latex(args.outdir+"/latek.tex") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI.py new file mode 100644 index 00000000..0f7cd706 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI.py @@ -0,0 +1,749 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + #ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + #ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + #mass_mev = args.mass + #epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + #"vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_", + features_to_plot = ["psum","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + features_to_plot += ["vertex.pos_.fX", "vertex.pos_.fY", "vertex.pos_.fZ"] + + param_list = [[120,1e-3],[120,8e-4],[120,5e-4],[120,2e-4]] + plotarray_data = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_bins = [[0 for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_weights = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_ranges = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + + + ##WHAT I WILL WANT TO DO IS LOOP OVER THE MASS AND EPSILON LIST (AND ONE FOR BACKGROUND], FOR EACH, STORE SDATA INTO THE RELEVANT FEATURE POSITION + ##SO PLOTARRAY SHOULD BE DIMENSION [len(param_list)+1]X[feature_list] + count=1 + for mass_mev,epsilon in param_list: + Mkey = base._mass_key(1.8*mass_mev/3.0) + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = float(Mkey)/1000.0 #1.8*Mkey / (1000.0 * 3.0) + print("Mass of Vd: "+str(mass_mev)) + print("Location of Center: "+str(center_geV)) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + #(invM > -1)#(invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + ####REWEIGHTING SHIT HAPPENS HERE + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted cut length: "+str(rho_length)) + #z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + z_temp = np.asarray(events["true_vd.vtx_z_"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print(z_temp) + z_temp=z_temp*(z_temp>=0.0) + print(z_temp) + #print(gamma_temp) + #print(z_temp/(gamma_temp*rho_length)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + p_accept_temp/= max(p_accept_temp) + + '''rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print(u) + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + #events = events[mask] + #print("GOT HERE")''' + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + + print(Val2) + projval=10.0*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + + + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val,Val2) + zvals = events["vertex.pos_.fZ"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #print("unboosted decay length: "+str(rho_length)) + #print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + if(z_cent<0): + z_cent=0 + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + + #features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + + # Plot 1D histograms overlay + count2 = 0 + for feat in features_to_plot: + + + '''s_data = np.asarray(events[feat])[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background''' + + if feat in events: + s_data = np.asarray(events[feat])[sig_final_mask] + reweight = p_accept_temp[sig_final_mask] + elif feat.startswith("vertex.pos_.") and ("vertex.pos_" in events): + _fld = feat.split(".")[-1] + s_data = np.asarray(events["vertex.pos_"][_fld])[sig_final_mask] + else: + continue # feature not present in signal, skip + + # Background feature fetch (supports either split keys or record "vertex.pos_") + if bg_final_mask is not None and (feat in arrays.keys()): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + elif bg_final_mask is not None and feat.startswith("vertex.pos_.") and ("vertex.pos_" in arrays.keys()): + _fld = feat.split(".")[-1] + b_data = ak.to_numpy(arrays["vertex.pos_"][_fld])[bg_final_mask] + else: + b_data = np.array([]) + + + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + + if(normalized): + S_final = 1.0 + B_final = 1.0 + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size)*reweight + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + bins = args.bins + if((count==1)): + plotarray_data[0][count2].extend(b_data) + plotarray_bins[0][count2]=bins + plotarray_weights[0][count2].extend(b_w) + plotarray_ranges[0][count2].extend(hist_range) + plotarray_data[count][count2].extend(s_data) + plotarray_bins[count][count2]=bins + plotarray_weights[count][count2].extend(s_w) + plotarray_ranges[count][count2].extend(hist_range) + count2+=1 + count+=1 + #NOW I CAN PLOT WITH THE ARRAYS + for I in range(len(features_to_plot)): + feat=features_to_plot[I] + plt.hist(plotarray_data[0][I],bins=plotarray_bins[0][I],range=plotarray_ranges[0][I], weights=plotarray_weights[0][I],histtype="step",linewidth=1.8,label="Background") + for J in range(len(param_list)): + plt.hist(plotarray_data[1+J][I],bins=plotarray_bins[1+J][I],range=plotarray_ranges[1+J][I], weights=plotarray_weights[1+J][I],histtype="step",linewidth=1.8,label="Mass: "+str(param_list[J][0])+", eps: "+str(param_list[J][1])) + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + ## Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI_just_z_fit.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI_just_z_fit.py new file mode 100644 index 00000000..a093f610 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_4_MULTI_just_z_fit.py @@ -0,0 +1,810 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + #ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + #ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + #mass_mev = args.mass + #epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + #"vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_", + features_to_plot = ["psum","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + features_to_plot += ["vertex.pos_.fX", "vertex.pos_.fY", "vertex.pos_.fZ"] + + param_list = [[90,1e-2],[90,9e-3],[90,8e-3],[90,7e-3],[90,6e-3],[90,5e-3],[90,4e-3],[90,3e-3],[90,2e-3],[90,1e-3],[90,9e-4],[90,8e-4],[90,7e-4]] + plotarray_data = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_bins = [[0 for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_weights = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + plotarray_ranges = [[[] for j in range(len(features_to_plot))] for i in range(len(param_list)+1)] + + + ##WHAT I WILL WANT TO DO IS LOOP OVER THE MASS AND EPSILON LIST (AND ONE FOR BACKGROUND], FOR EACH, STORE SDATA INTO THE RELEVANT FEATURE POSITION + ##SO PLOTARRAY SHOULD BE DIMENSION [len(param_list)+1]X[feature_list] + count=1 + for mass_mev,epsilon in param_list: + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + Mkey = base._mass_key(1.8*mass_mev/3.0) + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + proj_mask = (proj_sig < 1.6) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = float(Mkey)/1000.0 + #1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + + ##I THINK THIS NEEDS TO BE DIVIDED BY 3.0 and times 1.8 + + try: + events = base._events_cache(mkey) + ####REWEIGHTING SHIT HAPPENS HERE + #THIS PORTION, WHILE LENGTHY, DOES REWEIGHTING BRIEFLY TO FIX CRAP, SHOULD MAKE EVERYTHING WORK RIGHT AWAY DOWNSTREAM + #print("GOT HERE") + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + print("unboosted cut length: "+str(rho_length)) + #z_temp = np.asarray(events["vertex.pos_.fZ"], dtype=np.float64) + z_temp = np.asarray(events["true_vd.vtx_z_"], dtype=np.float64) + psum_temp = np.asarray(events["psum"], dtype=np.float64) + gamma_temp = 1000*psum_temp/m_V_D + print(z_temp) + z_temp=z_temp*(z_temp>=0.0) + print(z_temp) + #print(gamma_temp) + #print(z_temp/(gamma_temp*rho_length)) + p_accept_temp = rho_fraction*np.exp(-z_temp/(gamma_temp*rho_length))/(rho_length*gamma_temp) + p_accept_temp += phi_fraction*np.exp(-z_temp/(gamma_temp*phi_length))/(phi_length*gamma_temp) + p_accept_temp/= max(p_accept_temp) + + '''rng = np.random.default_rng(123) # seed optional, helps reproducibility + u = rng.random(len(z_temp)) + #print("GOT HERE") + print(u) + print(p_accept_temp) + mask = (u < p_accept_temp) + print(mask) + mask = np.asarray(mask, dtype=bool) + events = {k: np.asarray(v)[mask] for k, v in events.items()} + #events = events[mask] + #print("GOT HERE")''' + + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + # Extract needed branches from events (assuming events behaves like a dict or similar mapping) + # We'll gather arrays for psum, z0, proj_sig, etc. + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + # Handle L1L1 for signal (use hasL0L1 if present, else derive from hit_layers) + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + # Derive from hit_layers if possible + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + s_z_thr = 0.5 * (float(Val) / 25.0) + s_z0_mask = np.logical_and((ele_z0 > s_z_thr) | (ele_z0 < -s_z_thr), + (pos_z0 > s_z_thr) | (pos_z0 < -s_z_thr)) + + print(Val2) + projval=10.0*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + + + total_sig_events = len(s_psum) + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, yield, cum_eff%) + # Compute theoretical yields at key stages: + # 1. Production yield (no decays yet) + # Use formula from base: core = scale_const * ratio(mA) * mA * epsilon^2:contentReference[oaicite:17]{index=17} + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + + N_B_TOTAL = 3.0e9 + + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8 NVMND I DONT THINK I NEED TO DO THIS + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_b_massbin = N_B_TOTAL * m_fraction * 1.0 # expected events in ±0.002 GeV window if no selection + + + prod_yield = N_b_massbin * core # number of A' produced (per baseline N_B events) + # 2. Visible yield (multiply by rho fraction last element):contentReference[oaicite:18]{index=18} + # Compute rho (and phi) branching fractions using hidden sector model + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + ##ALL OF THIS HAS BEEN VALIDATED SO FAR + + # Decay length in mm (lab): (HBAR_C/total_width in cm) * 10 * beta_gamma + #L_total_cm = HBAR_C / total_width + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho=0 + pphi=0 + + #print("DO I GET HERE") + den_edges, den_vals = base.read_den_hist(m_V_D) + #counts=[1.0 for i in range(len(den_vals))] + #valuesp=[0.0 for i in range(len(den_vals))] + #valuesr=[0.0 for i in range(len(den_vals))] + mk = base._mass_key(m_V_D) + mask = base.tight_selection(events,Val,Val2) + #zvals = events["vertex.pos_.fZ"][mask] + zvals = events["true_vd.vtx_z_"][mask] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + #print("\n z value: "+str(z_cent)) + #print("Length of psum vals: "+str(len(psum_vals))) + for J in range(len(psum_vals)): + psum = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum/m_V_D + #print("gamma value: "+str(gamma)) + #print("gamma prob: "+str(psum_vals[J])) + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #print("unboosted decay length: "+str(rho_length)) + #print("unboosted decay length: "+str(phi_length)) + #print("boosted decay length: "+str(rho_length*gamma)) + #print("z prob: "+str(np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("New prho: "+str(prho)) + if(z_cent<0): + z_cent=0 + if Ngen==0.0: + Ngen=1.0 + Nacc=0.0 + prho+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi+=psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("I DO GET HERE") + + ####ALL AFTER HAS BEEN CHECKED + + # Now selection stages: + # Initial (after acceptance, before any tight cuts) + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline for selection efficiency + # Sequentially apply cuts and compute yield + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # z0 cut + current_mask &= s_z0_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + + # ------------------------------ + # Significance calculation + # ------------------------------ + # We have expected B_yield from bg_cutflow and S_yield from sig_cutflow for matching stages + # (We consider "After acceptance" as our baseline stage 0 for significance, corresponding to background "No cuts" stage) + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None: + # Map background stages to yields for signal stages: + # Use "No cuts" background for "After acceptance" signal (assuming acceptance is effectively no tight cuts for background as well) + # and subsequent cuts correspond in order. + + + #ROW FIVE FOR BACKGROUND BASED ON IN, ROW SIX FOR BACKGROUND BASED ON OUT + bg_yields = { row[0]: row[6] for row in (bg_cutflow or []) } + for stage, S_yield, _eff in sig_cutflow: + # Find corresponding background stage (naming must match the way we labeled) + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + # Could be with or without +proj, handle above first + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + # If no background info, just output signal yields and placeholder zeros for background + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + # If background events loaded, apply final mask to get distributions; else only signal. + # We already have `current_mask` from signal final cut, and for background from above loop (after all cuts). + # For clarity, recompute final masks: + sig_final_mask = np.ones(total_sig_events, dtype=bool) + sig_final_mask &= s_psum_mask & s_L1L1_mask & s_proj_mask & s_z0_mask + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + + #features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + + # Plot 1D histograms overlay + count2 = 0 + for feat in features_to_plot: + + + '''s_data = np.asarray(events[feat])[sig_final_mask] + # Background: if available, use bg_final_mask (already includes mass window) + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + else: + b_data = np.array([]) # no background''' + + if feat in events: + s_data = np.asarray(events[feat])[sig_final_mask] + reweight = p_accept_temp[sig_final_mask] + elif feat.startswith("vertex.pos_.") and ("vertex.pos_" in events): + _fld = feat.split(".")[-1] + s_data = np.asarray(events["vertex.pos_"][_fld])[sig_final_mask] + else: + continue # feature not present in signal, skip + + # Background feature fetch (supports either split keys or record "vertex.pos_") + if bg_final_mask is not None and (feat in arrays.keys()): + b_data = ak.to_numpy(arrays[feat])[bg_final_mask] + elif bg_final_mask is not None and feat.startswith("vertex.pos_.") and ("vertex.pos_" in arrays.keys()): + _fld = feat.split(".")[-1] + b_data = ak.to_numpy(arrays["vertex.pos_"][_fld])[bg_final_mask] + else: + b_data = np.array([]) + + + # Compute S and B yields for annotation (use final yields from above) + S_final = yield_final if 'yield_final' in locals() else (len(s_data)) + B_final = bg_cutflow[-1][5] if bg is not None else 0.0 + Zbi_final = zbi_significance(S_final, B_final) + # Determine histogram range (0.5% to 99.5% quantile of combined data) + combined = np.concatenate([s_data, b_data]) if b_data.size > 0 else s_data + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + s_w = None; b_w = None + + if(normalized): + S_final = 1.0 + B_final = 1.0 + if s_data.size > 0: + s_w = np.full(s_data.shape, S_final / s_data.size)*reweight + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + bins = args.bins + if((count==1)): + plotarray_data[0][count2].extend(b_data) + plotarray_bins[0][count2]=bins + plotarray_weights[0][count2].extend(b_w) + plotarray_ranges[0][count2].extend(hist_range) + plotarray_data[count][count2].extend(s_data) + plotarray_bins[count][count2]=bins + plotarray_weights[count][count2].extend(s_w) + plotarray_ranges[count][count2].extend(hist_range) + count2+=1 + count+=1 + #NOW I CAN PLOT WITH THE ARRAYS + I=len(features_to_plot)-1 + feat=features_to_plot[I] + xmax_list = [6.0,6.0,6.0,6.0,6.0,6.0,6.0,20.0,20.0,25.0,25.0,25.0,25.0] + slopelist=[] + rho_list=[] + phi_list=[] + phi_frac_list=[] + + plt.hist(plotarray_data[0][I],bins=plotarray_bins[0][I],range=plotarray_ranges[0][I], weights=plotarray_weights[0][I],histtype="step",linewidth=1.8,label="Background") + for J in range(len(param_list)): + counts, edges, _ = plt.hist(plotarray_data[1+J][I],bins=plotarray_bins[1+J][I],range=plotarray_ranges[1+J][I], weights=plotarray_weights[1+J][I],histtype="step",linewidth=1.8,label="Mass: "+str(param_list[J][0])+", eps: "+str(param_list[J][1])) + # --- 2) NOW do the log-linear fit + overlay --- + centers = 0.5 * (edges[:-1] + edges[1:]) + y = counts.astype(float) + + xmax = float(xmax_list[J]) # array aligned with param_list + mask = (centers >= 0.0) & (centers <= xmax) & (y > 0.0) + + if np.count_nonzero(mask) < 2: + continue + + xfit = centers[mask] + yfit = y[mask] + + logy = np.log(np.maximum(yfit, 1e-30)) + slope, intercept = np.polyfit(xfit, logy, 1) + slopelist.append(slope) + xline = np.linspace(0.0, xmax, 200) + yline = np.exp(intercept + slope * xline) + + plt.plot(xline, yline, linestyle="--", linewidth=1.5) + + mass_mev = param_list[J][0] + epsilon = param_list[J][1] + alpha_D = 0.01 # fixed in decayLength7sel + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + # For maximum visible fraction, use f_pi_D such that invisible width is minimal (f_pi as in last rho array entry scenario) + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + #print("GOT HERE") + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + #print("GOT HERE") + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + psum = 2.0 + gamma = 1000*psum/m_V_D + rho_list.append(rho_length*gamma) + phi_list.append(phi_length*gamma) + phi_frac_list.append(phi_fraction) + + title = (f"{feat} distribution after final cuts\n" + f"S = {S_final:.2g}, B = {B_final:.2g}, Z$_{{Bi}}$ = {Zbi_final:.2f}; " + f"mass = {mass_mev:.0f} MeV, $\\epsilon$ = {epsilon}, Val = {Val}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + ## Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_{int(round(mass_mev))}MeV_eps{str(epsilon).replace('.', 'p')}_V{Val}.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + plt.scatter([a[1] for a in param_list],slopelist,s=10,c="r",marker="o") + plt.scatter([a[1] for a in param_list],[-1/a for a in rho_list],s=10,c="b",marker="o") + plt.scatter([a[1] for a in param_list],[-1/a for a in phi_list],s=10,c="y",marker="o") + plt.title("Slopes as a function of Epsilon") + plt.ylabel("Slope") + plt.xlabel("Epsilon") + plt.savefig(os.path.join(args.outdir,"slopesplot.png")) + plt.close() + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BDT.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BDT.py new file mode 100644 index 00000000..dac2c60e --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BDT.py @@ -0,0 +1,709 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path +import joblib # Added to load BDT model + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7/8sel model equations) +HBAR_C = 1.973e-14 # GeV*cm + +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) + +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) + +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + fig, ax = plt.subplots(figsize=(6,4)) + ax.plot(np.linspace(0,2,100), rho_width, label="A'->rho pi", color="red") + ax.plot(np.linspace(0,2,100), phi_width, label="A'->phi pi", color="blue") + ax.plot(np.linspace(0,2,100), invis_width, label="A'->pi pi", color="green") + ax.set_xlabel("mpi_D / f_pi_D") + ax.set_ylabel("Width [GeV]") + ax.set_title("Partial widths vs f ratio") + ax.legend() + ax.set_yscale("log") + plt.savefig(os.path.join(outdir, "rates.png")) + +## ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow and final Zbi for signal/background, and write LaTeX tables.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0/BDT threshold index).") + ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output tables/files.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms (unused here, kept for interface).") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms (unused here, kept for interface).") + ap.add_argument("--base-module", type=str, default="decayLength8sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + args = ap.parse_args() + + mass_mev = args.mass + epsilon = args.epsilon + Val = args.Val + Val2 = args.Val2 + + os.makedirs(args.outdir, exist_ok=True) + + # Load the trained BDT model (for replacing z-threshold cuts) + BDT_MODEL_PATH = "/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer/bdt_model.joblib" + try: + bdt_model = joblib.load(BDT_MODEL_PATH) + except Exception as e: + sys.stderr.write(f"[error] Could not load BDT model: {e}\n") + sys.exit(1) + + # Import the signal base module (e.g., decayLength8sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + # ------------------------------ + # Background events processing + # ------------------------------ + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + bg_cutflow = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load (extended BDT feature set) + desired_branches = [ + "vertex.invM_", "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + + # BDT scoring for background events (replaces z-threshold cut) + # Prepare feature matrix for BDT (same features used in training) + bdt_features = [] + # vertex.invM_, psum + bdt_features.append(invM.reshape(-1,1)) + bdt_features.append(psum.reshape(-1,1)) + # vertex.pos_ fields + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + for fld in vertex_pos.dtype.names: + bdt_features.append(vertex_pos[fld].reshape(-1,1)) + # electron track features + ele_arr = { + "n_hits_": ak.to_numpy(arrays["ele.track_.n_hits_"]), + "d0_": ak.to_numpy(arrays["ele.track_.d0_"]), + "phi0_": ak.to_numpy(arrays["ele.track_.phi0_"]), + "z0_": ak.to_numpy(arrays["ele.track_.z0_"]), + "tan_lambda_": ak.to_numpy(arrays["ele.track_.tan_lambda_"]), + "px_": ak.to_numpy(arrays["ele.track_.px_"]), + "py_": ak.to_numpy(arrays["ele.track_.py_"]), + "pz_": ak.to_numpy(arrays["ele.track_.pz_"]), + "chi2_": ak.to_numpy(arrays["ele.track_.chi2_"]), + "x_at_ecal_": ak.to_numpy(arrays["ele.track_.x_at_ecal_"]), + "y_at_ecal_": ak.to_numpy(arrays["ele.track_.y_at_ecal_"]), + "z_at_ecal_": ak.to_numpy(arrays["ele.track_.z_at_ecal_"]) + } + for arr in ele_arr.values(): + bdt_features.append(arr.reshape(-1,1)) + # positron track features + pos_arr = { + "n_hits_": ak.to_numpy(arrays["pos.track_.n_hits_"]), + "d0_": ak.to_numpy(arrays["pos.track_.d0_"]), + "phi0_": ak.to_numpy(arrays["pos.track_.phi0_"]), + "z0_": ak.to_numpy(arrays["pos.track_.z0_"]), + "tan_lambda_": ak.to_numpy(arrays["pos.track_.tan_lambda_"]), + "px_": ak.to_numpy(arrays["pos.track_.px_"]), + "py_": ak.to_numpy(arrays["pos.track_.py_"]), + "pz_": ak.to_numpy(arrays["pos.track_.pz_"]), + "chi2_": ak.to_numpy(arrays["pos.track_.chi2_"]), + "x_at_ecal_": ak.to_numpy(arrays["pos.track_.x_at_ecal_"]), + "y_at_ecal_": ak.to_numpy(arrays["pos.track_.y_at_ecal_"]), + "z_at_ecal_": ak.to_numpy(arrays["pos.track_.z_at_ecal_"]) + } + for arr in pos_arr.values(): + bdt_features.append(arr.reshape(-1,1)) + # vertex chi2 and invMerr + bdt_features.append(ak.to_numpy(arrays["vertex.chi2_"]).reshape(-1,1)) + bdt_features.append(ak.to_numpy(arrays["vertex.invMerr_"]).reshape(-1,1)) + # vertex projection features + bdt_features.append(proj_sig.reshape(-1,1)) + bdt_features.append(ak.to_numpy(arrays["vtx_proj_x_sig"]).reshape(-1,1)) + bdt_features.append(ak.to_numpy(arrays["vtx_proj_y_sig"]).reshape(-1,1)) + # Combine into feature matrix + X_bg = np.hstack(bdt_features) + X_bg = np.nan_to_num(X_bg, nan=0.0, posinf=0.0, neginf=0.0) + # Predict BDT score (probability of signal) for each event + bdt_scores_bg = bdt_model.predict_proba(X_bg)[:,1] + threshold_val = 0.5 * (float(Val) / 25.0) + bdt_mask = (bdt_scores_bg > threshold_val) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = 1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None, None)) # yields filled later + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None, None)) + # 3. proj_sig + current_mask &= proj_sig < (2*(float(Val2)/10)+1) + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None, None)) + # 4. BDT score (replaces z0 cut) + current_mask &= bdt_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + x_gev = mass_mev / 1000.0 + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.002 GeV window if no selection + + # Loop through cutflow to fill expected yields + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _, _ = row + if i == 0: + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + + ## ALL OF THIS HAS BEEN VALIDATED SO FAR + + # ------------------------------ + # Signal events processing + # ------------------------------ + # Load signal numerator events (no tight selection applied yet) + mkey = base._mass_key(1.8*mass_mev/3.0) + try: + events = base._events_cache(mkey) + except Exception as e: + sys.stderr.write(f"[error] Could not load signal events for mass {mass_mev}: {e}\n") + sys.exit(1) + + # Extract needed branches from events (assuming events behaves like a dict) + try: + s_vertex_z = np.asarray(events["vertex.pos_.fZ"]) + s_psum = np.asarray(events["psum"]) + ele_z0 = np.asarray(events["ele.track_.z0_"]) + pos_z0 = np.asarray(events["pos.track_.z0_"]) + s_proj = np.asarray(events["vtx_proj_sig"]) + except Exception as e: + sys.stderr.write(f"[error] Signal events missing required branches: {e}\n") + sys.exit(1) + + # Handle L1L1 for signal + if "ele.hasL0L1" in events: + s_e_hasL0L1 = np.asarray(events["ele.hasL0L1"], dtype=bool) + else: + s_e_hasL0L1 = None + if "pos.hasL0L1" in events: + s_p_hasL0L1 = np.asarray(events["pos.hasL0L1"], dtype=bool) + else: + s_p_hasL0L1 = None + if s_e_hasL0L1 is None or s_p_hasL0L1 is None: + if "ele.track_.hit_layers_" in events: + ele_layers = events["ele.track_.hit_layers_"] + pos_layers = events.get("pos.track_.hit_layers_", None) + ele_layers_ak = ak.Array(ele_layers) + hasL0_e = (ele_layers_ak == 0).any(axis=1) + hasL1_e = (ele_layers_ak == 1).any(axis=1) + s_e_hasL0L1 = np.logical_and(np.array(hasL0_e, dtype=bool), np.array(hasL1_e, dtype=bool)) + if pos_layers is not None: + pos_layers_ak = ak.Array(pos_layers) + hasL0_p = (pos_layers_ak == 0).any(axis=1) + hasL1_p = (pos_layers_ak == 1).any(axis=1) + s_p_hasL0L1 = np.logical_and(np.array(hasL0_p, dtype=bool), np.array(hasL1_p, dtype=bool)) + else: + s_p_hasL0L1 = np.ones_like(s_e_hasL0L1, dtype=bool) + else: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + s_L1L1_mask = np.logical_and(s_e_hasL0L1, s_p_hasL0L1) + + # Define selection masks for signal + s_psum_mask = (s_psum >= 1.5) & (s_psum <= 3.0) + + # BDT scoring for signal events (replaces z-threshold cut) + s_threshold_val = 0.5 * (float(Val) / 25.0) + s_bdt_features = [] + # invariant mass, psum + s_bdt_features.append(np.asarray(events["vertex.invM_"]).reshape(-1,1)) + s_bdt_features.append(np.asarray(events["psum"]).reshape(-1,1)) + # vertex.pos_ fields + s_bdt_features.append(np.asarray(events["vertex.pos_.fX"]).reshape(-1,1)) + s_bdt_features.append(np.asarray(events["vertex.pos_.fY"]).reshape(-1,1)) + s_bdt_features.append(np.asarray(events["vertex.pos_.fZ"]).reshape(-1,1)) + # electron track features + ele_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in ele_keys: + s_bdt_features.append(np.asarray(events[f"ele.track_.{key}"]).reshape(-1,1)) + # positron track features + pos_keys = ["n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", "px_", "py_", "pz_", "chi2_", "x_at_ecal_", "y_at_ecal_", "z_at_ecal_"] + for key in pos_keys: + s_bdt_features.append(np.asarray(events[f"pos.track_.{key}"]).reshape(-1,1)) + # vertex chi2 and invMerr + s_bdt_features.append(np.asarray(events["vertex.chi2_"]).reshape(-1,1)) + s_bdt_features.append(np.asarray(events["vertex.invMerr_"]).reshape(-1,1)) + # vertex projection features + s_bdt_features.append(np.asarray(events["vtx_proj_sig"]).reshape(-1,1)) + s_bdt_features.append(np.asarray(events["vtx_proj_x_sig"]).reshape(-1,1)) + s_bdt_features.append(np.asarray(events["vtx_proj_y_sig"]).reshape(-1,1)) + X_sig = np.hstack(s_bdt_features) + X_sig = np.nan_to_num(X_sig, nan=0.0, posinf=0.0, neginf=0.0) + bdt_scores_sig = bdt_model.predict_proba(X_sig)[:,1] + bdt_sig_mask = (bdt_scores_sig > s_threshold_val) + + projval = 2*(float(Val2)/10)+1 + s_proj_mask = (s_proj < projval) + total_sig_events = len(s_psum) + + # Count survivors and efficiencies stage by stage + sig_cutflow = [] # list of tuples (stage, S_yield, cum_eff%) + + # 1. Production yield (no decays yet) + scale_const = 3.0 * math.pi / (2.0 * 1.0 * (1.0 / 137.0459991)) + try: + ratio_val = base.ratio(mass_mev) + except Exception as e: + sys.stderr.write(f"[error] Failed to compute ratio(mA): {e}\n") + ratio_val = 0.0 + core = scale_const * ratio_val * mass_mev * (epsilon ** 2) + + N_B_TOTAL = 3.0e9 + x_gev = mass_mev / 1000.0 + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 + + prod_yield = N_b_massbin * core # number of A' produced + + # 2. Visible yield fractions + alpha_D = 0.01 # fixed + m_pi_D = mass_mev / 3.0 + m_V_D = 1.8*mass_mev / 3.0 + f_pi_D = (mass_mev / 3.0) * (1.0 / (4.0 * math.pi)) + rho_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=0.75) + phi_width = width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, multiplicity=1.5) + invis_width = width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mass_mev) + charged_width = width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev) + total_width = rho_width + phi_width + invis_width + charged_width + rho_fraction = rho_width/total_width + phi_fraction = phi_width/total_width + + # acceptance integral + s_gamma = (s_psum *1000.0)/m_V_D + s_x = s_vertex_z/s_gamma + prho = 0.0 + pphi = 0.0 + + den_edges, den_vals = base.read_den_hist(m_V_D) + mk = base._mass_key(m_V_D) + mask_hist = base.tight_selection(events,Val,Val2) # updated tight selection (BDT-based) + zvals = events["vertex.pos_.fZ"][mask_hist] + num_vals, _ = np.histogram(zvals, bins=den_edges) + psum_edges, psum_vals = base.read_psum_hist(m_V_D) + for I in range(len(den_vals)): + Ngen = float(den_vals[I]) + Nacc = float(num_vals[I]) + z_cent = .5*(den_edges[I]+den_edges[I+1]) + for J in range(len(psum_vals)): + psum_val = .5*(psum_edges[J]+psum_edges[J+1]) + gamma = 1000*psum_val/m_V_D + rho_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, True) + phi_width = rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, mass_mev, epsilon, .511, False) + rho_length=(1000*HBAR_C*10.0)/rho_width + phi_length=(1000*HBAR_C*10.0)/phi_width + #print("Psum Val: "+str(psum_vals[J])) + #print("F(z): "+str(Nacc/Ngen)) + #print("Nacc: "+str(Nacc)) + #print("Ngen: "+str(Ngen)) + #print("rho_len: "+str(rho_length)) + #print("gamma: "+str(gamma)) + #print("z_cent: "+str(z_cent)) + #print("Shifted rho_len: "+str(rho_length*gamma)) + #print("exp val: "+str(np.exp(-z_cent/(gamma*rho_length)))) + #print("Added Prob: "+str(psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma))) + #print("Total prob: "+str(prho)+"\n") + prho += psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*rho_length))/(rho_length*gamma) + pphi += psum_vals[J]*(Nacc/Ngen)*np.exp(-z_cent/(gamma*phi_length))/(phi_length*gamma) + + vis_yield = prod_yield*(rho_fraction+phi_fraction) + acc_yield = prod_yield*(prho*rho_fraction+pphi*phi_fraction) + print("Signal processing complete") + + # Selection stages: + if acc_yield < 0: + acc_yield = 0.0 # ensure non-negative + sig_cutflow.append(("After acceptance", acc_yield, acc_yield/vis_yield)) # baseline + + current_mask = np.ones(total_sig_events, dtype=bool) + # psum cut + current_mask &= s_psum_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_psum = acc_yield * frac_survive + sig_cutflow.append(("After psum cut", yield_psum, frac_survive * 100.0)) + # L1L1 cut + current_mask &= s_L1L1_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_L1 = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1", yield_L1, frac_survive * 100.0)) + # proj_sig cut + current_mask &= s_proj_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_proj = acc_yield * frac_survive + sig_cutflow.append(("After psum+L1L1+proj", yield_proj, frac_survive * 100.0)) + # BDT score cut (replaces z0 cut) + current_mask &= bdt_sig_mask + frac_survive = np.count_nonzero(current_mask) / total_sig_events if total_sig_events > 0 else 0.0 + yield_final = acc_yield * frac_survive + sig_cutflow.append(("After all cuts", yield_final, frac_survive * 100.0)) + + # ------------------------------ + # Significance calculation + # ------------------------------ + sig_table = [] # will hold (stage, S_yield, B_yield, Zbi) + if bg is not None and bg_cutflow is not None: + bg_yields = { row[0]: row[5] for row in (bg_cutflow or []) } # use in-window yields + for stage, S_yield, _eff in sig_cutflow: + if stage == "After acceptance": + bg_stage = "No cuts" + elif stage.startswith("After psum+L1L1+proj"): + bg_stage = "After psum+L1L1+proj" + elif stage.startswith("After psum+L1L1"): + bg_stage = "After psum+L1L1" + elif stage.startswith("After psum"): + bg_stage = "After psum cut" + elif stage == "After all cuts": + bg_stage = "After all cuts" + else: + bg_stage = None + B_yield = bg_yields.get(bg_stage, 0.0) if bg_yields else 0.0 + Zbi_val = zbi_significance(S_yield, B_yield) + sig_table.append((stage, S_yield, B_yield, Zbi_val)) + else: + for stage, S_yield, _eff in sig_cutflow: + sig_table.append((stage, S_yield, 0.0, float('inf') if S_yield>0 else 0.0)) + + # ------------------------------ + # Output: final text and LaTeX tables + # ------------------------------ + if sig_table: + final_stage, final_S, final_B, final_Z = sig_table[-1] + else: + final_stage, final_S, final_B, final_Z = ("After all cuts", 0.0, 0.0, 0.0) + + # Write a simple text summary in outdir (replacing former --outtxt) + txt_name = f"final_yields_m{int(mass_mev)}_Val{Val}_Val2{Val2}.txt" + outtxt_path = os.path.join(args.outdir, txt_name) + try: + with open(outtxt_path, "w") as fout: + fout.write(f"mass_MeV {mass_mev}\n") + fout.write(f"epsilon {epsilon}\n") + fout.write(f"Val {Val}\n") + fout.write(f"Val2 {Val2}\n") + fout.write(f"stage {final_stage}\n") + fout.write(f"S_yield {final_S:.6e}\n") + fout.write(f"B_yield {final_B:.6e}\n") + fout.write(f"Zbi {final_Z:.6f}\n") + except Exception as e: + sys.stderr.write(f"[error] Failed to write output text file '{outtxt_path}': {e}\n") + sys.exit(1) + + if args.debug: + print(f"[done] Wrote final yields and significance to {outtxt_path}") + + # ------------------------------ + # LaTeX-formatted tables (same structure as plot_all_features_and_do_tables_3.py) + # ------------------------------ + tex_path = os.path.join(args.outdir, "latek.tex") + with open(tex_path, "w") as file1: + file1.write("\\documentclass{article}\n") + file1.write("\\usepackage{graphicx} % Required for inserting images\n") + file1.write("\\usepackage{amsmath}\n") + file1.write("\\usepackage{xcolor}\n") + file1.write("\\pagecolor[rgb]{1,1,1}\n") + file1.write("\\title{TablesForBackgroundRates}\n") + file1.write("\\author{rodwyer100 }\n") + file1.write("\\date{November 2025}\n") + file1.write("\\begin{document}\n") + file1.write("\\maketitle\n") + + # Summary table with core and yields + file1.write("\\begin{tabular}{l|r}\n") + file1.write("Original Core & "+str(core)+"\\\\\n") + file1.write("Total Num Events & 3e9\\\\\n") + file1.write("Area Under Curve At Point & "+str(m_fraction * 4.0)+"\\\\\n") + file1.write("Events At Mass & "+str(format_sci(N_b_massbin))+"\\\\\n") + file1.write("A Prime Yield &"+str(format_sci(prod_yield))+"\\\\\n") + file1.write("Rho BR and Phi BR &"+str(np.round(rho_fraction,4))+","+str(np.round(phi_fraction,4))+"\\\\\n") + file1.write("Visible Yield &"+ str(format_sci(vis_yield))+"\\\\\n") + file1.write("Rho Acc and Phi Acc &"+str(np.round(prho,6))+","+str(np.round(pphi,6))+"\\\\\n") + file1.write("Rho Yield and Phi Yield &"+str(format_sci(prod_yield*prho*rho_fraction))+","+str(format_sci(prod_yield*pphi*phi_fraction))+"\\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\\textbf{Cut 0}\n") + + # Background cutflow table + if bg is not None and bg_cutflow is not None: + file1.write("\n%% Background cutflow table\n") + file1.write("\\begin{tabular}{l|rr|rr|r}\n") + file1.write("\\textbf{Cut stage} & $N_{\\text{in}}$ & $N_{\\text{out}}$ & In~(\\% total) & Out~(\\% total) & $B_{\\text{yield}}$ \\\\ \\hline\n") + for stage, n_in, n_out, pct_in, pct_out, B_yield, _ in bg_cutflow: + count_in_str = f"{int(n_in)}" + count_out_str = f"{int(n_out)}" + pct_in_str = f"{pct_in:.2f}\\%" + pct_out_str = f"{pct_out:.2f}\\%" + yield_str = format_sci(B_yield) if B_yield is not None else "--" + file1.write(f"{stage} & {count_in_str} & {count_out_str} & {pct_in_str} & {pct_out_str} & ${yield_str}$ \\\\\n") + file1.write("\\end{tabular}\n") + + # Signal yield cutflow table + file1.write("%% Signal yield table\n") + file1.write("\\begin{tabular}{l|r@{~}r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & (cum.\\,eff\\%) \\\\ \\hline\n") + prod_str = format_sci(prod_yield) + vis_str = format_sci(vis_yield) + vis_frac_pct = rho_fraction * 100.0 + file1.write(f"Production (total) & ${prod_str}$ & (100\\%) \\\\\n") + file1.write(f"Visible decays & ${vis_str}$ & ({vis_frac_pct:.2f}\\%) \\\\\n") + for stage, S_yield, eff in sig_cutflow: + yield_str = format_sci(S_yield) + file1.write(f"{stage} & ${yield_str}$ & ({eff:.2f}\\%) \\\\\n") + file1.write("\\end{tabular}\n") + + # Significance per step table + file1.write("\\hline\n") + file1.write("%% Significance per selection stage\n") + file1.write("\\begin{tabular}{l|r r r}\n") + file1.write("\\textbf{Stage} & $S_{\\text{yield}}$ & $B_{\\text{yield}}$ & $Z_{\\text{Bi}}$ \\\\ \\hline\n") + for stage, S_yield, B_yield, Zbi_val in sig_table: + S_str = format_sci(S_yield) + B_str = format_sci(B_yield) + Z_str = f"{Zbi_val:.2f}" if math.isfinite(Zbi_val) else "$\\infty$" + file1.write(f"{stage} & ${S_str}$ & ${B_str}$ & {Z_str} \\\\\n") + file1.write("\\end{tabular}\n") + file1.write("\n") + file1.write("\\end{document}\n") + + # Optionally compile LaTeX (if pdflatex is available) + try: + compile_latex(tex_path) + except Exception as e: + if args.debug: + print(f"[warn] pdflatex compilation failed: {e}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BK_w_CUTs.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BK_w_CUTs.py new file mode 100644 index 00000000..e4d32a8e --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_and_do_tables_BK_w_CUTs.py @@ -0,0 +1,429 @@ +#!/usr/bin/env python3 +import os, sys +import argparse +import numpy as np +import uproot, awkward as ak +import math +import matplotlib.pyplot as plt +import subprocess +from scipy.special import betainc, erfinv +from pathlib import Path + +# Try to import background efficiency module for file path and any helpers +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +#DEN_PATH_TMPL_2 = LOCATION+"logger_{mass}.root" +#DEN_HIST_NAME_2 = "h_z_eepair" + + +#@lru_cache(maxsize=None) +#def _den_cache_2(mass_key): +# """Cache denominator histogram once per mass.""" +# den_path = DEN_PATH_TMPL_2.format(mass=int(mass_key)) +# with uproot.open(den_path) as f: +# h = f[DEN_HIST_NAME_2] +# edges = np.asarray(h.axes[0].edges()) +# vals = np.asarray(h.values(), dtype=float) +# return edges, vals + +def compile_latex(tex_file): + tex_path = Path(tex_file).resolve() + # Run twice for references, etc. + for _ in range(2): + subprocess.run( + ["pdflatex", "-interaction=nonstopmode", tex_path.name], + cwd=tex_path.parent, + check=True, + ) + +def _extract_z_from_arrays(arrays): + """Prefer vertex.pos_.fZ if split; otherwise inspect nested record for fZ/Z/z.""" + for cand in ["vertex.pos_.fZ", "vertex.pos__fZ", "vertex.pos_.Z", "vertex.pos_.z"]: + if cand in arrays.fields: + return np.asarray(ak.to_numpy(arrays[cand])) + if "vertex.pos_" in arrays.fields: + rec = arrays["vertex.pos_"] + flds = ak.fields(rec) + for fn in ["fZ", "Z", "z"]: + if fn in flds: + return np.asarray(ak.to_numpy(rec[fn])) + for sub in ["fCoordinates", "fCoord", "coords", "Coord", "coord"]: + if sub in flds: + subrec = rec[sub] + for fn in ["fZ", "Z", "z"]: + if fn in ak.fields(subrec): + return np.asarray(ak.to_numpy(subrec[fn])) + for k in arrays.fields: + if k.endswith("fZ") or k.endswith(".fZ"): + return np.asarray(ak.to_numpy(arrays[k])) + raise KeyError("Could not locate z coordinate from vertex.pos_.") + +def zbi_significance(S: float, B: float) -> float: + """Compute the Zbi significance for signal yield S and background yield B:contentReference[oaicite:9]{index=9}:contentReference[oaicite:10]{index=10}.""" + if B < 0: + return float('nan') + if B == 0: + return 9.0 if S > 0 else 0.0 + p = betainc(S+B, 1+B, 0.5) + z = math.sqrt(2.0) * erfinv(1 - 2*p) + if p < 1e-16: + z = 9.0 + # Do not allow negative significance (downward fluctuation scenario) + if z < 0: + z = 0.0 + return float(z) + +# Utility for scientific notation formatting in LaTeX +def format_sci(value: float, prec: int = 2) -> str: + if value == 0 or not math.isfinite(value): + return f"{value:.{prec}f}" + exp = int(math.floor(math.log10(abs(value)))) + base = value / (10**exp) + # Round base to desired precision + fmt_base = f"{base:.{prec}f}" + # Remove trailing zeros and dot if needed + fmt_base = fmt_base.rstrip('0').rstrip('.') + return f"{fmt_base} \\times 10^{{{exp}}}" + +# Functions to compute hidden sector decay fractions (using decayLength7sel model equations) +HBAR_C = 1.973e-14 # GeV*cm +def beta_func(x, y): + return (1 + y**2 - x**2 - 2*y) * (1 + y**2 - x**2 + 2*y) +def width_Ap_to_charged(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap): + x = m_pi_D / m_Ap + y = m_V_D / m_Ap + Tv = 18.0 - ((3.0/2.0)+(3.0/4.0)) + coeff = alpha_D * Tv / (192.0 * np.power(math.pi, 4)) + return coeff * np.power((m_Ap / m_pi_D), 2) * np.power(m_V_D / m_pi_D, 2) * np.power((m_pi_D / f_pi_D), 4) * m_Ap * np.power(beta_func(x, y), 3 / 2.0) + +def width_Ap_to_vector(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, multiplicity=1.0): + # Partial width for A' -> V_D + (n_pions) with given multiplicity + x = m_V_D / m_Ap + y = m_pi_D / m_Ap + prefactor = (alpha_D * multiplicity) / (192.0 * (math.pi**4)) + ratio_terms = (m_Ap / m_pi_D)**2 * (m_V_D / m_pi_D)**2 * (m_pi_D / f_pi_D)**4 + return prefactor * ratio_terms * m_Ap * (beta_func(x, y)**1.5) +def width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, m_Ap): + # Partial width for A' -> pi_D pi_D (invisible mode) + term1 = 1 - (4.0 * m_pi_D**2) / (m_Ap**2) + term2 = ((m_V_D**2) / (m_Ap**2 - m_V_D**2))**2 + return ((2.0 * alpha_D) / 3.0) * m_Ap * (term1**1.5) * term2 + +def rate_2l(alpha_D, f_pi_D, m_pi_D, m_V_D, m_Ap, epsilon, m_l, rho): + alpha = 1.0 / 137.0 + coeff = (16 * math.pi * alpha_D * alpha * epsilon**2 * f_pi_D**2) / (3 * m_V_D**2) + term1 = (m_V_D**2 / (m_Ap**2 - m_V_D**2))**2 + term2 = (1 - (4 * m_l**2 / m_V_D**2))**0.5 + term3 = 1 + (2 * m_l**2 / m_V_D**2) + constant = 1 if not rho else 2 + return coeff * term1 * term2 * term3 * m_V_D * constant + + +def plotRates(outdir): + mAp = 100 + m_V_D = 1.8*mAp/3.0 + m_pi_D = mAp/3.0 + mpi_over_fpi=[2*(float(t)/100)+4*np.pi*(1.0-float(t)/100) for t in range(100)] + alpha_D = .01 + rho_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=0.75) for i in range(len(mpi_over_fpi)) ] + phi_width = [width_Ap_to_vector(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp, multiplicity=1.5) for i in range(len(mpi_over_fpi)) ] + invis_width = [width_Ap_to_invis(alpha_D, m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + charged_width = [width_Ap_to_charged(alpha_D, m_pi_D/mpi_over_fpi[i], m_pi_D, m_V_D, mAp) for i in range(len(mpi_over_fpi)) ] + total_width = [rho_width[i]+phi_width[i]+invis_width[i]+charged_width[i] for i in range(len(mpi_over_fpi)) ] + rho_width = [rho_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + phi_width = [phi_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + invis_width = [invis_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + charged_width = [charged_width[i]/total_width[i] for i in range(len(mpi_over_fpi)) ] + fig, ax=plt.subplots() + ax.plot(mpi_over_fpi,invis_width,"red") + ax.plot(mpi_over_fpi,rho_width,"yellow") + ax.plot(mpi_over_fpi,phi_width,"green") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i] for i in range(len(mpi_over_fpi))],"blue") + ax.plot(mpi_over_fpi,charged_width,"purple") + ax.plot(mpi_over_fpi,[phi_width[i]+rho_width[i]+charged_width[i] for i in range(len(mpi_over_fpi))],"black") + ax.set_yscale("log") + plt.savefig(outdir+"/contours.png") + +##ALL OF THIS HAS BEEN VALIDATED SO FAR + +# Main function +def main(): + ap = argparse.ArgumentParser(description="Compute cutflow tables and plots for signal/background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV") + ap.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing parameter epsilon") + #ap.add_argument("--Val", type=int, default=25, help="Selection parameter (e.g. z0 threshold index).") + #ap.add_argument("--Val2", type=int, default=25, help="Selection parameter (e.g. proj_sig threshold index).") + ap.add_argument("--outdir", type=str, default="output_plots", help="Directory for output plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D histograms.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D histograms.") + ap.add_argument("--base-module", type=str, default="decayLength7sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Enable debug output") + ap.add_argument("--normalized", type=int, default=0, help="1 if normalizaed") + args = ap.parse_args() + + mass_mev = args.mass + epsilon = args.epsilon + #Val = args.Val + Val2 = args.Val2 + normalized = (args.normalized==1) + + os.makedirs(args.outdir, exist_ok=True) + + # Import the signal base module (e.g., decayLength7sel.py) dynamically + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + # ------------------------------ + # Background events processing + # ------------------------------ + + mask_list = [] + cutflow_list = [] + + for Val in range(25): + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + sys.stderr.write("[warn] Background module not available, skipping background processing.\n") + bg_events = None + else: + bg_path = bg.BACKGROUND_PATH + try: + # Open background file and get the TTree + with uproot.open(bg_path) as f: + # Use helper from module if available to find the tree + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + # fallback: pick the first tree + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + sys.stderr.write("[error] No TTree found in background file.\n") + sys.exit(1) + # Define the branches to load + desired_branches = [ + "psum","vertex.pos_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + + "vertex.invM_", "psum", + "ele.track_.z0_", "pos.track_.z0_", + "vtx_proj_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_" + #"vertex.pos_.fZ_" + ] + arrays = tree.arrays(desired_branches, library="ak", how=dict) + except Exception as e: + sys.stderr.write(f"[error] Failed to read background file: {e}\n") + sys.exit(1) + + # Convert Awkward arrays to numpy for easier masking + invM = ak.to_numpy(arrays.get("vertex.invM_")) + psum = ak.to_numpy(arrays.get("psum")) + ele_z0 = ak.to_numpy(arrays.get("ele.track_.z0_")) + pos_z0 = ak.to_numpy(arrays.get("pos.track_.z0_")) + proj_sig = ak.to_numpy(arrays.get("vtx_proj_sig")) + ele_layers_s = arrays.get("ele.track_.hit_layers_") + pos_layers_s = arrays.get("pos.track_.hit_layers_") + if ele_layers_s is not None: + ele_hasL0_s = ak.to_numpy(ak.any(ele_layers_s == 0, axis=-1)) + ele_hasL1_s = ak.to_numpy(ak.any(ele_layers_s == 1, axis=-1)) + ele_L1L1_s = np.asarray(ele_hasL0_s & ele_hasL1_s, bool) + else: + ele_L1L1_s = np.ones_like(invM_sig, bool) + if pos_layers_s is not None: + pos_hasL0_s = ak.to_numpy(ak.any(pos_layers_s == 0, axis=-1)) + pos_hasL1_s = ak.to_numpy(ak.any(pos_layers_s == 1, axis=-1)) + pos_L1L1_s = np.asarray(pos_hasL0_s & pos_hasL1_s, bool) + else: + pos_L1L1_s = np.ones_like(invM_sig, bool) + + L1L1_mask = np.logical_and(ele_L1L1_s, pos_L1L1_s) + + # Define cut masks for background + psum_mask = (psum >= 1.5) & (psum <= 3.0) + z_thr = 0.5 * (float(Val) / 25.0) + z0_mask = np.logical_and((ele_z0 > z_thr) | (ele_z0 < -z_thr), + (pos_z0 > z_thr) | (pos_z0 < -z_thr)) + #proj_mask = (proj_sig < 50) + + proj_mask = (proj_sig < 20.0*(Val2)+1.0) + + # Mass window mask (±2 MeV around 1.8m_A/3.0) + center_geV = 1.8*mass_mev / (1000.0 * 3.0) + mass_mask = (invM>-1.0)#(invM > (center_geV - 0.002)) & (invM < (center_geV + 0.002)) + + total_events = len(invM) + initial_in = np.count_nonzero(mass_mask) + initial_out = total_events - initial_in + if total_events > 0: + init_in_frac = 100.0 * initial_in / total_events + init_out_frac = 100.0 * initial_out / total_events + else: + init_in_frac = init_out_frac = 0.0 + if args.debug: + print(f"Initial in-window fraction: {init_in_frac:.2f}%, out-of-window: {init_out_frac:.2f}%") + + # Sequentially apply cuts and count in/out + bg_cutflow = [] # will store tuples for table rows + # Stage 0: no cuts (baseline) + bg_cutflow.append(("No cuts", + initial_in, initial_out, + init_in_frac, init_out_frac, + None)) # yield to be filled after computing expected yields + # Apply each cut in order, building on previous mask + current_mask = np.ones(total_events, dtype=bool) + # 1. psum + current_mask &= psum_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum cut", n_in, n_out, frac_in, frac_out, None)) + # 2. L1L1 + current_mask &= L1L1_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1", n_in, n_out, frac_in, frac_out, None)) + # 3. proj_sig + current_mask &= proj_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After psum+L1L1+proj", n_in, n_out, frac_in, frac_out, None)) + # 4. z0 + current_mask &= z0_mask + n_in = np.count_nonzero(current_mask & mass_mask) + n_out = np.count_nonzero(current_mask & ~mass_mask) + frac_in = 100.0 * n_in / total_events if total_events > 0 else 0.0 + frac_out = 100.0 * n_out / total_events if total_events > 0 else 0.0 + bg_cutflow.append(("After all cuts", n_in, n_out, frac_in, frac_out, None)) + + # Compute expected background yield in the ±2 MeV window after each stage + # Use polynomial m(x) fraction for the mass window (0.1 GeV width):contentReference[oaicite:16]{index=16} + #x_gev = 1.8*mass_mev / (1000.0*3.0) + x_gev = mass_mev / (1000.0) + + #DIVIDED BY 3.0 HERE TOO AND TIMES 1.8. NVMD I DON'T ACTUALLY THINK THIS IS RIGHT NOW!! + + poly_num = (-6860.03 + 299358.0*x_gev - 4087220.0*(x_gev**2) + + 25209900.0*(x_gev**3) - 73485900.0*(x_gev**4) + 82579800.0*(x_gev**5)) + m_fraction = poly_num / (82.9268041667 * 1000.0) # fraction per 0.1 GeV in this mass bin + N_B_TOTAL = 3.0e9 # total number of background-triggered events (given by user) + N_b_massbin = N_B_TOTAL * m_fraction * 4.0 # expected events in ±0.005 GeV window if no selection, this is 4 MeV wide + # Loop through cutflow to fill expected yields for in-window + for i, row in enumerate(bg_cutflow): + stage, n_in, n_out, pct_in, pct_out, _ = row + if i == 0: + # initial (no cuts) expected yield in window = N_b_massbin (baseline) + exp_yield_in = N_b_massbin + exp_yield_out = N_b_massbin + else: + if initial_in > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive = n_in / initial_in + else: + frac_survive = 0.0 + if initial_out > 0: + # fraction of in-window events surviving = n_in / initial_in + frac_survive_out = n_out / initial_out + else: + frac_survive_out = 0.0 + exp_yield_in = N_b_massbin * frac_survive + exp_yield_out = N_b_massbin * frac_survive_out + bg_cutflow[i] = (stage, n_in, n_out, pct_in, pct_out, exp_yield_in, exp_yield_out) + cutflow_list.append(bg_cutflow) + bg_final_mask = None + if bg is not None: + total_events = len(invM) + bg_final_mask = np.ones(total_events, dtype=bool) + bg_final_mask &= psum_mask & L1L1_mask & proj_mask & z0_mask & mass_mask # also ensure in mass window for plotting background + mask_list.append(bg_final_mask) + # ------------------------------ + # Plot generation (after final cuts) + # ------------------------------ + + # Choose features to plot: use common features present in both samples + features_to_plot = ["psum","vertex.pos_.fX_","vertex.pos_.fY_","vertex.pos_.fZ_","vertex.invM_","vertex.invMerr_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", "ele.track_.z0_", "ele.track_.tan_lambda_", + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", "ele.track_.x_at_ecal_", + "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", "pos.track_.z0_", "pos.track_.tan_lambda_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", "pos.track_.x_at_ecal_", + "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig"] + + #"vertex.pos_.fZ", "ele.track_.z0_", "pos.track_.z0_", "psum", "vtx_proj_sig"] + # Ensure those exist in events and arrays + print(features_to_plot) + #features_to_plot = [f for f in features_to_plot if (f in events) and (bg is None or f in arrays.keys())] + #print(features_to_plot) + # Plot 1D histograms overlay + for feat in features_to_plot: + for I in range(25): + if bg_final_mask is not None and feat in arrays.keys(): + b_data = ak.to_numpy(arrays[feat])[mask_list[I]] + print("The background for "+str(feat)+" is as follows") + print(b_data) + else: + b_data = np.array([]) # no background + print("I said there was no background for "+str(feat)) + # Compute S and B yields for annotation (use final yields from above) + B_final = cutflow_list[I][-1][5] if bg is not None else 0.0 + combined = b_data + #np.concatenate([b_data]) if b_data.size > 0 + if combined.size > 0: + q_low, q_high = np.percentile(combined, [0.5, 99.5]) + else: + q_low, q_high = 0, 1 + # Add a small padding + pad = 0.05 * (q_high - q_low if q_high > q_low else 1.0) + hist_range = (q_low - pad, q_high + pad) + # Bin weights such that total area equals expected yield + b_w = None + + if(normalized): + B_final = 1.0 + if b_data.size > 0: + b_w = np.full(b_data.shape, B_final / b_data.size) + #plt.figure(figsize=(7.5, 4.5)) + bins = args.bins + plt.hist(b_data, bins=bins, range=hist_range, weights=b_w, histtype="step", linewidth=1.8, label="Val="+str(25*(I/25.0))) + title = (f"{feat} distribution after final cuts\n" + f"B = {B_final:.2g} " + f"Val = {I}") + plt.title(title) + plt.xlabel(feat) + plt.ylabel("Expected events") + plt.yscale("log") + plt.legend(loc="best") + plt.grid(alpha=0.3) + # Save plot + fname = feat.replace("/", "_").replace(".", "_") + outfile = os.path.join(args.outdir, f"plot1D_{fname}_overlaidValues.png") + plt.tight_layout() + plt.savefig(outfile, dpi=150) + plt.close() + if args.debug: + print(f"[debug] Saved 1D plot: {outfile}") + if args.debug: + print("[done] All tables generated and plots saved.") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_features_normed_and_2d_debug.py b/tools/simp-search-tools/plot-making/misc/plot_all_features_normed_and_2d_debug.py new file mode 100644 index 00000000..8c86f778 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_features_normed_and_2d_debug.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import itertools +import numpy as np +import matplotlib.pyplot as plt +from pathlib import Path + +# ---------------------- Robust Position Extractors ---------------------- +def _first_present_field(arrays, candidates): + for name in candidates: + if name in getattr(arrays, "fields", []): + try: + import awkward as ak + arr = ak.to_numpy(arrays[name]) + return np.asarray(arr) + except Exception: + continue + return None + +def _extract_pos_components(arrays): + # Candidates for each axis in descending preference + candX = [ + "vertex.pos_.fX","vertex.pos_.X","vertex.pos_.x","vtx_x","vertex.x","pos_x","vtxPosX","vtxX","vtx.pos.x" + ] + candY = [ + "vertex.pos_.fY","vertex.pos_.Y","vertex.pos_.y","vtx_y","vertex.y","pos_y","vtxPosY","vtxY","vtx.pos.y" + ] + candZ = [ + "vertex.pos_.fZ","vertex.pos_.Z","vertex.pos_.z","vtx_z","vertex.z","pos_z","vtxPosZ","vtxZ","vtx.pos.z" + ] + out = {} + x = _first_present_field(arrays, candX) + y = _first_present_field(arrays, candY) + z = _first_present_field(arrays, candZ) + if x is not None: out["vertex.pos_.fX"] = x + if y is not None: out["vertex.pos_.fY"] = y + if z is not None: out["vertex.pos_.fZ"] = z + return out + +# ---------------------- Alias Helpers ---------------------- +def _first_numeric_alias(arrays, names): + import awkward as ak + for nm in names: + if nm in getattr(arrays, "fields", []): + try: + arr = ak.to_numpy(arrays[nm]) + arr = np.asarray(arr) + if arr.ndim == 1 and np.issubdtype(arr.dtype, np.number): + return arr + except Exception: + pass + return None + +# ---------------------- Imports & Base Modules ---------------------- +# Background helper module (expected in user's repo) +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write("[warn] Could not import bk_eff_selection as bg: {}.\n".format(e)) + bg = None + +# Signal base module (decayLength5sel by default) +def import_signal_base(module_name="decayLength7sel"): + try: + base = __import__(module_name) + return base + except SystemExit: + sys.stderr.write("[fatal] Importing '{}' triggered SystemExit (argparse at top-level?). Guard CLI with if __name__ == '__main__'.\n".format(module_name)) + raise + except Exception: + import importlib.util + here = os.path.dirname(os.path.abspath(__file__)) + candidate = os.path.join(here, module_name + ".py") + if os.path.exists(candidate): + spec = importlib.util.spec_from_file_location(module_name, candidate) + mod = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(mod) + return mod + except SystemExit: + sys.stderr.write("[fatal] Executing '{}' still triggered argparse at import. Please guard the CLI in that file.\n".format(candidate)) + raise + else: + raise + +# ---------------------- I/O Utilities ---------------------- +def _ensure_dir(d): + if d and not os.path.isdir(d): + os.makedirs(d, exist_ok=True) + +# ---------------------- Data Loading ---------------------- +def load_signal_events(base, mass_mev, Val): + """Return dict of feature->np.array for signal after tight selection.""" + mkey = base._mass_key(mass_mev) + events = base._events_cache(mkey) + mask = base.tight_selection(events, Val) + out = {} + for k, v in events.items(): + if isinstance(v, np.ndarray) and v.ndim == 1 and len(v) == len(mask): + vv = v[mask] + if np.issubdtype(vv.dtype, np.number): + vv = vv[np.isfinite(vv)] + out[k] = vv + if "vertex.pos_.fZ" not in out and "vertex.pos_.z" in out: + out["vertex.pos_.fZ"] = out["vertex.pos_.z"] + return out + +from pathlib import Path # make sure this import is at the top of the file + +def _map_desired_to_available(avail_names, desired_list): + """ + Map canonical desired branch names (e.g. 'vertex.invM_') to actual ROOT + branch names present in the file (e.g. 'vertex./vertex.invM_'). + Strategy: + 1) exact match + 2) suffix match on '/{desired}' or '.{desired}' or '{desired}' + 3) last-token match after '/' then after '.' + """ + avail = list(avail_names) + mapping = {} + + def last_token(s): + part = s.split('/')[-1] + return part.split('.')[-1] + + for d in desired_list: + if d in avail: + mapping[d] = d + continue + # suffix match first + cand = [a for a in avail if a.endswith("/"+d) or a.endswith("."+d) or a.endswith(d)] + if not cand: + dtok = last_token(d) + cand = [a for a in avail if last_token(a) == dtok] + if cand: + mapping[d] = sorted(cand, key=len)[0] # shortest path + return mapping + +def _get_array_by_canonical(arrays, canonical, name_map): + """Fetch arrays[name_map[canonical]] if mapped, else try suffix match in arrays.fields.""" + real = name_map.get(canonical) + if real is None and canonical in getattr(arrays, "fields", []): + real = canonical + if real is None: + for f in getattr(arrays, "fields", []): + if f.endswith("/"+canonical) or f.endswith("."+canonical) or f.endswith(canonical): + real = f + break + if real is None: + return None + import awkward as ak + try: + return np.asarray(ak.to_numpy(arrays[real])) + except Exception: + return None + +def load_background_events(mass_mev, Val, desired_keys, debug=False): + """Return dict feature->np.array for background after tight selection, robust to 'vertex./...' names.""" + if bg is None: + return {} + + import uproot, awkward as ak, glob + + # Resolve BACKGROUND_PATH -> list of files (file, dir, glob, list are accepted) + def _resolve_bg_paths(bg_module): + paths = [] + cand = getattr(bg_module, "BACKGROUND_PATH", None) + def add(c): + if c is None: return + if isinstance(c, (list, tuple)): + for cc in c: add(cc); return + c = str(c) + p = Path(c) + if any(ch in c for ch in "*?["): + paths.extend(sorted(glob.glob(c))) + elif p.is_dir(): + paths.extend(sorted(str(pp) for pp in p.rglob("*.root"))) + elif p.exists(): + paths.append(str(p)) + add(cand) + return paths + + files = _resolve_bg_paths(bg) + if not files: + if debug: + print("[debug] bk_eff_selection from:", getattr(bg, "__file__", "")) + print("[debug] BACKGROUND_PATH:", getattr(bg, "BACKGROUND_PATH", None)) + print("[debug] resolved background files: 0") + return {} + + with uproot.open(files[0]) as f: + if hasattr(bg, "_open_first_tree"): + t = bg._open_first_tree(f) + else: + # choose first TTree-like key + candidates = [k for k in f.keys() if ";" in k] + tname = candidates[0] if candidates else None + if tname is None: + if debug: + print("[debug] no TTree candidates in:", files[0]) + return {} + t = f[tname] + + try: + avail = list(t.keys()) # this returns the full branch paths, like 'vertex./vertex.invM_' + except Exception: + avail = [] + + # Build the request: desired keys + common aux (mass, psum, vtx_proj_sig, vertex.pos_* aliases) + base_needed = { + "vertex.invM_", "psum", "vtx_proj_sig", "vertex.pos_", + "vertex.pos_.fX","vertex.pos_.fY","vertex.pos_.fZ", + "vertex.pos_.X","vertex.pos_.Y","vertex.pos_.Z", + "vertex.pos_.x","vertex.pos_.y","vertex.pos_.z", + } + req_all = set(desired_keys) | base_needed | set(getattr(bg, "BRANCHES", [])) + + # Map canonical requests to available names like 'vertex./vertex.invM_' + name_map = _map_desired_to_available(avail, sorted(req_all)) + branch_req = sorted(set(name_map.values())) + + arrays = t.arrays(branch_req, library="ak") + + # ----- Build events dict using canonical names ----- + events_bg = {} + + # Position (X/Y/Z): try via your project-specific extractor first, then robust aliases + if hasattr(bg, "_extract_z_from_arrays"): + try: + zvals = bg._extract_z_from_arrays(arrays) + events_bg["vertex.pos_.fZ"] = np.asarray(zvals, dtype=float) + except Exception: + pass + + # Robust XYZ using the actual names we loaded + def _first_numeric_alias(arrays, names): + import awkward as ak + for nm in names: + if nm in getattr(arrays, "fields", []): + try: + arr = np.asarray(ak.to_numpy(arrays[nm])) + if arr.ndim == 1 and np.issubdtype(arr.dtype, np.number): + return arr + except Exception: + pass + return None + + # Z fallback through mapping + if "vertex.pos_.fZ" not in events_bg: + z = _get_array_by_canonical(arrays, "vertex.pos_.fZ", name_map) + if z is None: + for alias in ["vertex.pos_.Z","vertex.pos_.z"]: + z = _get_array_by_canonical(arrays, alias, name_map) + if z is not None: break + if z is not None: + events_bg["vertex.pos_.fZ"] = z + + # Bring over any requested desired keys using the mapping + for k in desired_keys: + arr = _get_array_by_canonical(arrays, k, name_map) + if arr is not None and np.issubdtype(arr.dtype, np.number): + events_bg[k] = arr + + # psum and vtx_proj_sig (with aliases, then mapping) + if "psum" not in events_bg: + _tmp = _first_numeric_alias(arrays, ["psum","vertex.psum","p_sum","pSum","sumP"]) + if _tmp is None: + _tmp = _get_array_by_canonical(arrays, "psum", name_map) + if _tmp is not None: + events_bg["psum"] = _tmp + + if "vtx_proj_sig" not in events_bg: + _tmp = _first_numeric_alias(arrays, ["vtx_proj_sig","vertex.projSig","vtxProjSig","vtx_sigma_proj"]) + if _tmp is None: + _tmp = _get_array_by_canonical(arrays, "vtx_proj_sig", name_map) + if _tmp is not None: + events_bg["vtx_proj_sig"] = _tmp + + # Mass window (use canonical or alias via mapping). If not found, skip mass windowing. + invM_aliases = ["vertex.invM_", "invM_", "invMass", "m_inv", "mInv", "vtxInvM", "vtx.invM"] + invM = None + for nm in invM_aliases: + invM = _get_array_by_canonical(arrays, nm, name_map) + if invM is not None: + break + if invM is None: + mwin = None + if debug: + print("[debug] invariant mass not found via mapping; skipping mass window.") + else: + mwin = bg._mass_window_mask(invM, mass_mev) if hasattr(bg, "_mass_window_mask") else np.ones_like(invM, dtype=bool) + + # Tight selection mask + tight_mask = None + if hasattr(bg, "_tight_selection_mask"): + try: + tight_mask = bg._tight_selection_mask(events_bg, Val) + except Exception as e: + if debug: + print("[debug] bg._tight_selection_mask failed:", e) + if tight_mask is None: + z = events_bg.get("vertex.pos_.fZ") + tight_mask = np.isfinite(z) if z is not None else np.ones_like(next(iter(events_bg.values())), dtype=bool) + + mask = tight_mask if mwin is None else (tight_mask & mwin) + mask = mask.astype(bool) + + # Finalize numeric 1D arrays + out = {} + for k, v in events_bg.items(): + if isinstance(v, np.ndarray) and v.ndim == 1 and len(v) == len(mask) and np.issubdtype(v.dtype, np.number): + vv = v[mask] + vv = vv[np.isfinite(vv)] + out[k] = vv + return out + + +# ---------------------- Feature Selection ---------------------- +def pick_1d_numeric_features(sig_dict, bg_dict): + """Return sorted list of feature names present with >=2 entries in both dicts.""" + common = [] + for k in sig_dict.keys(): + if k in bg_dict and sig_dict[k].size >= 2 and bg_dict[k].size >= 2: + common.append(k) + prioritise = [ + "vertex.pos_.fZ", + "ele.track_.z0_", + "pos.track_.z0_", + "ele.track_.d0_", + "pos.track_.d0_", + "vertex.invM_", + "psum", + "vtx_proj_sig", + "ele.track_.chi2_", + "pos.track_.chi2_", + "ele.track_.px_", + "ele.track_.py_", + "ele.track_.pz_", + "pos.track_.px_", + "pos.track_.py_", + "pos.track_.pz_", + ] + seen = set() + ordered = [] + for p in prioritise: + if p in common and p not in seen: + ordered.append(p) + seen.add(p) + for k in sorted(common): + if k not in seen: + ordered.append(k) + seen.add(k) + return ordered + +# ---------------------- Plotting ---------------------- +def sanitize(name: str) -> str: + bad = [" ", "/", "\\", "(", ")", "[", "]", "{", "}", ":", ";", ",", "|", "<", ">", "?", "*", "'", '"', "."] + out = name + for b in bad: + out = out.replace(b, "_") + while "__" in out: + out = out.replace("__", "_") + return out.strip("_") + +# ---------------------- Background Path Resolver ---------------------- +def _resolve_bg_paths(bg_module): + """Return a list of ROOT file paths for background. + Accepts: + - Exact file path (string) + - Directory containing ROOT files + - Glob pattern(s) + - List/tuple of any of the above + """ + import glob + paths = [] + cand = getattr(bg_module, "BACKGROUND_PATH", None) + if cand is None: + return paths + def add_candidate(c): + if c is None: + return + if isinstance(c, (list, tuple)): + for cc in c: + add_candidate(cc) + return + c = str(c) + p = Path(c) + if any(ch in c for ch in ["*", "?", "["]): + paths.extend(sorted(glob.glob(c))) + elif p.is_dir(): + paths.extend(sorted(str(pp) for pp in p.rglob("*.root"))) + elif p.exists(): + paths.append(str(p)) + else: + # allow silent miss; caller may print debug + pass + add_candidate(cand) + return paths + +def _debug_bg_info(bg_module, files, t=None): + try: + print("[debug] bk_eff_selection imported from:", getattr(bg_module, "__file__", "")) + print("[debug] BACKGROUND_PATH:", getattr(bg_module, "BACKGROUND_PATH", None)) + print("[debug] resolved background files:", len(files)) + for f in files[:5]: + print(" -", f) + if t is not None: + try: + print("[debug] tree keys count:", len(t.keys())) + print(t.keys()) + except Exception: + pass + except Exception: + pass + +def plot_1d_overlaid(sig_arr, bg_arr, feat, outdir, mass_mev, Val, bins): + fig, ax = plt.subplots(figsize=(7.8, 4.8)) + both = np.concatenate([sig_arr, bg_arr]) + q1, q99 = np.quantile(both, [0.005, 0.995]) + pad = 0.05 * (q99 - q1 + 1e-12) + rrange = (q1 - pad, q99 + pad) + ax.hist(sig_arr, bins=bins, range=rrange, density=True, histtype="step", linewidth=1.8, label="signal") + ax.hist(bg_arr, bins=bins, range=rrange, density=True, histtype="step", linewidth=1.8, label="background") + ax.set_title(f"{feat}\nunit-normalized; mass={mass_mev:g} MeV, Val={Val}") + ax.set_xlabel(feat) + ax.set_ylabel("density") + ax.grid(True, alpha=0.30) + ax.legend() + f = os.path.join(outdir, f"oned_{sanitize(feat)}_{int(round(mass_mev))}MeV_V{Val}.png") + fig.tight_layout(); fig.savefig(f, dpi=160, bbox_inches="tight"); plt.close(fig) + return f + +def plot_2d_single(data_x, data_y, feat_x, feat_y, label, outdir, mass_mev, Val, bins2d): + fig, ax = plt.subplots(figsize=(6.6, 5.8)) + def rrange(a): + q1, q99 = np.quantile(a, [0.01, 0.99]) + pad = 0.05 * (q99 - q1 + 1e-12) + return (q1 - pad, q99 + pad) + rx = rrange(data_x) + ry = rrange(data_y) + h = ax.hist2d(data_x, data_y, bins=bins2d, range=[rx, ry]) + cb = fig.colorbar(h[3], ax=ax) + cb.set_label("counts") + ax.set_title(f"{label} 2D: {feat_x} vs {feat_y}\nmass={mass_mev:g} MeV, Val={Val}") + ax.set_xlabel(feat_x); ax.set_ylabel(feat_y) + ax.grid(True, alpha=0.15) + f = os.path.join(outdir, f"twoD_{label}_{sanitize(feat_x)}__{sanitize(feat_y)}_{int(round(mass_mev))}MeV_V{Val}.png") + fig.tight_layout(); fig.savefig(f, dpi=160, bbox_inches="tight"); plt.close(fig) + return f + +# ---------------------- Main ---------------------- +def main(): + ap = argparse.ArgumentParser(description="Plot overlaid unit-normalized 1D and separate 2D histograms for signal/background.") + ap.add_argument("--mass", type=float, required=True, help="Mass in MeV used to choose files in signal module.") + ap.add_argument("--epsilon", type=float, default=None, help="(Currently unused) Optional epsilon for future per-event weighting.") + ap.add_argument("--Val", type=int, default=25, help="Tight selection parameter used in selections. Default 25.") + ap.add_argument("--outdir", type=str, default="plots_all_features", help="Output directory for plots.") + ap.add_argument("--bins", type=int, default=80, help="Bins for 1D plots.") + ap.add_argument("--bins2d", type=int, default=80, help="Bins per axis for 2D plots.") + ap.add_argument("--features", type=str, nargs="*", default=None, help="Optional subset of feature names to include (must match keys).") + ap.add_argument("--max-pairs", type=int, default=None, help="Optional cap on number of 2D feature pairs (per sample).") + ap.add_argument("--base-module", type=str, default="decayLength5sel", help="Signal base module name.") + ap.add_argument("--debug", action="store_true", help="Print background resolution and tree info.") + args = ap.parse_args() + + _ensure_dir(args.outdir) + + base = import_signal_base(args.base_module) + + sig = load_signal_events(base, args.mass, args.Val) + if not sig: + sys.stderr.write("[error] No signal features loaded after selection.\n"); sys.exit(2) + + all_sig_feats = sorted([k for k, v in sig.items() if isinstance(v, np.ndarray) and v.ndim == 1 and v.size >= 2]) + if "vertex.pos_.fZ" not in all_sig_feats and "vertex.pos_.z" in all_sig_feats: + all_sig_feats.insert(0, "vertex.pos_.fZ") + + bg_dict = load_background_events(args.mass, args.Val, desired_keys=set(all_sig_feats), debug=args.debug) + + if args.features: + selected = [f for f in args.features if f in sig and (not bg_dict or f in bg_dict)] + if not selected: + sys.stderr.write("[warn] --features list produced no usable features; falling back to common set.\n") + selected = None + else: + selected = None + + common_feats = pick_1d_numeric_features(sig, bg_dict) if bg_dict else sorted(list(sig.keys())) + features_1d = selected if selected else common_feats + + # 1D overlaid plots + out_1d = [] + if bg_dict: + for feat in features_1d: + try: + fpath = plot_1d_overlaid(sig[feat], bg_dict[feat], feat, args.outdir, args.mass, args.Val, args.bins) + out_1d.append(fpath) + except Exception as e: + sys.stderr.write(f"[warn] 1D overlay failed for {feat}: {e}\n") + else: + for feat in features_1d: + try: + fig, ax = plt.subplots(figsize=(7.8, 4.8)) + arr = sig[feat] + q1, q99 = np.quantile(arr, [0.005, 0.995]) + pad = 0.05 * (q99 - q1 + 1e-12) + rrange = (q1 - pad, q99 + pad) + ax.hist(arr, bins=args.bins, range=rrange, density=True, histtype="step", linewidth=1.8, label="signal") + ax.set_title(f"{feat}\nunit-normalized; mass={args.mass:g} MeV, Val={args.Val}") + ax.set_xlabel(feat); ax.set_ylabel("density"); ax.grid(True, alpha=0.30); ax.legend() + f = os.path.join(args.outdir, f"oned_signal_{sanitize(feat)}_{int(round(args.mass))}MeV_V{args.Val}.png") + fig.tight_layout(); fig.savefig(f, dpi=160, bbox_inches="tight"); plt.close(fig) + out_1d.append(f) + except Exception as e: + sys.stderr.write(f"[warn] 1D signal-only failed for {feat}: {e}\n") + + # 2D plots (separate) + feats_for_2d = features_1d + pairs = list(itertools.combinations(feats_for_2d, 2)) + if args.max_pairs is not None: + pairs = pairs[: int(args.max_pairs)] + + out_2d_sig = [] + out_2d_bg = [] + + # Signal 2D + for fx, fy in pairs: + try: + x = sig[fx]; y = sig[fy] + if x.size >= 10 and y.size >= 10: + f = plot_2d_single(x, y, fx, fy, "signal", args.outdir, args.mass, args.Val, args.bins2d) + out_2d_sig.append(f) + except Exception as e: + sys.stderr.write(f"[warn] 2D signal failed for {fx} vs {fy}: {e}\n") + + # Background 2D + if bg_dict: + for fx, fy in pairs: + try: + x = bg_dict[fx]; y = bg_dict[fy] + if x.size >= 10 and y.size >= 10: + f = plot_2d_single(x, y, fx, fy, "background", args.outdir, args.mass, args.Val, args.bins2d) + out_2d_bg.append(f) + except Exception as e: + sys.stderr.write(f"[warn] 2D background failed for {fx} vs {fy}: {e}\n") + + print("[done] 1D plots written:", len(out_1d)) + print("[done] 2D signal plots written:", len(out_2d_sig)) + print("[done] 2D background plots written:", len(out_2d_bg)) + if out_1d: + print("examples:") + for p in out_1d[: min(5, len(out_1d))]: + print(" ", p) + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_all_together_vals.py b/tools/simp-search-tools/plot-making/misc/plot_all_together_vals.py new file mode 100644 index 00000000..8454f7c9 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_all_together_vals.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +import argparse, os, re, glob, numpy as np, matplotlib.pyplot as plt, warnings, copy + +# ---------- helpers (from your current flow) ---------- +_float_line_re = re.compile(r'^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$') +_float_token_re = re.compile( + r'([+-]?(?:nan|inf))|([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)' +) + +def epsilon2_grid(N: int) -> np.ndarray: + j = np.arange(N, dtype=float) + return 10.0 ** (-4.0 - 6.0 * j / float(N)) + +def extract_last_numeric_list(text: str) -> np.ndarray: + # find last [...] span by counting brackets + spans, stack = [], [] + for i, ch in enumerate(text): + if ch == '[': + stack.append(i) + elif ch == ']': + if stack: + start = stack.pop() + spans.append((start, i+1)) + if not spans: + raise ValueError("No bracketed list found") + start, end = spans[-1] + payload = text[start:end] + + values = [] + for m in _float_token_re.finditer(payload): + token = m.group(1) or m.group(2) + if token is None: + continue + t = token.lower() + if t in ('nan', '+nan', '-nan'): + values.append(np.nan) + elif t in ('inf', '+inf'): + values.append(np.inf) + elif t == '-inf': + values.append(-np.inf) + else: + try: + values.append(float(token)) + except Exception: + pass + return np.asarray(values, dtype=float) + +def parse_combined_file(path: str): + with open(path, "r") as f: + text = f.read() + bg_frac = None + for ln in text.splitlines(): + s = ln.strip() + if not s: + continue + if _float_line_re.match(s): + bg_frac = float(s) + break + if bg_frac is None: + raise RuntimeError("Could not locate background fraction") + Z_arr = extract_last_numeric_list(text) + return None, float(bg_frac), Z_arr # keep signature parity + +def collect_mass_indices(base: str, L: int, val: int): + pat = os.path.join(base, f"combined_sig_I*_L{L}_V{val}.txt") + files = glob.glob(pat) + indices = [] + for fp in files: + m = re.search(rf"_I(\d+)_L{L}_V{val}\.txt$", fp) + if m: + indices.append(int(m.group(1))) + return sorted(set(indices)) + +def mass_from_index(I: int, L: int) -> float: + return 200.0/float(L) * float(I) + +def edges_from_centers(centers: np.ndarray) -> np.ndarray: + if centers.size == 1: + return np.array([centers[0]-1.0, centers[0]+1.0]) + mids = 0.5*(centers[1:] + centers[:-1]) + first = centers[0] - (mids[0] - centers[0]) + last = centers[-1] + (centers[-1] - mids[-1]) + return np.r_[first, mids, last] + +# ---------- main: overlay one iso-level across all V ---------- +def main(): + ap = argparse.ArgumentParser(description="Overlay Z=level contour for all V on one plot") + ap.add_argument("--base", default="/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/outputText") + ap.add_argument("--out", default="./sig_maps/iso_overlay_Z8.png") + ap.add_argument("--L", type=int, default=50) + ap.add_argument("--val-min", type=int, default=0) + ap.add_argument("--val-max", type=int, default=24) + ap.add_argument("--level", type=float, default=6.0, help="Significance level for the iso-contour") + ap.add_argument("--dpi", type=int, default=180) + args = ap.parse_args() + + # Gather a reference epsilon2 grid length by peeking at any available file + ref_eps = None + ref_masses = None + + plt.figure(figsize=(8.6, 6.0)) + styles = ['-', '--', '-.', ':'] + cmap = plt.cm.get_cmap('tab20') # enough distinct colors + + plotted_any = False + legend_entries = [] + + for val in range(args.val_min, args.val_max + 1): + mass_indices = collect_mass_indices(args.base, args.L, val) + if not mass_indices: + print(f"[warn] No files for V={val}, skipping") + continue + + # read one file to learn epsilon2 dimension + sample_path = os.path.join(args.base, f"combined_sig_I{mass_indices[0]}_L{args.L}_V{val}.txt") + try: + _, _, Z_sample = parse_combined_file(sample_path) + except Exception as e: + print(f"[warn] parse fail for V={val} sample {sample_path}: {e}") + continue + + N_eps = len(Z_sample) + eps2 = epsilon2_grid(N_eps) + + masses = np.array([mass_from_index(I, args.L) for I in mass_indices], dtype=float) + Zmat = np.full((N_eps, len(masses)), np.nan, dtype=float) + + for j, I in enumerate(mass_indices): + path = os.path.join(args.base, f"combined_sig_I{I}_L{args.L}_V{val}.txt") + try: + _, _, Z = parse_combined_file(path) + except Exception as e: + print(f"[warn] Failed to parse {path}: {e}") + continue + if len(Z) != N_eps: + print(f"[warn] Length mismatch in {path} (got {len(Z)} != {N_eps}); skipping this I") + continue + # cap crazy negatives a bit (like your existing) + Zmat[:, j] = [max(zz, -0.25) for zz in Z] + + Zmask = np.ma.masked_invalid(Zmat) + + # Build center grids for contour + Xc, Yc = np.meshgrid(masses, eps2) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=UserWarning) + cs = plt.contour( + Xc, Yc, Zmask.filled(np.nan), + levels=[args.level], + linewidths=1.8, + colors=[cmap((val - args.val_min) % cmap.N)], + linestyles=styles[(val - args.val_min) % len(styles)] + ) + + if len(cs.allsegs[0]) > 0: + plotted_any = True + # make a proxy line for legend + line = plt.Line2D([0], [0], + color=cmap((val - args.val_min) % cmap.N), + linestyle=styles[(val - args.val_min) % len(styles)], + linewidth=2.0) + legend_entries.append((line, f"V={val} (Z={args.level:g})")) + + if not plotted_any: + print("[error] No contours were drawn at the requested level. Try lowering --level or checking inputs.") + return + + # Axes/labels + plt.yscale('log') + plt.xlabel("reconstructed mass [MeV]") + plt.ylabel(r"$\epsilon^2$") + plt.title(f"Iso-significance overlay: Z = {args.level:g} for all V in [{args.val_min},{args.val_max}]") + + # tidy legend (cap to reasonable columns) + if legend_entries: + handles, labels = zip(*legend_entries) + ncols = 2 if len(legend_entries) <= 16 else 3 + plt.legend( + handles, + labels, + fontsize=9, + ncol=ncols, + frameon=True, + loc='lower left' # <-- moved here + ) + + os.makedirs(os.path.dirname(args.out), exist_ok=True) + plt.savefig(args.out, dpi=args.dpi, bbox_inches="tight") + plt.close() + print(f"[write] {args.out}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_ann_vs_bdt.py b/tools/simp-search-tools/plot-making/misc/plot_ann_vs_bdt.py new file mode 100644 index 00000000..84f51ff2 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_ann_vs_bdt.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 +import sys +import gc +import math +import argparse + +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt +import joblib +import torch +from torch import nn + +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + + +class ANNClassifier(nn.Module): + def __init__(self, in_features: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 64, bias=False), + nn.BatchNorm1d(64), + nn.LeakyReLU(), + nn.Linear(64, 1), + ) + + def forward(self, x): + return self.net(x) + + +def _as_float32_col(arr): + return np.asarray(arr, dtype=np.float32).reshape(-1, 1) + + +def ann_predict_score(model, scaler_mean, scaler_scale, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + X_scaled = (X_chunk.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled) + with torch.no_grad(): + scores[start:end] = torch.sigmoid(model(X_tensor)).cpu().numpy().ravel() + return scores + + +def bdt_predict_score_batched(model, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + scores[start:end] = model.predict_proba(X_chunk)[:, 1].astype(np.float32, copy=False) + return scores + + +def load_models(whichmass): + print(int(whichmass)) + ann_model_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/classifier_adv_2021_v9_pass5_run42QualCuts_{int(whichmass)}.pt" + ann_scaler_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_arrays_{int(whichmass)}.npz" + bdt_model_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer_31026_massdep/bdt_model_{int(whichmass)}.joblib" + + sys.modules['numpy._core'] = np.core + ann_scaler = np.load(ann_scaler_path) + ann_scaler_mean = ann_scaler["mean"].astype(np.float32) + ann_scaler_scale = ann_scaler["scale"].astype(np.float32) + + ann_model = ANNClassifier(in_features=34) + ann_state = torch.load(ann_model_path, map_location="cpu") + ann_model.load_state_dict(ann_state) + ann_model.eval() + + bdt_model = joblib.load(bdt_model_path) + return ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model + + +def common_branch_list(): + return [ + "vertex.invM_", "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_", + "ele_L1_iso_significance", "pos_L1_iso_significance", + ] + + +def build_ann_matrix_from_bg_arrays(arrays): + psum = ak.to_numpy(arrays["psum"]) + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + + feats = [ + _as_float32_col(vertex_pos["fX"]), + _as_float32_col(vertex_pos["fY"]), + _as_float32_col(vertex_pos["fZ"]), + _as_float32_col(psum), + ] + + ele_keys = [ + "n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", + "px_", "py_", "pz_", "chi2_", + "x_at_ecal_", "y_at_ecal_", "z_at_ecal_" + ] + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"pos.track_.{key}"]))) + + feats.append(_as_float32_col(ak.to_numpy(arrays["vertex.chi2_"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["pos_L1_iso_significance"]))) + return np.hstack(feats) + + +def build_bdt_matrix_from_bg_arrays(arrays): + psum = ak.to_numpy(arrays["psum"]) + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + + feats = [ + _as_float32_col(psum), + _as_float32_col(vertex_pos["fX"]), + _as_float32_col(vertex_pos["fY"]), + _as_float32_col(vertex_pos["fZ"]), + ] + + ele_keys = [ + "n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", + "px_", "py_", "pz_", "chi2_", + "x_at_ecal_", "y_at_ecal_", "z_at_ecal_" + ] + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(ak.to_numpy(arrays[f"pos.track_.{key}"]))) + + feats.append(_as_float32_col(ak.to_numpy(arrays["vertex.chi2_"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(ak.to_numpy(arrays["pos_L1_iso_significance"]))) + return np.hstack(feats) + + +def build_ann_matrix_from_sig_events(events): + feats = [ + _as_float32_col(np.asarray(events["vertex.pos_.fX"])), + _as_float32_col(np.asarray(events["vertex.pos_.fY"])), + _as_float32_col(np.asarray(events["vertex.pos_.fZ"])), + _as_float32_col(np.asarray(events["psum"])), + ] + + ele_keys = [ + "n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", + "px_", "py_", "pz_", "chi2_", + "x_at_ecal_", "y_at_ecal_", "z_at_ecal_" + ] + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"pos.track_.{key}"]))) + + feats.append(_as_float32_col(np.asarray(events["vertex.chi2_"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(np.asarray(events["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(np.asarray(events["pos_L1_iso_significance"]))) + return np.hstack(feats) + + +def build_bdt_matrix_from_sig_events(events): + feats = [ + _as_float32_col(np.asarray(events["psum"])), + _as_float32_col(np.asarray(events["vertex.pos_.fX"])), + _as_float32_col(np.asarray(events["vertex.pos_.fY"])), + _as_float32_col(np.asarray(events["vertex.pos_.fZ"])), + ] + + ele_keys = [ + "n_hits_", "d0_", "phi0_", "z0_", "tan_lambda_", + "px_", "py_", "pz_", "chi2_", + "x_at_ecal_", "y_at_ecal_", "z_at_ecal_" + ] + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"ele.track_.{key}"]))) + for key in ele_keys: + feats.append(_as_float32_col(np.asarray(events[f"pos.track_.{key}"]))) + + feats.append(_as_float32_col(np.asarray(events["vertex.chi2_"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_x_sig"]))) + feats.append(_as_float32_col(np.asarray(events["vtx_proj_y_sig"]))) + feats.append(_as_float32_col(np.asarray(events["ele_L1_iso_significance"]))) + feats.append(_as_float32_col(np.asarray(events["pos_L1_iso_significance"]))) + return np.hstack(feats) + + +def derive_l1l1_from_ak(hit_layers): + if hit_layers is None: + return None + hasL0 = ak.to_numpy(ak.any(hit_layers == 0, axis=-1)) + hasL1 = ak.to_numpy(ak.any(hit_layers == 1, axis=-1)) + return np.asarray(hasL0 & hasL1, dtype=bool) + + +def derive_l1l1_from_events(events, side): + flag_key = f"{side}.hasL0L1" + if flag_key in events: + return np.asarray(events[flag_key], dtype=bool) + layers_key = f"{side}.track_.hit_layers_" + if layers_key in events: + layers_ak = ak.Array(events[layers_key]) + hasL0 = np.asarray((layers_ak == 0).any(axis=1), dtype=bool) + hasL1 = np.asarray((layers_ak == 1).any(axis=1), dtype=bool) + return hasL0 & hasL1 + return None + + +def load_background(base, mass_mev): + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + raise RuntimeError("Background module/path unavailable.") + + bg_path = bg.BACKGROUND_PATH + with uproot.open(bg_path) as f: + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + raise RuntimeError("No TTree found in background file.") + arrays = tree.arrays(common_branch_list(), library="ak", how=dict) + + invM = ak.to_numpy(arrays["vertex.invM_"]) + psum = ak.to_numpy(arrays["psum"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + + ele_L1L1 = derive_l1l1_from_ak(arrays.get("ele.track_.hit_layers_")) + pos_L1L1 = derive_l1l1_from_ak(arrays.get("pos.track_.hit_layers_")) + if ele_L1L1 is None: + ele_L1L1 = np.ones_like(invM, dtype=bool) + if pos_L1L1 is None: + pos_L1L1 = np.ones_like(invM, dtype=bool) + l1l1_mask = ele_L1L1 & pos_L1L1 + + X_ann_bg = build_ann_matrix_from_bg_arrays(arrays) + X_bdt_bg = build_bdt_matrix_from_bg_arrays(arrays) + + return { + "psum": psum, + "proj_sig": proj_sig, + "l1l1_mask": l1l1_mask, + "X_ann": X_ann_bg, + "X_bdt": X_bdt_bg, + } + + +def load_signal(base): + events = base._events_cache(base._mass_key(1.8 * args.mass / 3.0)) + + s_psum = np.asarray(events["psum"]) + proj_sig = np.asarray(events["vtx_proj_sig"]) + + s_e_hasL0L1 = derive_l1l1_from_events(events, "ele") + s_p_hasL0L1 = derive_l1l1_from_events(events, "pos") + if s_e_hasL0L1 is None: + s_e_hasL0L1 = np.ones_like(s_psum, dtype=bool) + if s_p_hasL0L1 is None: + s_p_hasL0L1 = np.ones_like(s_psum, dtype=bool) + l1l1_mask = s_e_hasL0L1 & s_p_hasL0L1 + + X_ann_sig = build_ann_matrix_from_sig_events(events) + X_bdt_sig = build_bdt_matrix_from_sig_events(events) + + return { + "psum": s_psum, + "proj_sig": proj_sig, + "l1l1_mask": l1l1_mask, + "X_ann": X_ann_sig, + "X_bdt": X_bdt_sig, + } + + +def build_common_mask(psum, l1l1_mask, proj_sig, proj_fixed): + psum_mask = (psum >= 1.5) & (psum <= 3.0) + proj_mask = (proj_sig < proj_fixed) + return psum_mask & l1l1_mask & proj_mask + + +def main(): + parser = argparse.ArgumentParser( + description="Make a 2D ANN-vs-BDT score scatter plot on the same events." + ) + parser.add_argument("--mass", type=float, default=150.0) + parser.add_argument("--base-module", type=str, default="decayLength8sel") + parser.add_argument("--proj-fixed", type=float, default=1.0e9) + parser.add_argument("--dataset", choices=["background", "signal", "both"], default="background") + parser.add_argument("--outfile", type=str, default="ann_vs_bdt_scatter.png") + parser.add_argument("--max-points", type=int, default=0, + help="Optional cap on number of plotted points per class. 0 means plot all.") + parser.add_argument("--debug", action="store_true") + global args + args = parser.parse_args() + + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + whichmass = base._mass_key(1.8 * args.mass / 3.0) + ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model = load_models(whichmass) + + fig, ax = plt.subplots(figsize=(7, 7)) + + rng = np.random.default_rng(12345) + + if args.dataset in ["background", "both"]: + bg_data = load_background(base, args.mass) + bg_common = build_common_mask( + bg_data["psum"], + bg_data["l1l1_mask"], + bg_data["proj_sig"], + args.proj_fixed + ) + + ann_scores_bg = ann_predict_score( + ann_model, ann_scaler_mean, ann_scaler_scale, bg_data["X_ann"] + ) + bdt_scores_bg = bdt_predict_score_batched( + bdt_model, bg_data["X_bdt"] + ) + + x_bg = bdt_scores_bg[bg_common] + y_bg = ann_scores_bg[bg_common] + + if args.max_points > 0 and len(x_bg) > args.max_points: + idx = rng.choice(len(x_bg), size=args.max_points, replace=False) + x_bg = x_bg[idx] + y_bg = y_bg[idx] + + ax.scatter( + x_bg, + y_bg, + s=3, + alpha=0.35, + label=f"Background ({len(x_bg)})" + ) + + del bg_data, ann_scores_bg, bdt_scores_bg + gc.collect() + + if args.dataset in ["signal", "both"]: + sig_data = load_signal(base) + sig_common = build_common_mask( + sig_data["psum"], + sig_data["l1l1_mask"], + sig_data["proj_sig"], + args.proj_fixed + ) + + ann_scores_sig = ann_predict_score( + ann_model, ann_scaler_mean, ann_scaler_scale, sig_data["X_ann"] + ) + bdt_scores_sig = bdt_predict_score_batched( + bdt_model, sig_data["X_bdt"] + ) + + x_sig = bdt_scores_sig[sig_common] + y_sig = ann_scores_sig[sig_common] + + if args.max_points > 0 and len(x_sig) > args.max_points: + idx = rng.choice(len(x_sig), size=args.max_points, replace=False) + x_sig = x_sig[idx] + y_sig = y_sig[idx] + + ax.scatter( + x_sig, + y_sig, + s=5, + alpha=0.45, + label=f"Signal ({len(x_sig)})" + ) + + del sig_data, ann_scores_sig, bdt_scores_sig + gc.collect() + + ax.set_xlabel("BDT score") + ax.set_ylabel("ANN score") + ax.set_title(f"ANN vs BDT scores on same events, m = {args.mass:.0f} MeV") + ax.set_xlim(0.0, 1.0) + ax.set_ylim(0.0, 1.0) + ax.grid(True, alpha=0.3) + ax.legend(loc="best") + fig.tight_layout() + plt.savefig(args.outfile, dpi=200) + + if args.debug: + print(f"Saved plot to: {args.outfile}") + + +if __name__ == "__main__": + main() diff --git a/tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt.py b/tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt.py new file mode 100644 index 00000000..8d4f8116 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 +import os +import sys +import gc +import math +import argparse + +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt +import joblib +import torch +from torch import nn + +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +# ===== SHELL-PLOT NOTE START ===== +# This script uses the same ANN/BDT feature ordering as write_roc_overlay_all3_fixed_order.py. +# It does NOT subtract binned histograms numerically after the fact. +# Instead, for stability, it builds the nested retention masks directly and then plots the shell: +# shell(8e-5 -> 4e-5) = pass(8e-5) AND NOT pass(4e-5) +# shell(4e-5 -> 2e-5) = pass(4e-5) AND NOT pass(2e-5) +# This is equivalent to subtracting the retained populations when the tighter selection is nested. +# +# It makes 34 plots per shell range, using the union of the ANN feature set. For variables that are +# not present in the BDT feature set (the two L1 isolation significances), the BDT line is omitted. +# +# The default interpretation of the requested numbers is FRACTIONAL background retention: +# 0.00008, 0.00004, 0.00002 +# not percent units. If you truly meant percent units, use: +# --retentions 8e-7 4e-7 2e-7 +# ===== SHELL-PLOT NOTE END ===== + + +class ANNClassifier(nn.Module): + def __init__(self, in_features: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 64, bias=False), + nn.BatchNorm1d(64), + nn.LeakyReLU(), + nn.Linear(64, 1), + ) + + def forward(self, x): + return self.net(x) + + +ANN_FEATURE_NAMES = [ + "vertex_pos_x", + "vertex_pos_y", + "vertex_pos_z", + "psum", + "ele_track_n_hits", + "ele_track_d0", + "ele_track_phi0", + "ele_track_z0", + "ele_track_tan_lambda", + "ele_track_px", + "ele_track_py", + "ele_track_pz", + "ele_track_chi2", + "ele_track_x_at_ecal", + "ele_track_y_at_ecal", + "ele_track_z_at_ecal", + "pos_track_n_hits", + "pos_track_d0", + "pos_track_phi0", + "pos_track_z0", + "pos_track_tan_lambda", + "pos_track_px", + "pos_track_py", + "pos_track_pz", + "pos_track_chi2", + "pos_track_x_at_ecal", + "pos_track_y_at_ecal", + "pos_track_z_at_ecal", + "vertex_chi2", + "vtx_proj_sig", + "vtx_proj_x_sig", + "vtx_proj_y_sig", + "ele_L1_iso_significance", + "pos_L1_iso_significance", +] + +BDT_FEATURE_NAMES = [ + "psum", + "vertex_pos_x", + "vertex_pos_y", + "vertex_pos_z", + "ele_track_n_hits", + "ele_track_d0", + "ele_track_phi0", + "ele_track_z0", + "ele_track_tan_lambda", + "ele_track_px", + "ele_track_py", + "ele_track_pz", + "ele_track_chi2", + "ele_track_x_at_ecal", + "ele_track_y_at_ecal", + "ele_track_z_at_ecal", + "pos_track_n_hits", + "pos_track_d0", + "pos_track_phi0", + "pos_track_z0", + "pos_track_tan_lambda", + "pos_track_px", + "pos_track_py", + "pos_track_pz", + "pos_track_chi2", + "pos_track_x_at_ecal", + "pos_track_y_at_ecal", + "pos_track_z_at_ecal", + "vertex_chi2", + "vtx_proj_sig", + "vtx_proj_x_sig", + "vtx_proj_y_sig", +] + + +def _as_float32_col(arr): + return np.asarray(arr, dtype=np.float32).reshape(-1, 1) + + +def ann_predict_score(model, scaler_mean, scaler_scale, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + X_scaled = (X_chunk.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled) + with torch.no_grad(): + scores[start:end] = torch.sigmoid(model(X_tensor)).cpu().numpy().ravel() + return scores + + +def bdt_predict_score_batched(model, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + scores[start:end] = model.predict_proba(X_chunk)[:, 1].astype(np.float32, copy=False) + return scores + + +def load_models(whichmass): + ann_model_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/classifier_adv_2021_v9_pass5_run42QualCuts_{int(whichmass)}.pt" + ann_scaler_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_arrays_{int(whichmass)}.npz" + bdt_model_path = "/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer_2426/bdt_model.joblib" + + sys.modules['numpy._core'] = np.core + ann_scaler = np.load(ann_scaler_path) + ann_scaler_mean = ann_scaler["mean"].astype(np.float32) + ann_scaler_scale = ann_scaler["scale"].astype(np.float32) + + ann_model = ANNClassifier(in_features=34) + ann_state = torch.load(ann_model_path, map_location="cpu") + ann_model.load_state_dict(ann_state) + ann_model.eval() + + bdt_model = joblib.load(bdt_model_path) + return ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model + + +def common_branch_list(): + return [ + "vertex.invM_", "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_", + "ele_L1_iso_significance", "pos_L1_iso_significance", + ] + + +def derive_l1l1_from_ak(hit_layers): + if hit_layers is None: + return None + hasL0 = ak.to_numpy(ak.any(hit_layers == 0, axis=-1)) + hasL1 = ak.to_numpy(ak.any(hit_layers == 1, axis=-1)) + return np.asarray(hasL0 & hasL1, dtype=bool) + + +def load_background_arrays(): + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + raise RuntimeError("Background module/path unavailable.") + + bg_path = bg.BACKGROUND_PATH + with uproot.open(bg_path) as f: + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + raise RuntimeError("No TTree found in background file.") + arrays = tree.arrays(common_branch_list(), library="ak", how=dict) + return arrays + + +def build_feature_value_dict_from_bg_arrays(arrays): + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + out = { + "vertex_pos_x": np.asarray(vertex_pos["fX"]), + "vertex_pos_y": np.asarray(vertex_pos["fY"]), + "vertex_pos_z": np.asarray(vertex_pos["fZ"]), + "psum": np.asarray(ak.to_numpy(arrays["psum"])), + "ele_track_n_hits": np.asarray(ak.to_numpy(arrays["ele.track_.n_hits_"])), + "ele_track_d0": np.asarray(ak.to_numpy(arrays["ele.track_.d0_"])), + "ele_track_phi0": np.asarray(ak.to_numpy(arrays["ele.track_.phi0_"])), + "ele_track_z0": np.asarray(ak.to_numpy(arrays["ele.track_.z0_"])), + "ele_track_tan_lambda": np.asarray(ak.to_numpy(arrays["ele.track_.tan_lambda_"])), + "ele_track_px": np.asarray(ak.to_numpy(arrays["ele.track_.px_"])), + "ele_track_py": np.asarray(ak.to_numpy(arrays["ele.track_.py_"])), + "ele_track_pz": np.asarray(ak.to_numpy(arrays["ele.track_.pz_"])), + "ele_track_chi2": np.asarray(ak.to_numpy(arrays["ele.track_.chi2_"])), + "ele_track_x_at_ecal": np.asarray(ak.to_numpy(arrays["ele.track_.x_at_ecal_"])), + "ele_track_y_at_ecal": np.asarray(ak.to_numpy(arrays["ele.track_.y_at_ecal_"])), + "ele_track_z_at_ecal": np.asarray(ak.to_numpy(arrays["ele.track_.z_at_ecal_"])), + "pos_track_n_hits": np.asarray(ak.to_numpy(arrays["pos.track_.n_hits_"])), + "pos_track_d0": np.asarray(ak.to_numpy(arrays["pos.track_.d0_"])), + "pos_track_phi0": np.asarray(ak.to_numpy(arrays["pos.track_.phi0_"])), + "pos_track_z0": np.asarray(ak.to_numpy(arrays["pos.track_.z0_"])), + "pos_track_tan_lambda": np.asarray(ak.to_numpy(arrays["pos.track_.tan_lambda_"])), + "pos_track_px": np.asarray(ak.to_numpy(arrays["pos.track_.px_"])), + "pos_track_py": np.asarray(ak.to_numpy(arrays["pos.track_.py_"])), + "pos_track_pz": np.asarray(ak.to_numpy(arrays["pos.track_.pz_"])), + "pos_track_chi2": np.asarray(ak.to_numpy(arrays["pos.track_.chi2_"])), + "pos_track_x_at_ecal": np.asarray(ak.to_numpy(arrays["pos.track_.x_at_ecal_"])), + "pos_track_y_at_ecal": np.asarray(ak.to_numpy(arrays["pos.track_.y_at_ecal_"])), + "pos_track_z_at_ecal": np.asarray(ak.to_numpy(arrays["pos.track_.z_at_ecal_"])), + "vertex_chi2": np.asarray(ak.to_numpy(arrays["vertex.chi2_"])), + "vtx_proj_sig": np.asarray(ak.to_numpy(arrays["vtx_proj_sig"])), + "vtx_proj_x_sig": np.asarray(ak.to_numpy(arrays["vtx_proj_x_sig"])), + "vtx_proj_y_sig": np.asarray(ak.to_numpy(arrays["vtx_proj_y_sig"])), + "ele_L1_iso_significance": np.asarray(ak.to_numpy(arrays["ele_L1_iso_significance"])), + "pos_L1_iso_significance": np.asarray(ak.to_numpy(arrays["pos_L1_iso_significance"])), + } + return out + + +def build_ann_matrix_from_bg_arrays(arrays): + vals = build_feature_value_dict_from_bg_arrays(arrays) + feats = [_as_float32_col(vals[name]) for name in ANN_FEATURE_NAMES] + return np.hstack(feats) + + +def build_bdt_matrix_from_bg_arrays(arrays): + vals = build_feature_value_dict_from_bg_arrays(arrays) + feats = [_as_float32_col(vals[name]) for name in BDT_FEATURE_NAMES] + return np.hstack(feats) + + +def build_common_mask_from_bg_arrays(arrays, proj_fixed): + invM = ak.to_numpy(arrays["vertex.invM_"]) + psum = ak.to_numpy(arrays["psum"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + + ele_l1l1 = derive_l1l1_from_ak(arrays.get("ele.track_.hit_layers_")) + pos_l1l1 = derive_l1l1_from_ak(arrays.get("pos.track_.hit_layers_")) + if ele_l1l1 is None: + ele_l1l1 = np.ones_like(invM, dtype=bool) + if pos_l1l1 is None: + pos_l1l1 = np.ones_like(invM, dtype=bool) + l1l1_mask = ele_l1l1 & pos_l1l1 + + psum_mask = (psum >= 1.5) & (psum <= 3.0) + proj_mask = (proj_sig < proj_fixed) + return psum_mask & l1l1_mask & proj_mask + + +def score_threshold_for_retention(scores, base_mask, retention): + sel_scores = np.asarray(scores[base_mask], dtype=np.float64) + if sel_scores.size == 0: + raise RuntimeError("No background events survived the common preselection.") + if retention <= 0.0 or retention >= 1.0: + raise ValueError(f"Retention must be between 0 and 1, got {retention}") + + n = sel_scores.size + n_keep = max(1, int(np.ceil(retention * n))) + order = np.sort(sel_scores) + threshold = order[-n_keep] + realized = np.count_nonzero(sel_scores >= threshold) / float(n) + return float(threshold), float(realized), int(n_keep), int(n) + + +def shell_mask(scores, base_mask, thr_looser, thr_tighter): + pass_looser = base_mask & (scores >= thr_looser) + pass_tighter = base_mask & (scores >= thr_tighter) + return pass_looser & (~pass_tighter) + + +def robust_edges(x, bins): + x = np.asarray(x) + x = x[np.isfinite(x)] + if x.size == 0: + return np.linspace(0.0, 1.0, bins + 1) + xmin = np.min(x) + xmax = np.max(x) + if xmin == xmax: + pad = 0.5 if xmin == 0 else 0.05 * abs(xmin) + return np.linspace(xmin - pad, xmax + pad, bins + 1) + q1 = np.quantile(x, 0.01) + q99 = np.quantile(x, 0.99) + if np.isfinite(q1) and np.isfinite(q99) and q99 > q1: + return np.linspace(q1, q99, bins + 1) + return np.linspace(xmin, xmax, bins + 1) + + +def sanitize_filename(name): + return ''.join(c if c.isalnum() or c in ('_', '-', '.') else '_' for c in name) + + +def plot_shell_histogram(var_name, values, ann_mask, bdt_mask, ann_meta, bdt_meta, outpath, bins=60, density=True): + ann_vals = np.asarray(values[ann_mask]) + bdt_vals = np.asarray(values[bdt_mask]) + combined = np.concatenate([ann_vals[np.isfinite(ann_vals)], bdt_vals[np.isfinite(bdt_vals)]]) if (ann_vals.size + bdt_vals.size) > 0 else np.array([]) + edges = robust_edges(combined, bins) + + fig, ax = plt.subplots(figsize=(7.5, 5.5)) + any_plotted = False + + if ann_vals.size > 0: + ax.hist(ann_vals, bins=edges, histtype='step', linewidth=2.0, density=density, + label=(f"ANN shell, target {ann_meta['lo_target']:.1e}->{ann_meta['hi_target']:.1e}; " + f"realized {ann_meta['lo_realized']:.3e}->{ann_meta['hi_realized']:.3e}; " + f"N={ann_vals.size}")) + any_plotted = True + + if bdt_mask is not None and bdt_vals.size > 0: + ax.hist(bdt_vals, bins=edges, histtype='step', linewidth=2.0, density=density, + label=(f"BDT shell, target {bdt_meta['lo_target']:.1e}->{bdt_meta['hi_target']:.1e}; " + f"realized {bdt_meta['lo_realized']:.3e}->{bdt_meta['hi_realized']:.3e}; " + f"N={bdt_vals.size}")) + any_plotted = True + + ax.set_title(f"Background shell comparison: {var_name}") + ax.set_xlabel(var_name) + ax.set_ylabel("Density" if density else "Entries") + ax.grid(True, alpha=0.3) + if any_plotted: + ax.legend(fontsize=8) + fig.tight_layout() + plt.savefig(outpath, dpi=180) + plt.close(fig) + + +def main(): + ap = argparse.ArgumentParser(description="Plot ANN-vs-BDT background shell histograms between tiny retention bands.") + ap.add_argument("--mass", type=float, default=150.0) + ap.add_argument("--base-module", type=str, default="decayLength8sel") + ap.add_argument("--proj-fixed", type=float, default=1.0e9) + ap.add_argument("--retentions", nargs=3, type=float, default=[8e-5, 4e-5, 2e-5], + help="Three nested background retention targets, e.g. 8e-5 4e-5 2e-5") + ap.add_argument("--outdir", type=str, default="bg_shell_feature_plots_m150") + ap.add_argument("--bins", type=int, default=60) + ap.add_argument("--counts", action="store_true", + help="Plot counts instead of density-normalized histograms") + ap.add_argument("--debug", action="store_true") + args = ap.parse_args() + + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + if len(args.retentions) != 3: + raise ValueError("Please provide exactly three retentions, e.g. --retentions 8e-5 4e-5 2e-5") + r_loose, r_mid, r_tight = args.retentions + if not (r_loose > r_mid > r_tight > 0.0): + raise ValueError("Retentions must be strictly descending and positive, e.g. 8e-5 > 4e-5 > 2e-5") + + whichmass = base._mass_key(1.8 * args.mass / 3.0) + ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model = load_models(whichmass) + + arrays = load_background_arrays() + common_mask = build_common_mask_from_bg_arrays(arrays, args.proj_fixed) + values = build_feature_value_dict_from_bg_arrays(arrays) + + X_ann = build_ann_matrix_from_bg_arrays(arrays) + X_bdt = build_bdt_matrix_from_bg_arrays(arrays) + + ann_scores = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_ann) + bdt_scores = bdt_predict_score_batched(bdt_model, X_bdt) + + del X_ann, X_bdt + gc.collect() + + ann_thr_loose, ann_real_loose, ann_keep_loose, ann_nbase = score_threshold_for_retention(ann_scores, common_mask, r_loose) + ann_thr_mid, ann_real_mid, ann_keep_mid, _ = score_threshold_for_retention(ann_scores, common_mask, r_mid) + ann_thr_tight, ann_real_tight, ann_keep_tight, _ = score_threshold_for_retention(ann_scores, common_mask, r_tight) + + bdt_thr_loose, bdt_real_loose, bdt_keep_loose, bdt_nbase = score_threshold_for_retention(bdt_scores, common_mask, r_loose) + bdt_thr_mid, bdt_real_mid, bdt_keep_mid, _ = score_threshold_for_retention(bdt_scores, common_mask, r_mid) + bdt_thr_tight, bdt_real_tight, bdt_keep_tight, _ = score_threshold_for_retention(bdt_scores, common_mask, r_tight) + + ann_shell_loose_mid = shell_mask(ann_scores, common_mask, ann_thr_loose, ann_thr_mid) + ann_shell_mid_tight = shell_mask(ann_scores, common_mask, ann_thr_mid, ann_thr_tight) + bdt_shell_loose_mid = shell_mask(bdt_scores, common_mask, bdt_thr_loose, bdt_thr_mid) + bdt_shell_mid_tight = shell_mask(bdt_scores, common_mask, bdt_thr_mid, bdt_thr_tight) + + ranges = [ + ( + f"{r_loose:.1e}_to_{r_mid:.1e}", + ann_shell_loose_mid, + bdt_shell_loose_mid, + {"lo_target": r_loose, "hi_target": r_mid, "lo_realized": ann_real_loose, "hi_realized": ann_real_mid}, + {"lo_target": r_loose, "hi_target": r_mid, "lo_realized": bdt_real_loose, "hi_realized": bdt_real_mid}, + ), + ( + f"{r_mid:.1e}_to_{r_tight:.1e}", + ann_shell_mid_tight, + bdt_shell_mid_tight, + {"lo_target": r_mid, "hi_target": r_tight, "lo_realized": ann_real_mid, "hi_realized": ann_real_tight}, + {"lo_target": r_mid, "hi_target": r_tight, "lo_realized": bdt_real_mid, "hi_realized": bdt_real_tight}, + ), + ] + + os.makedirs(args.outdir, exist_ok=True) + summary_path = os.path.join(args.outdir, "shell_summary.txt") + with open(summary_path, "w") as sf: + sf.write("Background shell histogram summary\n") + sf.write(f"mass = {args.mass}\n") + sf.write(f"base module = {args.base_module}\n") + sf.write(f"proj_fixed = {args.proj_fixed}\n") + sf.write(f"common-preselection survivors = {np.count_nonzero(common_mask)}\n\n") + + sf.write("ANN thresholds\n") + sf.write(f" target {r_loose:.3e}: threshold = {ann_thr_loose:.8g}, realized = {ann_real_loose:.8g}, requested keep ~= {ann_keep_loose}/{ann_nbase}\n") + sf.write(f" target {r_mid:.3e}: threshold = {ann_thr_mid:.8g}, realized = {ann_real_mid:.8g}, requested keep ~= {ann_keep_mid}/{ann_nbase}\n") + sf.write(f" target {r_tight:.3e}: threshold = {ann_thr_tight:.8g}, realized = {ann_real_tight:.8g}, requested keep ~= {ann_keep_tight}/{ann_nbase}\n\n") + + sf.write("BDT thresholds\n") + sf.write(f" target {r_loose:.3e}: threshold = {bdt_thr_loose:.8g}, realized = {bdt_real_loose:.8g}, requested keep ~= {bdt_keep_loose}/{bdt_nbase}\n") + sf.write(f" target {r_mid:.3e}: threshold = {bdt_thr_mid:.8g}, realized = {bdt_real_mid:.8g}, requested keep ~= {bdt_keep_mid}/{bdt_nbase}\n") + sf.write(f" target {r_tight:.3e}: threshold = {bdt_thr_tight:.8g}, realized = {bdt_real_tight:.8g}, requested keep ~= {bdt_keep_tight}/{bdt_nbase}\n\n") + + sf.write("Shell populations\n") + sf.write(f" ANN {r_loose:.3e}->{r_mid:.3e}: {np.count_nonzero(ann_shell_loose_mid)}\n") + sf.write(f" ANN {r_mid:.3e}->{r_tight:.3e}: {np.count_nonzero(ann_shell_mid_tight)}\n") + sf.write(f" BDT {r_loose:.3e}->{r_mid:.3e}: {np.count_nonzero(bdt_shell_loose_mid)}\n") + sf.write(f" BDT {r_mid:.3e}->{r_tight:.3e}: {np.count_nonzero(bdt_shell_mid_tight)}\n") + + for range_tag, ann_mask, bdt_mask, ann_meta, bdt_meta in ranges: + subdir = os.path.join(args.outdir, sanitize_filename(range_tag)) + os.makedirs(subdir, exist_ok=True) + for var_name in ANN_FEATURE_NAMES: + outpath = os.path.join(subdir, sanitize_filename(var_name) + ".png") + # ===== PLOT NOTE ===== + # For the two isolation-significance variables, BDT has no corresponding trained input column. + # We still show the ANN shell for that variable, and omit the BDT line there. + bdt_mask_for_plot = bdt_mask if var_name in BDT_FEATURE_NAMES else None + plot_shell_histogram( + var_name=var_name, + values=values[var_name], + ann_mask=ann_mask, + bdt_mask=bdt_mask_for_plot, + ann_meta=ann_meta, + bdt_meta=bdt_meta, + outpath=outpath, + bins=args.bins, + density=(not args.counts), + ) + + if args.debug: + print(f"Common-preselection background count: {np.count_nonzero(common_mask)}") + print(f"ANN realized retentions: {ann_real_loose:.6e}, {ann_real_mid:.6e}, {ann_real_tight:.6e}") + print(f"BDT realized retentions: {bdt_real_loose:.6e}, {bdt_real_mid:.6e}, {bdt_real_tight:.6e}") + print(f"ANN shell counts: {np.count_nonzero(ann_shell_loose_mid)}, {np.count_nonzero(ann_shell_mid_tight)}") + print(f"BDT shell counts: {np.count_nonzero(bdt_shell_loose_mid)}, {np.count_nonzero(bdt_shell_mid_tight)}") + print(f"Saved plots under: {args.outdir}") + print(f"Summary file: {summary_path}") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt2.py b/tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt2.py new file mode 100644 index 00000000..2dd18088 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_bg_shell_feature_diffs_ann_bdt2.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +import os +import sys +import gc +import math +import argparse + +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt +import joblib +import torch +from torch import nn + +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write(f"[warn] Could not import bk_eff_selection (background module): {e}\n") + bg = None + +# ===== SHELL-PLOT NOTE START ===== +# This script uses the same ANN/BDT feature ordering as write_roc_overlay_all3_fixed_order.py. +# It does NOT subtract binned histograms numerically after the fact. +# Instead, for stability, it builds the nested retention masks directly and then plots the shell: +# shell(8e-5 -> 4e-5) = pass(8e-5) AND NOT pass(4e-5) +# shell(4e-5 -> 2e-5) = pass(4e-5) AND NOT pass(2e-5) +# This is equivalent to subtracting the retained populations when the tighter selection is nested. +# +# It makes 34 plots per shell range, using the union of the ANN feature set. For variables that are +# not present in the BDT feature set (the two L1 isolation significances), the BDT line is omitted. +# +# The default interpretation of the requested numbers is FRACTIONAL background retention: +# 0.00008, 0.00004, 0.00002 +# not percent units. If you truly meant percent units, use: +# --retentions 8e-7 4e-7 2e-7 +# ===== SHELL-PLOT NOTE END ===== + + +class ANNClassifier(nn.Module): + def __init__(self, in_features: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_features, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 264, bias=False), + nn.BatchNorm1d(264), + nn.LeakyReLU(), + nn.Linear(264, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 128, bias=False), + nn.BatchNorm1d(128), + nn.LeakyReLU(), + nn.Linear(128, 64, bias=False), + nn.BatchNorm1d(64), + nn.LeakyReLU(), + nn.Linear(64, 1), + ) + + def forward(self, x): + return self.net(x) + + +ANN_FEATURE_NAMES = [ + "vertex_pos_x", + "vertex_pos_y", + "vertex_pos_z", + "psum", + "ele_track_n_hits", + "ele_track_d0", + "ele_track_phi0", + "ele_track_z0", + "ele_track_tan_lambda", + "ele_track_px", + "ele_track_py", + "ele_track_pz", + "ele_track_chi2", + "ele_track_x_at_ecal", + "ele_track_y_at_ecal", + "ele_track_z_at_ecal", + "pos_track_n_hits", + "pos_track_d0", + "pos_track_phi0", + "pos_track_z0", + "pos_track_tan_lambda", + "pos_track_px", + "pos_track_py", + "pos_track_pz", + "pos_track_chi2", + "pos_track_x_at_ecal", + "pos_track_y_at_ecal", + "pos_track_z_at_ecal", + "vertex_chi2", + "vtx_proj_sig", + "vtx_proj_x_sig", + "vtx_proj_y_sig", + "ele_L1_iso_significance", + "pos_L1_iso_significance", +] + +BDT_FEATURE_NAMES = [ + "psum", + "vertex_pos_x", + "vertex_pos_y", + "vertex_pos_z", + "ele_track_n_hits", + "ele_track_d0", + "ele_track_phi0", + "ele_track_z0", + "ele_track_tan_lambda", + "ele_track_px", + "ele_track_py", + "ele_track_pz", + "ele_track_chi2", + "ele_track_x_at_ecal", + "ele_track_y_at_ecal", + "ele_track_z_at_ecal", + "pos_track_n_hits", + "pos_track_d0", + "pos_track_phi0", + "pos_track_z0", + "pos_track_tan_lambda", + "pos_track_px", + "pos_track_py", + "pos_track_pz", + "pos_track_chi2", + "pos_track_x_at_ecal", + "pos_track_y_at_ecal", + "pos_track_z_at_ecal", + "vertex_chi2", + "vtx_proj_sig", + "vtx_proj_x_sig", + "vtx_proj_y_sig", +] + + +def _as_float32_col(arr): + return np.asarray(arr, dtype=np.float32).reshape(-1, 1) + + +def ann_predict_score(model, scaler_mean, scaler_scale, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + X_scaled = (X_chunk.astype(np.float32) - scaler_mean) / scaler_scale + X_tensor = torch.from_numpy(X_scaled) + with torch.no_grad(): + scores[start:end] = torch.sigmoid(model(X_tensor)).cpu().numpy().ravel() + return scores + + +def bdt_predict_score_batched(model, X, batch_size=100000): + n = X.shape[0] + scores = np.empty(n, dtype=np.float32) + for start in range(0, n, batch_size): + end = min(start + batch_size, n) + X_chunk = np.nan_to_num(X[start:end], nan=0.0, posinf=0.0, neginf=0.0) + scores[start:end] = model.predict_proba(X_chunk)[:, 1].astype(np.float32, copy=False) + return scores + + +def load_models(whichmass): + ann_model_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/classifier_adv_2021_v9_pass5_run42QualCuts_{int(whichmass)}.pt" + ann_scaler_path = f"/sdf/group/hps/users/rodwyer1/run/reach_curves/annstuff/scaler_arrays_{int(whichmass)}.npz" + bdt_model_path = "/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/bdt_trainer_2426/bdt_model.joblib" + + sys.modules['numpy._core'] = np.core + ann_scaler = np.load(ann_scaler_path) + ann_scaler_mean = ann_scaler["mean"].astype(np.float32) + ann_scaler_scale = ann_scaler["scale"].astype(np.float32) + + ann_model = ANNClassifier(in_features=34) + ann_state = torch.load(ann_model_path, map_location="cpu") + ann_model.load_state_dict(ann_state) + ann_model.eval() + + bdt_model = joblib.load(bdt_model_path) + return ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model + + +def common_branch_list(): + return [ + "vertex.invM_", "psum", "vertex.pos_", + "ele.track_.n_hits_", "ele.track_.d0_", "ele.track_.phi0_", + "ele.track_.z0_", "ele.track_.tan_lambda_", "ele.track_.px_", + "ele.track_.py_", "ele.track_.pz_", "ele.track_.chi2_", + "ele.track_.x_at_ecal_", "ele.track_.y_at_ecal_", "ele.track_.z_at_ecal_", + "pos.track_.n_hits_", "pos.track_.d0_", "pos.track_.phi0_", + "pos.track_.z0_", "pos.track_.tan_lambda_", "pos.track_.px_", + "pos.track_.py_", "pos.track_.pz_", "pos.track_.chi2_", + "pos.track_.x_at_ecal_", "pos.track_.y_at_ecal_", "pos.track_.z_at_ecal_", + "vertex.chi2_", "vertex.invMerr_", + "vtx_proj_sig", "vtx_proj_x_sig", "vtx_proj_y_sig", + "ele.track_.hit_layers_", "pos.track_.hit_layers_", + "ele_L1_iso_significance", "pos_L1_iso_significance", + ] + + +def derive_l1l1_from_ak(hit_layers): + if hit_layers is None: + return None + hasL0 = ak.to_numpy(ak.any(hit_layers == 0, axis=-1)) + hasL1 = ak.to_numpy(ak.any(hit_layers == 1, axis=-1)) + return np.asarray(hasL0 & hasL1, dtype=bool) + + +def load_background_arrays(): + if bg is None or not hasattr(bg, "BACKGROUND_PATH"): + raise RuntimeError("Background module/path unavailable.") + + bg_path = bg.BACKGROUND_PATH + with uproot.open(bg_path) as f: + if hasattr(bg, "_open_first_tree"): + tree = bg._open_first_tree(f) + else: + keys = [k for k in f.keys() if ";" in k] + tree = f[keys[0]] if keys else None + if tree is None: + raise RuntimeError("No TTree found in background file.") + arrays = tree.arrays(common_branch_list(), library="ak", how=dict) + return arrays + + +def build_feature_value_dict_from_bg_arrays(arrays): + vertex_pos = ak.to_numpy(arrays["vertex.pos_"]) + out = { + "vertex_pos_x": np.asarray(vertex_pos["fX"]), + "vertex_pos_y": np.asarray(vertex_pos["fY"]), + "vertex_pos_z": np.asarray(vertex_pos["fZ"]), + "psum": np.asarray(ak.to_numpy(arrays["psum"])), + "ele_track_n_hits": np.asarray(ak.to_numpy(arrays["ele.track_.n_hits_"])), + "ele_track_d0": np.asarray(ak.to_numpy(arrays["ele.track_.d0_"])), + "ele_track_phi0": np.asarray(ak.to_numpy(arrays["ele.track_.phi0_"])), + "ele_track_z0": np.asarray(ak.to_numpy(arrays["ele.track_.z0_"])), + "ele_track_tan_lambda": np.asarray(ak.to_numpy(arrays["ele.track_.tan_lambda_"])), + "ele_track_px": np.asarray(ak.to_numpy(arrays["ele.track_.px_"])), + "ele_track_py": np.asarray(ak.to_numpy(arrays["ele.track_.py_"])), + "ele_track_pz": np.asarray(ak.to_numpy(arrays["ele.track_.pz_"])), + "ele_track_chi2": np.asarray(ak.to_numpy(arrays["ele.track_.chi2_"])), + "ele_track_x_at_ecal": np.asarray(ak.to_numpy(arrays["ele.track_.x_at_ecal_"])), + "ele_track_y_at_ecal": np.asarray(ak.to_numpy(arrays["ele.track_.y_at_ecal_"])), + "ele_track_z_at_ecal": np.asarray(ak.to_numpy(arrays["ele.track_.z_at_ecal_"])), + "pos_track_n_hits": np.asarray(ak.to_numpy(arrays["pos.track_.n_hits_"])), + "pos_track_d0": np.asarray(ak.to_numpy(arrays["pos.track_.d0_"])), + "pos_track_phi0": np.asarray(ak.to_numpy(arrays["pos.track_.phi0_"])), + "pos_track_z0": np.asarray(ak.to_numpy(arrays["pos.track_.z0_"])), + "pos_track_tan_lambda": np.asarray(ak.to_numpy(arrays["pos.track_.tan_lambda_"])), + "pos_track_px": np.asarray(ak.to_numpy(arrays["pos.track_.px_"])), + "pos_track_py": np.asarray(ak.to_numpy(arrays["pos.track_.py_"])), + "pos_track_pz": np.asarray(ak.to_numpy(arrays["pos.track_.pz_"])), + "pos_track_chi2": np.asarray(ak.to_numpy(arrays["pos.track_.chi2_"])), + "pos_track_x_at_ecal": np.asarray(ak.to_numpy(arrays["pos.track_.x_at_ecal_"])), + "pos_track_y_at_ecal": np.asarray(ak.to_numpy(arrays["pos.track_.y_at_ecal_"])), + "pos_track_z_at_ecal": np.asarray(ak.to_numpy(arrays["pos.track_.z_at_ecal_"])), + "vertex_chi2": np.asarray(ak.to_numpy(arrays["vertex.chi2_"])), + "vtx_proj_sig": np.asarray(ak.to_numpy(arrays["vtx_proj_sig"])), + "vtx_proj_x_sig": np.asarray(ak.to_numpy(arrays["vtx_proj_x_sig"])), + "vtx_proj_y_sig": np.asarray(ak.to_numpy(arrays["vtx_proj_y_sig"])), + "ele_L1_iso_significance": np.asarray(ak.to_numpy(arrays["ele_L1_iso_significance"])), + "pos_L1_iso_significance": np.asarray(ak.to_numpy(arrays["pos_L1_iso_significance"])), + } + return out + + +def build_ann_matrix_from_bg_arrays(arrays): + vals = build_feature_value_dict_from_bg_arrays(arrays) + feats = [_as_float32_col(vals[name]) for name in ANN_FEATURE_NAMES] + return np.hstack(feats) + + +def build_bdt_matrix_from_bg_arrays(arrays): + vals = build_feature_value_dict_from_bg_arrays(arrays) + feats = [_as_float32_col(vals[name]) for name in BDT_FEATURE_NAMES] + return np.hstack(feats) + + +def build_common_mask_from_bg_arrays(arrays, proj_fixed): + invM = ak.to_numpy(arrays["vertex.invM_"]) + psum = ak.to_numpy(arrays["psum"]) + proj_sig = ak.to_numpy(arrays["vtx_proj_sig"]) + + ele_l1l1 = derive_l1l1_from_ak(arrays.get("ele.track_.hit_layers_")) + pos_l1l1 = derive_l1l1_from_ak(arrays.get("pos.track_.hit_layers_")) + if ele_l1l1 is None: + ele_l1l1 = np.ones_like(invM, dtype=bool) + if pos_l1l1 is None: + pos_l1l1 = np.ones_like(invM, dtype=bool) + l1l1_mask = ele_l1l1 & pos_l1l1 + + psum_mask = (psum >= 1.5) & (psum <= 3.0) + proj_mask = (proj_sig < proj_fixed) + return psum_mask & l1l1_mask & proj_mask + + +def score_threshold_for_retention(scores, base_mask, retention): + sel_scores = np.asarray(scores[base_mask], dtype=np.float64) + if sel_scores.size == 0: + raise RuntimeError("No background events survived the common preselection.") + if retention <= 0.0 or retention >= 1.0: + raise ValueError(f"Retention must be between 0 and 1, got {retention}") + + n = sel_scores.size + n_keep = max(1, int(np.ceil(retention * n))) + order = np.sort(sel_scores) + threshold = order[-n_keep] + realized = np.count_nonzero(sel_scores >= threshold) / float(n) + return float(threshold), float(realized), int(n_keep), int(n) + + +def shell_mask(scores, base_mask, thr_looser, thr_tighter): + pass_looser = base_mask & (scores >= thr_looser) + pass_tighter = base_mask & (scores >= thr_tighter) + return pass_looser & (~pass_tighter) + + +def robust_edges(x, bins): + x = np.asarray(x) + x = x[np.isfinite(x)] + if x.size == 0: + return np.linspace(0.0, 1.0, bins + 1) + xmin = np.min(x) + xmax = np.max(x) + if xmin == xmax: + pad = 0.5 if xmin == 0 else 0.05 * abs(xmin) + return np.linspace(xmin - pad, xmax + pad, bins + 1) + q1 = np.quantile(x, 0.01) + q99 = np.quantile(x, 0.99) + if np.isfinite(q1) and np.isfinite(q99) and q99 > q1: + return np.linspace(q1, q99, bins + 1) + return np.linspace(xmin, xmax, bins + 1) + + +def sanitize_filename(name): + return ''.join(c if c.isalnum() or c in ('_', '-', '.') else '_' for c in name) + + +# ===== TAIL-PATCH NOTE ===== +# This helper now supports both the original shell labels and a single-threshold tail mode. +def plot_shell_histogram(var_name, values, ann_mask, bdt_mask, ann_meta, bdt_meta, outpath, bins=60, density=True): + ann_vals = np.asarray(values[ann_mask]) + bdt_vals = np.asarray(values[bdt_mask]) + combined = np.concatenate([ann_vals[np.isfinite(ann_vals)], bdt_vals[np.isfinite(bdt_vals)]]) if (ann_vals.size + bdt_vals.size) > 0 else np.array([]) + edges = robust_edges(combined, bins) + + fig, ax = plt.subplots(figsize=(7.5, 5.5)) + any_plotted = False + + # ===== TAIL-PATCH CHANGE START ===== + ann_mode = ann_meta.get("mode", "shell") + bdt_mode = bdt_meta.get("mode", "shell") if bdt_meta is not None else None + + if ann_vals.size > 0: + if ann_mode == "tail": + ann_label = (f"ANN tail, target {ann_meta['target']:.1e}; " + f"realized {ann_meta['realized']:.3e}; " + f"N={ann_vals.size}") + else: + ann_label = (f"ANN shell, target {ann_meta['lo_target']:.1e}->{ann_meta['hi_target']:.1e}; " + f"realized {ann_meta['lo_realized']:.3e}->{ann_meta['hi_realized']:.3e}; " + f"N={ann_vals.size}") + ax.hist(ann_vals, bins=edges, histtype='step', linewidth=2.0, density=density, label=ann_label) + any_plotted = True + + if bdt_mask is not None and bdt_vals.size > 0: + if bdt_mode == "tail": + bdt_label = (f"BDT tail, target {bdt_meta['target']:.1e}; " + f"realized {bdt_meta['realized']:.3e}; " + f"N={bdt_vals.size}") + else: + bdt_label = (f"BDT shell, target {bdt_meta['lo_target']:.1e}->{bdt_meta['hi_target']:.1e}; " + f"realized {bdt_meta['lo_realized']:.3e}->{bdt_meta['hi_realized']:.3e}; " + f"N={bdt_vals.size}") + ax.hist(bdt_vals, bins=edges, histtype='step', linewidth=2.0, density=density, label=bdt_label) + any_plotted = True + + title_mode = ann_mode if ann_mode is not None else "shell" + if title_mode == "tail": + ax.set_title(f"Background tail comparison: {var_name}") + else: + ax.set_title(f"Background shell comparison: {var_name}") + # ===== TAIL-PATCH CHANGE END ===== + ax.set_xlabel(var_name) + ax.set_ylabel("Density" if density else "Entries") + ax.grid(True, alpha=0.3) + if any_plotted: + ax.legend(fontsize=8) + fig.tight_layout() + plt.savefig(outpath, dpi=180) + plt.close(fig) + + +def main(): + ap = argparse.ArgumentParser(description="Plot ANN-vs-BDT background shell histograms between tiny retention bands, or a single retained tail.") + ap.add_argument("--mass", type=float, default=150.0) + ap.add_argument("--base-module", type=str, default="decayLength8sel") + ap.add_argument("--proj-fixed", type=float, default=1.0e9) + ap.add_argument("--retentions", nargs=3, type=float, default=[8e-5, 4e-5, 2e-5], + help="Three nested background retention targets, e.g. 8e-5 4e-5 2e-5") + # ===== TAIL-PATCH CHANGE START ===== + ap.add_argument("--tail-retention", type=float, default=None, + help="If set, make a single 34-plot comparison for the retained tail at this background fraction, e.g. 1e-4") + # ===== TAIL-PATCH CHANGE END ===== + ap.add_argument("--outdir", type=str, default="bg_shell_feature_plots_m150") + ap.add_argument("--bins", type=int, default=60) + ap.add_argument("--counts", action="store_true", + help="Plot counts instead of density-normalized histograms") + ap.add_argument("--debug", action="store_true") + args = ap.parse_args() + + try: + base = __import__(args.base_module) + except Exception as e: + sys.stderr.write(f"[error] Could not import signal base module '{args.base_module}': {e}\n") + sys.exit(1) + + # ===== TAIL-PATCH CHANGE START ===== + if args.tail_retention is None: + if len(args.retentions) != 3: + raise ValueError("Please provide exactly three retentions, e.g. --retentions 8e-5 4e-5 2e-5") + r_loose, r_mid, r_tight = args.retentions + if not (r_loose > r_mid > r_tight > 0.0): + raise ValueError("Retentions must be strictly descending and positive, e.g. 8e-5 > 4e-5 > 2e-5") + else: + if args.tail_retention <= 0.0 or args.tail_retention >= 1.0: + raise ValueError("--tail-retention must be between 0 and 1, e.g. 1e-4") + r_tail = args.tail_retention + # ===== TAIL-PATCH CHANGE END ===== + + whichmass = base._mass_key(1.8 * args.mass / 3.0) + ann_model, ann_scaler_mean, ann_scaler_scale, bdt_model = load_models(whichmass) + + arrays = load_background_arrays() + common_mask = build_common_mask_from_bg_arrays(arrays, args.proj_fixed) + values = build_feature_value_dict_from_bg_arrays(arrays) + + X_ann = build_ann_matrix_from_bg_arrays(arrays) + X_bdt = build_bdt_matrix_from_bg_arrays(arrays) + + ann_scores = ann_predict_score(ann_model, ann_scaler_mean, ann_scaler_scale, X_ann) + bdt_scores = bdt_predict_score_batched(bdt_model, X_bdt) + + del X_ann, X_bdt + gc.collect() + + # ===== TAIL-PATCH CHANGE START ===== + if args.tail_retention is None: + ann_thr_loose, ann_real_loose, ann_keep_loose, ann_nbase = score_threshold_for_retention(ann_scores, common_mask, r_loose) + ann_thr_mid, ann_real_mid, ann_keep_mid, _ = score_threshold_for_retention(ann_scores, common_mask, r_mid) + ann_thr_tight, ann_real_tight, ann_keep_tight, _ = score_threshold_for_retention(ann_scores, common_mask, r_tight) + + bdt_thr_loose, bdt_real_loose, bdt_keep_loose, bdt_nbase = score_threshold_for_retention(bdt_scores, common_mask, r_loose) + bdt_thr_mid, bdt_real_mid, bdt_keep_mid, _ = score_threshold_for_retention(bdt_scores, common_mask, r_mid) + bdt_thr_tight, bdt_real_tight, bdt_keep_tight, _ = score_threshold_for_retention(bdt_scores, common_mask, r_tight) + + ann_shell_loose_mid = shell_mask(ann_scores, common_mask, ann_thr_loose, ann_thr_mid) + ann_shell_mid_tight = shell_mask(ann_scores, common_mask, ann_thr_mid, ann_thr_tight) + bdt_shell_loose_mid = shell_mask(bdt_scores, common_mask, bdt_thr_loose, bdt_thr_mid) + bdt_shell_mid_tight = shell_mask(bdt_scores, common_mask, bdt_thr_mid, bdt_thr_tight) + + ranges = [ + ( + f"{r_loose:.1e}_to_{r_mid:.1e}", + ann_shell_loose_mid, + bdt_shell_loose_mid, + {"mode": "shell", "lo_target": r_loose, "hi_target": r_mid, "lo_realized": ann_real_loose, "hi_realized": ann_real_mid}, + {"mode": "shell", "lo_target": r_loose, "hi_target": r_mid, "lo_realized": bdt_real_loose, "hi_realized": bdt_real_mid}, + ), + ( + f"{r_mid:.1e}_to_{r_tight:.1e}", + ann_shell_mid_tight, + bdt_shell_mid_tight, + {"mode": "shell", "lo_target": r_mid, "hi_target": r_tight, "lo_realized": ann_real_mid, "hi_realized": ann_real_tight}, + {"mode": "shell", "lo_target": r_mid, "hi_target": r_tight, "lo_realized": bdt_real_mid, "hi_realized": bdt_real_tight}, + ), + ] + else: + ann_thr_tail, ann_real_tail, ann_keep_tail, ann_nbase = score_threshold_for_retention(ann_scores, common_mask, r_tail) + bdt_thr_tail, bdt_real_tail, bdt_keep_tail, bdt_nbase = score_threshold_for_retention(bdt_scores, common_mask, r_tail) + + ann_tail_mask = common_mask & (ann_scores >= ann_thr_tail) + bdt_tail_mask = common_mask & (bdt_scores >= bdt_thr_tail) + + ranges = [ + ( + f"tail_{r_tail:.1e}", + ann_tail_mask, + bdt_tail_mask, + {"mode": "tail", "target": r_tail, "realized": ann_real_tail}, + {"mode": "tail", "target": r_tail, "realized": bdt_real_tail}, + ), + ] + # ===== TAIL-PATCH CHANGE END ===== + + os.makedirs(args.outdir, exist_ok=True) + summary_path = os.path.join(args.outdir, "shell_summary.txt") + with open(summary_path, "w") as sf: + # ===== TAIL-PATCH FIX START ===== + if args.tail_retention is None: + sf.write("Background shell histogram summary\n") + else: + sf.write("Background tail histogram summary\n") + # ===== TAIL-PATCH FIX END ===== + sf.write(f"mass = {args.mass}\n") + sf.write(f"base module = {args.base_module}\n") + sf.write(f"proj_fixed = {args.proj_fixed}\n") + sf.write(f"common-preselection survivors = {np.count_nonzero(common_mask)}\n\n") + + sf.write("ANN thresholds\n") + # ===== TAIL-PATCH FIX START ===== + if args.tail_retention is None: + sf.write(f" target {r_loose:.3e}: threshold = {ann_thr_loose:.8g}, realized = {ann_real_loose:.8g}, requested keep ~= {ann_keep_loose}/{ann_nbase}\n") + sf.write(f" target {r_mid:.3e}: threshold = {ann_thr_mid:.8g}, realized = {ann_real_mid:.8g}, requested keep ~= {ann_keep_mid}/{ann_nbase}\n") + sf.write(f" target {r_tight:.3e}: threshold = {ann_thr_tight:.8g}, realized = {ann_real_tight:.8g}, requested keep ~= {ann_keep_tight}/{ann_nbase}\n\n") + else: + sf.write(f" target tail_retention={r_tail:.3e}: threshold = {ann_thr_tail:.8g}, realized = {ann_real_tail:.8g}, requested keep ~= {ann_keep_tail}/{ann_nbase}\n\n") + # ===== TAIL-PATCH FIX END ===== + + sf.write("BDT thresholds\n") + # ===== TAIL-PATCH FIX START ===== + if args.tail_retention is None: + sf.write(f" target {r_loose:.3e}: threshold = {bdt_thr_loose:.8g}, realized = {bdt_real_loose:.8g}, requested keep ~= {bdt_keep_loose}/{bdt_nbase}\n") + sf.write(f" target {r_mid:.3e}: threshold = {bdt_thr_mid:.8g}, realized = {bdt_real_mid:.8g}, requested keep ~= {bdt_keep_mid}/{bdt_nbase}\n") + sf.write(f" target {r_tight:.3e}: threshold = {bdt_thr_tight:.8g}, realized = {bdt_real_tight:.8g}, requested keep ~= {bdt_keep_tight}/{bdt_nbase}\n\n") + else: + sf.write(f" target tail_retention={r_tail:.3e}: threshold = {bdt_thr_tail:.8g}, realized = {bdt_real_tail:.8g}, requested keep ~= {bdt_keep_tail}/{bdt_nbase}\n\n") + # ===== TAIL-PATCH FIX END ===== + + # ===== TAIL-PATCH FIX START ===== + if args.tail_retention is None: + sf.write("Shell populations\n") + sf.write(f" ANN {r_loose:.3e}->{r_mid:.3e}: {np.count_nonzero(ann_shell_loose_mid)}\n") + sf.write(f" ANN {r_mid:.3e}->{r_tight:.3e}: {np.count_nonzero(ann_shell_mid_tight)}\n") + sf.write(f" BDT {r_loose:.3e}->{r_mid:.3e}: {np.count_nonzero(bdt_shell_loose_mid)}\n") + sf.write(f" BDT {r_mid:.3e}->{r_tight:.3e}: {np.count_nonzero(bdt_shell_mid_tight)}\n") + else: + sf.write("Tail populations\n") + sf.write(f" ANN >= {r_tail:.3e}: {np.count_nonzero(ann_tail_mask)}\n") + sf.write(f" BDT >= {r_tail:.3e}: {np.count_nonzero(bdt_tail_mask)}\n") + # ===== TAIL-PATCH FIX END ===== + for range_tag, ann_mask, bdt_mask, ann_meta, bdt_meta in ranges: + subdir = os.path.join(args.outdir, sanitize_filename(range_tag)) + os.makedirs(subdir, exist_ok=True) + for var_name in ANN_FEATURE_NAMES: + outpath = os.path.join(subdir, sanitize_filename(var_name) + ".png") + # ===== PLOT NOTE ===== + # For the two isolation-significance variables, BDT has no corresponding trained input column. + # We still show the ANN shell for that variable, and omit the BDT line there. + bdt_mask_for_plot = bdt_mask if var_name in BDT_FEATURE_NAMES else None + plot_shell_histogram( + var_name=var_name, + values=values[var_name], + ann_mask=ann_mask, + bdt_mask=bdt_mask_for_plot, + ann_meta=ann_meta, + bdt_meta=bdt_meta, + outpath=outpath, + bins=args.bins, + density=(not args.counts), + ) + + if args.debug: + print(f"Common-preselection background count: {np.count_nonzero(common_mask)}") + if args.tail_retention is None: + print(f"ANN realized retentions: {ann_real_loose:.6e}, {ann_real_mid:.6e}, {ann_real_tight:.6e}") + print(f"BDT realized retentions: {bdt_real_loose:.6e}, {bdt_real_mid:.6e}, {bdt_real_tight:.6e}") + print(f"ANN shell counts: {np.count_nonzero(ann_shell_loose_mid)}, {np.count_nonzero(ann_shell_mid_tight)}") + print(f"BDT shell counts: {np.count_nonzero(bdt_shell_loose_mid)}, {np.count_nonzero(bdt_shell_mid_tight)}") + else: + print(f"ANN realized retention: {ann_real_tail:.6e}") + print(f"BDT realized retention: {bdt_real_tail:.6e}") + print(f"ANN tail count: {np.count_nonzero(ann_tail_mask)}") + print(f"BDT tail count: {np.count_nonzero(bdt_tail_mask)}") + print(f"Saved plots under: {args.outdir}") + print(f"Summary file: {summary_path}") + + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_features_normed.py b/tools/simp-search-tools/plot-making/misc/plot_features_normed.py new file mode 100644 index 00000000..a5982fd3 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_features_normed.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +import argparse, os, sys, numpy as np +import matplotlib.pyplot as plt + +# ---- pull in background helpers like in plot_z0_signal_v_back.py ---- +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write("ERROR: Could not import bk_eff_selection as bg. Make sure it is on PYTHONPATH or in CWD.\n") + raise + +# ---- safe import for the signal "base" module (default: decayLength5sel) ---- +def import_signal_base(module_name="decayLength5sel"): + try: + base = __import__(module_name) + return base + except SystemExit as se: + sys.stderr.write( + "FATAL: Importing '{}' triggered SystemExit, probably from argparse at top level.\n" + "Please guard the CLI with if __name__ == '__main__'.\n".format(module_name) + ) + raise + except Exception: + import importlib.util + here = os.path.dirname(os.path.abspath(__file__)) + candidate = os.path.join(here, module_name + ".py") + if os.path.exists(candidate): + spec = importlib.util.spec_from_file_location(module_name, candidate) + mod = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(mod) + return mod + except SystemExit: + sys.stderr.write( + "FATAL: Executing '{}' still triggered argparse at import.\n" + "Please guard the CLI with if __name__ == '__main__' in that file.\n".format(candidate) + ) + raise + else: + raise + +# ---- import combine_zbi to reuse its normalization formulas ---- +try: + import combine_zbi as cz +except Exception as e: + sys.stderr.write("ERROR: Could not import combine_zbi.py. Place it next to this script or on PYTHONPATH.\n") + raise + +# ---------- utilities replicated/adapted from plot_z0_signal_v_back.py ---------- +def _ensure_dir(d): + if d and not os.path.isdir(d): + os.makedirs(d, exist_ok=True) + +def _compute_signal_weights(base, z_evt, epsilon, mass_mev, Val): + """Return per-event weights for signal using base.getProb(z, epsilon, mass, Val).""" + w = [] + for z in z_evt: + pr, pp = base.getProb(float(z), float(epsilon), float(mass_mev), int(Val)) + ww = float(pr) + float(pp) + if not np.isfinite(ww) or ww < 0.0: + ww = 0.0 + w.append(ww) + return np.asarray(w, dtype=float) + +def _load_background_arrays(mass_mev, Val): + import uproot + import awkward as ak + with uproot.open(bg.BACKGROUND_PATH) as f: + t = bg._open_first_tree(f) + arrays = t.arrays(bg.BRANCHES, library="ak") + invM = np.asarray(ak.to_numpy(arrays["vertex.invM_"])) + mwin = bg._mass_window_mask(invM, mass_mev) + events = {} + events["vertex.pos_.fZ"] = bg._extract_z_from_arrays(arrays) + events["psum"] = np.asarray(ak.to_numpy(arrays["psum"])) + events["vtx_proj_sig"] = np.asarray(ak.to_numpy(arrays["vtx_proj_sig"])) + tight = bg._tight_selection_mask(events, Val) + mask = mwin & tight + ele_z0 = np.asarray(ak.to_numpy(arrays["ele.track_.z0_"]))[mask] + pos_z0 = np.asarray(ak.to_numpy(arrays["pos.track_.z0_"]))[mask] + return ele_z0, pos_z0 + +def _load_signal_arrays(base, mass_mev, Val): + mkey = base._mass_key(mass_mev) + events = base._events_cache(mkey) + mask = base.tight_selection(events, Val) + ele_z0 = events["ele.track_.z0_"][mask] + pos_z0 = events["pos.track_.z0_"][mask] + z_evt = events["vertex.pos_.fZ"][mask] + return ele_z0, pos_z0, z_evt + +# ---------- combine_zbi normalization helpers ---------- +def _expected_counts_from_cz(II, L, Val): + """ + Reproduce combine_zbi main calculations to get arrays over epsilon slots: + S_arr (expected signal counts per eps-slot), + B_arr (expected background counts per eps-slot), + Z_arr (zbi per eps-slot). + """ + mass_mev = (240.0 / float(L)) * float(II) + x_gev = mass_mev / 1000.0 + + sig_path = cz.SIG_IN_TMPL.format(II=II, Val=Val) + bg_path = cz.BG_IN_TMPL.format(II=II, L=L, Val=Val) + + sig_frac = cz.parse_signal_array(sig_path) # array over epsilon slots + bg_frac = cz.read_bg_fraction(bg_path) # scalar + + m_val = cz.poly_m_of_x(x_gev) + N_b_massbin = cz.N_B_TOTAL_RECO * m_val * 10.0 + + S_arr = cz.N_B_TOTAL_RECO * sig_frac + B_arr = np.full_like(S_arr, N_b_massbin * bg_frac) + Z_arr = cz._zbi_wrapper(S_arr, B_arr) + return mass_mev, S_arr, B_arr, Z_arr + +# ---------- plotting ---------- +def _counts_hist(ax, data_sig, data_bg, bins, rrange, S_tot, B_tot, title, xlabel, weights_sig=None): + """ + Plot *count*-scaled histograms so that integrals match S_tot and B_tot. + """ + data_sig = np.asarray(data_sig) + data_bg = np.asarray(data_bg) + + # mask invalids + if weights_sig is not None: + weights_sig = np.asarray(weights_sig) + m_sig = np.isfinite(data_sig) & np.isfinite(weights_sig) + data_sig = data_sig[m_sig] + weights_sig = weights_sig[m_sig] + else: + m_sig = np.isfinite(data_sig) + data_sig = data_sig[m_sig] + weights_sig = np.ones_like(data_sig, dtype=float) + + data_bg = data_bg[np.isfinite(data_bg)] + + # auto-range if needed + if rrange is None: + both = np.concatenate([data_sig, data_bg]) if data_sig.size and data_bg.size else (data_sig if data_sig.size else data_bg) + if both.size: + q1, q99 = np.quantile(both, [0.005, 0.995]) + pad = 0.05 * (q99 - q1 + 1e-9) + rrange = (q1 - pad, q99 + pad) + else: + rrange = (-10.0, 10.0) + + # raw hists (counts per bin using current weights) + sig_counts, edges = np.histogram(data_sig, bins=bins, range=rrange, weights=weights_sig) + bg_counts, _ = np.histogram(data_bg, bins=bins, range=rrange) + + # scale so integrals match requested totals + sig_sum = sig_counts.sum() + bg_sum = bg_counts.sum() + sig_scale = (S_tot / sig_sum) if sig_sum > 0 else 0.0 + bg_scale = (B_tot / bg_sum) if bg_sum > 0 else 0.0 + + sig_counts *= sig_scale + bg_counts *= bg_scale + + # step plots + centers = 0.5*(edges[:-1] + edges[1:]) + ax.step(centers, sig_counts, where="mid", linewidth=1.8, label=f"signal (area={S_tot:.3g})") + ax.step(centers, bg_counts, where="mid", linewidth=1.8, label=f"background (area={B_tot:.3g})") + ax.set_title(title) + ax.set_xlabel(xlabel) + ax.set_ylabel("expected counts per bin") + ax.grid(True, alpha=0.30) + ax.legend() + +def main(): + ap = argparse.ArgumentParser(description="Plot feature histograms with combine_zbi normalization (expected S and B).") + ap.add_argument("--II", type=int, required=True, help="Index II (mass slot) used in reach curves.") + ap.add_argument("--L", type=int, required=True, help="L used in reach curves.") + ap.add_argument("--Val", type=int, default=25, help="Tight selection parameter. Default 25.") + ap.add_argument("--eps-index", type=int, required=True, help="Index into epsilon grid for normalization (from combine_zbi arrays).") + ap.add_argument("--epsilon", type=float, default=None, help="Optional epsilon for signal *shape* weighting (base.getProb).") + ap.add_argument("--base-module", type=str, default="decayLength5sel", help="Signal base module name. Default decayLength5sel.") + ap.add_argument("--bins", type=int, default=80, help="Number of bins. Default 80.") + ap.add_argument("--range", type=float, nargs=2, default=None, help="Explicit range: min max (in feature units).") + ap.add_argument("--outdir", type=str, default=".", help="Output directory.") + args = ap.parse_args() + + _ensure_dir(args.outdir) + + # expected S,B from combine_zbi logic + mass_mev, S_arr, B_arr, Z_arr = _expected_counts_from_cz(args.II, args.L, args.Val) + ei = int(args.eps_index) + if not (0 <= ei < len(S_arr)): + raise IndexError(f"--eps-index {ei} is out of bounds for arrays of length {len(S_arr)}") + S_tot = float(S_arr[ei]) + B_tot = float(B_arr[ei]) + Z_val = float(Z_arr[ei]) + + # load data + base = import_signal_base(args.base_module) + sig_ele, sig_pos, sig_z = _load_signal_arrays(base, mass_mev, args.Val) + bg_ele, bg_pos = _load_background_arrays(mass_mev, args.Val) + + # build feature arrays + sig_minabs = np.minimum(np.abs(sig_ele), np.abs(sig_pos)) + bg_minabs = np.minimum(np.abs(bg_ele), np.abs(bg_pos)) + + # signal per-event weights for shape if epsilon provided + if args.epsilon is not None: + weights_sig = _compute_signal_weights(base, sig_z, float(args.epsilon), mass_mev, args.Val) + eps_lab = f"eps={args.epsilon:g}" + else: + weights_sig = None + eps_lab = "eps=" + + # titles + head = f"mass={mass_mev:g} MeV, Val={args.Val}, eps-idx={ei}, {eps_lab}\nExpected S={S_tot:.3g}, B={B_tot:.3g}, ZBi={Z_val:.3g}" + + # 1) electron z0_ + fig1, ax1 = plt.subplots(figsize=(7.6, 4.6)) + _counts_hist(ax1, sig_ele, bg_ele, args.bins, tuple(args.range) if args.range else None, + S_tot, B_tot, title=head + "\nFeature: electron z0_", xlabel="ele.track_.z0_ [mm]", + weights_sig=weights_sig) + f1 = os.path.join(args.outdir, f"normed_ele_z0_{int(round(mass_mev))}MeV_V{args.Val}_eidx{ei}.png") + fig1.tight_layout(); fig1.savefig(f1, dpi=160, bbox_inches="tight"); plt.close(fig1) + + # 2) positron z0_ + fig2, ax2 = plt.subplots(figsize=(7.6, 4.6)) + _counts_hist(ax2, sig_pos, bg_pos, args.bins, tuple(args.range) if args.range else None, + S_tot, B_tot, title=head + "\nFeature: positron z0_", xlabel="pos.track_.z0_ [mm]", + weights_sig=weights_sig) + f2 = os.path.join(args.outdir, f"normed_pos_z0_{int(round(mass_mev))}MeV_V{args.Val}_eidx{ei}.png") + fig2.tight_layout(); fig2.savefig(f2, dpi=160, bbox_inches="tight"); plt.close(fig2) + + # 3) min(|z0_e|, |z0_p|) + fig3, ax3 = plt.subplots(figsize=(7.6, 4.6)) + _counts_hist(ax3, sig_minabs, bg_minabs, args.bins, tuple(args.range) if args.range else None, + S_tot, B_tot, title=head + "\nFeature: min(|z0_e|,|z0_p|)", xlabel="min(|ele.z0_|, |pos.z0_|) [mm]", + weights_sig=weights_sig) + f3 = os.path.join(args.outdir, f"normed_minabs_z0_{int(round(mass_mev))}MeV_V{args.Val}_eidx{ei}.png") + fig3.tight_layout(); fig3.savefig(f3, dpi=160, bbox_inches="tight"); plt.close(fig3) + + print("[ok] wrote:"); print(" ", f1); print(" ", f2); print(" ", f3) + print(f"[info] S={S_tot:.6g}, B={B_tot:.6g}, ZBi={Z_val:.6g}; arrays length = {len(S_arr)}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_psum_compare.py b/tools/simp-search-tools/plot-making/misc/plot_psum_compare.py new file mode 100644 index 00000000..49135226 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_psum_compare.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +import argparse +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt + +# ------------------------------------------------------------ +# Compute |p_e-| + |p_e+| (scalar-sum definition) +# ------------------------------------------------------------ +def psum_scalar(tree): + arr = tree.arrays([ + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", + ], library="ak") + + ex, ey, ez = (ak.to_numpy(arr["ele.track_.px_"]), + ak.to_numpy(arr["ele.track_.py_"]), + ak.to_numpy(arr["ele.track_.pz_"])) + px, py, pz = (ak.to_numpy(arr["pos.track_.px_"]), + ak.to_numpy(arr["pos.track_.py_"]), + ak.to_numpy(arr["pos.track_.pz_"])) + + pe = np.sqrt(ex*ex + ey*ey + ez*ez) + pp = np.sqrt(px*px + py*py + pz*pz) + return np.asarray(pe + pp, dtype=float) + + +# ------------------------------------------------------------ +# Compute |p_e- + p_e+| (vector-sum magnitude definition) +# ------------------------------------------------------------ +def psum_vector(tree): + arr = tree.arrays([ + "ele.track_.px_", "ele.track_.py_", "ele.track_.pz_", + "pos.track_.px_", "pos.track_.py_", "pos.track_.pz_", + ], library="ak") + + ex, ey, ez = (ak.to_numpy(arr["ele.track_.px_"]), + ak.to_numpy(arr["ele.track_.py_"]), + ak.to_numpy(arr["ele.track_.pz_"])) + px, py, pz = (ak.to_numpy(arr["pos.track_.px_"]), + ak.to_numpy(arr["pos.track_.py_"]), + ak.to_numpy(arr["pos.track_.pz_"])) + + sx = ex + px + sy = ey + py + sz = ez + pz + return np.asarray(np.sqrt(sx*sx + sy*sy + sz*sz), dtype=float) + + +# ------------------------------------------------------------ +# Main plotting routine +# ------------------------------------------------------------ +def main(): + ap = argparse.ArgumentParser(description="Compare psum definitions computed from momenta.") + ap.add_argument("--in", dest="infile", required=True, help="Input ROOT file (preselection style)") + ap.add_argument("--tree", default="preselection", help="Tree name (default: preselection)") + ap.add_argument("--bins", type=int, default=100, help="Histogram bins") + ap.add_argument("--xmin", type=float, default=0.0, help="x min") + ap.add_argument("--xmax", type=float, default=6.0, help="x max") + ap.add_argument("--out", default="psum_compare.png", help="Output PNG") + args = ap.parse_args() + + with uproot.open(args.infile) as f: + if args.tree in f: + t = f[args.tree] + else: + t = next(obj for _, obj in f.items() if getattr(obj, "classname", "").startswith("TTree")) + + # explicitly compute both, never use psum branch + psum1 = psum_scalar(t) + psum2 = psum_vector(t) + + # Remove NaN/inf + psum1 = psum1[np.isfinite(psum1)] + psum2 = psum2[np.isfinite(psum2)] + + plt.figure(figsize=(7.2,4.2)) + plt.hist(psum1, bins=args.bins, range=(args.xmin, args.xmax), + histtype="step", color="C0", label=r"$|\vec p_{e^-}|+|\vec p_{e^+}|$") + plt.hist(psum2, bins=args.bins, range=(args.xmin, args.xmax), + histtype="step", color="C3", label=r"$|\vec p_{e^-}+\vec p_{e^+}|$") + plt.xlabel(r"$p_{\mathrm{sum}}$ [GeV]") + plt.ylabel("Events") + plt.title("Comparison of psum definitions (computed directly)") + plt.legend() + plt.grid(alpha=0.3) + plt.tight_layout() + plt.savefig(args.out, dpi=140) + print(f"[write] {args.out}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex.py b/tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex.py new file mode 100644 index 00000000..dafe9559 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +import argparse +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt + +def extract_xyz_from_record(rec): + """Return (x,y,z) from an Awkward record with common field layouts.""" + flds = set(ak.fields(rec)) + # direct triplets + for trip in (("fX","fY","fZ"), ("X","Y","Z"), ("x","y","z")): + if all(c in flds for c in trip): + return (ak.to_numpy(rec[trip[0]]), + ak.to_numpy(rec[trip[1]]), + ak.to_numpy(rec[trip[2]])) + # nested coordinates container + for cname in ("fCoordinates","fCoord","coords","Coord","coord"): + if cname in flds: + c = rec[cname] + cf = set(ak.fields(c)) + for trip in (("fX","fY","fZ"), ("X","Y","Z"), ("x","y","z")): + if all(cc in cf for cc in trip): + return (ak.to_numpy(c[trip[0]]), + ak.to_numpy(c[trip[1]]), + ak.to_numpy(c[trip[2]])) + return None, None, None + +def load_p1p2(tree): + keys = set(tree.keys()) + needed = [k for k in ("vertex.p1_", "vertex.p2_") if k in keys] + if len(needed) < 2: + raise KeyError("vertex.p1_ and/or vertex.p2_ not found in the tree.") + arr = tree.arrays(needed, library="ak") + v1, v2 = arr["vertex.p1_"], arr["vertex.p2_"] + ex, ey, ez = extract_xyz_from_record(v1) # e- + px, py, pz = extract_xyz_from_record(v2) # e+ + if ex is None or px is None: + raise KeyError("Could not recognize x/y/z fields inside vertex.p1_/p2_.") + return ex, ey, ez, px, py, pz + +def main(): + ap = argparse.ArgumentParser(description="Overlay psum from vertex P1(e-) and P2(e+): |pe-|+|pe+| vs |pe-+pe+|.") + ap.add_argument("--in", dest="infile", required=True, help="Input ROOT file") + ap.add_argument("--tree", default="preselection", help="Tree name (default: preselection)") + ap.add_argument("--bins", type=int, default=100) + ap.add_argument("--xmin", type=float, default=0.0) + ap.add_argument("--xmax", type=float, default=6.0) + ap.add_argument("--out", default="psum_from_vertex.png") + ap.add_argument("--dump-keys", action="store_true", help="List branch keys and exit") + args = ap.parse_args() + + with uproot.open(args.infile) as f: + if args.tree in f: + t = f[args.tree] + else: + t = next(obj for _, obj in f.items() if getattr(obj, "classname", "").startswith("TTree")) + + if args.dump_keys: + print("\n".join(sorted(t.keys()))) + return + + ex, ey, ez, px, py, pz = load_p1p2(t) + + # scalar-sum definition: |pe-| + |pe+| + pe = np.sqrt(ex*ex + ey*ey + ez*ez) + pp = np.sqrt(px*px + py*py + pz*pz) + psum_scalar = pe + pp + + # vector-sum magnitude: |pe- + pe+| + sx, sy, sz = ex + px, ey + py, ez + pz + psum_vector = np.sqrt(sx*sx + sy*sy + sz*sz) + + # clean and plot + psum_scalar = psum_scalar[np.isfinite(psum_scalar)] + psum_vector = psum_vector[np.isfinite(psum_vector)] + + plt.figure(figsize=(7.2, 4.2)) + plt.hist(psum_scalar, bins=args.bins, range=(args.xmin, args.xmax), + histtype="step", label=r"$|\vec p_{e^-}|+|\vec p_{e^+}|$") + plt.hist(psum_vector, bins=args.bins, range=(args.xmin, args.xmax), + histtype="step", label=r"$|\vec p_{e^-}+\vec p_{e^+}|$") + plt.xlabel(r"$p_{\mathrm{sum}}$ [GeV]") + plt.ylabel("Events") + plt.title("psum from vertex P1(e−), P2(e+)") + plt.legend() + plt.grid(alpha=0.3) + plt.tight_layout() + plt.savefig(args.out, dpi=140) + print(f"[write] {args.out}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex_only.py b/tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex_only.py new file mode 100644 index 00000000..5c6648e3 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_psum_from_vertex_only.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +import argparse, re +import numpy as np +import uproot +import awkward as ak +import matplotlib.pyplot as plt + +def find_xyz_leaf_keys(all_keys, prefix): + """ + Find leaf names for x/y/z under a given prefix (e.g. 'vertex.p1_'). + Tries: + vertex.p1_.fX / fY / fZ + vertex.p1_.X / Y / Z + vertex.p1_.x / y / z + and one extra level like vertex.p1_.fCoordinates.fX (etc.) + Returns (kx, ky, kz) or raises KeyError. + """ + patterns = [ + (re.compile(rf"^{re.escape(prefix)}(?:fX|X|x)$"), + re.compile(rf"^{re.escape(prefix)}(?:fY|Y|y)$"), + re.compile(rf"^{re.escape(prefix)}(?:fZ|Z|z)$")), + (re.compile(rf"^{re.escape(prefix)}.*\.(?:fX|X|x)$"), + re.compile(rf"^{re.escape(prefix)}.*\.(?:fY|Y|y)$"), + re.compile(rf"^{re.escape(prefix)}.*\.(?:fZ|Z|z)$")), + ] + for rx, ry, rz in patterns: + kx = next((k for k in all_keys if rx.match(k)), None) + ky = next((k for k in all_keys if ry.match(k)), None) + kz = next((k for k in all_keys if rz.match(k)), None) + if kx and ky and kz: + return kx, ky, kz + raise KeyError(f"Could not find x/y/z leaves under '{prefix}'. Run with --dump-keys to inspect.") + +def load_vertex_ep_xyz_strict(tree): + """Strictly load e− (P1) and e+ (P2) 3-vectors from vertex.* leaves only.""" + keys = list(tree.keys()) + p1x, p1y, p1z = find_xyz_leaf_keys(keys, "vertex.p1_") + p2x, p2y, p2z = find_xyz_leaf_keys(keys, "vertex.p2_") + need = [p1x, p1y, p1z, p2x, p2y, p2z] + arr = tree.arrays(need, library="ak") + ex, ey, ez = ak.to_numpy(arr[p1x]), ak.to_numpy(arr[p1y]), ak.to_numpy(arr[p1z]) + px, py, pz = ak.to_numpy(arr[p2x]), ak.to_numpy(arr[p2y]), ak.to_numpy(arr[p2z]) + return ex, ey, ez, px, py, pz, f"vertex leaves ({p1x},{p1y},{p1z}) & ({p2x},{p2y},{p2z})" + +def main(): + ap = argparse.ArgumentParser(description="Overlay psum from vertex P1(e−) and P2(e+), no fallback.") + ap.add_argument("--in", dest="infile", required=True, help="Input ROOT file") + ap.add_argument("--tree", default="preselection", help="Tree name (default: preselection)") + ap.add_argument("--bins", type=int, default=100) + ap.add_argument("--xmin", type=float, default=0.0) + ap.add_argument("--xmax", type=float, default=6.0) + ap.add_argument("--out", default="psum_from_vertex_only.png") + ap.add_argument("--dump-keys", action="store_true", help="List branch keys and exit") + args = ap.parse_args() + + with uproot.open(args.infile) as f: + if args.tree in f: + t = f[args.tree] + else: + t = next(obj for _, obj in f.items() if getattr(obj, "classname", "").startswith("TTree")) + + if args.dump_keys: + print("\n".join(sorted(t.keys()))) + return + + ex, ey, ez, px, py, pz, src = load_vertex_ep_xyz_strict(t) + + # Compute both definitions from vertex P1/P2 components + pe = np.sqrt(ex*ex + ey*ey + ez*ez) + pp = np.sqrt(px*px + py*py + pz*pz) + psum_scalar = pe + pp + + sx, sy, sz = ex + px, ey + py, ez + pz + psum_vector = np.sqrt(sx*sx + sy*sy + sz*sz) + + # Clean and overlay + psum_scalar = psum_scalar[np.isfinite(psum_scalar)] + psum_vector = psum_vector[np.isfinite(psum_vector)] + + plt.figure(figsize=(7.2, 4.2)) + plt.hist(psum_scalar, bins=args.bins, range=(args.xmin, args.xmax), + histtype="step", label=r"$|\vec p_{e^-}|+|\vec p_{e^+}|$") + plt.hist(psum_vector, bins=args.bins, range=(args.xmin, args.xmax), + histtype="step", label=r"$|\vec p_{e^-}+\vec p_{e^+}|$") + plt.xlabel(r"$p_{\mathrm{sum}}$ [GeV]") + plt.ylabel("Events") + plt.title(f"psum from {src}") + plt.legend() + plt.grid(alpha=0.3) + plt.tight_layout() + plt.savefig(args.out, dpi=140) + print(f"[info] source: {src}") + print(f"[write] {args.out}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_sig_vs_mass.py b/tools/simp-search-tools/plot-making/misc/plot_sig_vs_mass.py new file mode 100644 index 00000000..df825765 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_sig_vs_mass.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# ======= plot_sig_vs_mass.py ======= +# Usage examples: +# python plot_sig_vs_mass.py --val 21 --eps2-value 1e-7 +# python plot_sig_vs_mass.py --val 21 --eps2-index 3 +# +# Notes: +# - Choose the interaction strength via --eps2-value (preferred) or --eps2-index. +# - Data files are expected as: combined_sig_I{I}_L{L}_V{val}.txt + +import argparse, os, re, glob, warnings +import numpy as np +import matplotlib.pyplot as plt + +def epsilon2_grid(N: int) -> np.ndarray: + j = np.arange(N, dtype=float) + return 10.0 ** (-4.0 - 6.0 * j / float(N)) + +_float_token_re = re.compile( + r'([+-]?(?:nan|inf))|([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)' +) +_float_line_re = re.compile(r'^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$') + +def extract_last_numeric_list(text: str) -> np.ndarray: + spans, stack = [], [] + for i, ch in enumerate(text): + if ch == '[': + stack.append(i) + elif ch == ']': + if stack: + start = stack.pop() + spans.append((start, i+1)) + if not spans: + raise ValueError("No bracketed list found") + start, end = spans[-1] + payload = text[start:end] + + values = [] + for m in _float_token_re.finditer(payload): + token = (m.group(1) or m.group(2)) + if token is None: + continue + t = token.lower() + if t in ('nan', '+nan', '-nan'): + values.append(np.nan) + elif t in ('inf', '+inf'): + values.append(np.inf) + elif t == '-inf': + values.append(-np.inf) + else: + try: + values.append(float(token)) + except Exception: + pass + return np.asarray(values, dtype=float) + +def parse_combined_file(path: str): + with open(path, "r") as f: + text = f.read() + bg_frac = None + for ln in text.splitlines(): + s = ln.strip() + if s and _float_line_re.match(s): + bg_frac = float(s) + break + if bg_frac is None: + raise RuntimeError("Could not locate background fraction as a bare float line") + Z_arr = extract_last_numeric_list(text) + return float(bg_frac), Z_arr + +def collect_mass_indices(base: str, L: int, val: int): + pat = os.path.join(base, f"combined_sig_I*_L{L}_V{val}.txt") + files = glob.glob(pat) + indices = [] + for fp in files: + m = re.search(r"_I(\d+)_L"+str(L)+r"_V"+str(val)+r"\.txt$", fp) + if m: + indices.append(int(m.group(1))) + return sorted(set(indices)) + +def mass_from_index(I: int, L: int) -> float: + return 200.0/float(L) * float(I) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--base", default="/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/outputText") + ap.add_argument("--out", default="sig_vs_mass.png") + ap.add_argument("--L", type=int, default=50) + ap.add_argument("--val", type=int, required=True) + # choose epsilon by value or index + ap.add_argument("--eps2-value", type=float, default=None, + help="Pick the ε² point closest to this value (preferred).") + ap.add_argument("--eps2-index", type=int, default=None, + help="Pick this ε² index (0-based) if --eps2-value not given.") + # bad-value handling + ap.add_argument("--insane-abs-threshold", type=float, default=1e6, + help="Mask any |Z| above this; set <=0 to disable. Default 1e6.") + args = ap.parse_args() + + mass_indices = collect_mass_indices(args.base, args.L, args.val) + if not mass_indices: + raise SystemExit(f"No files found for Val={args.val} at {args.base}") + + # probe first file to learn length of epsilon grid + probe = os.path.join(args.base, f"combined_sig_I{mass_indices[0]}_L{args.L}_V{args.val}.txt") + try: + _, Z0 = parse_combined_file(probe) + except Exception as e: + raise SystemExit(f"Failed to parse {probe}: {e}") + N_eps = len(Z0) + eps2 = epsilon2_grid(N_eps) + + # choose epsilon index + if args.eps2_value is not None: + k = int(np.argmin(np.abs(np.log10(eps2) - np.log10(args.eps2_value)))) + chosen_eps2 = eps2[k] + else: + if args.eps2_index is None: + raise SystemExit("Provide either --eps2-value or --eps2-index") + if not (0 <= args.eps2_index < N_eps): + raise SystemExit(f"--eps2-index must be in [0,{N_eps-1}]") + k = args.eps2_index + chosen_eps2 = eps2[k] + + masses = [] + Zvals = [] + + for I in mass_indices: + path = os.path.join(args.base, f"combined_sig_I{I}_L{args.L}_V{args.val}.txt") + try: + _, Z = parse_combined_file(path) + if len(Z) != N_eps: + print(f"[warn] length mismatch in {path}, skipping") + continue + z = float(Z[k]) + except Exception as e: + print(f"[warn] {path}: {e}") + continue + + # mask non-finite / absurd values + if not np.isfinite(z): + z = np.nan + elif args.insane_abs_threshold > 0 and abs(z) > args.insane_abs_threshold: + z = np.nan + + masses.append(mass_from_index(I, args.L)) + Zvals.append(z) + + masses = np.asarray(masses, dtype=float) + Zvals = np.asarray(Zvals, dtype=float) + + # sort by mass just in case + order = np.argsort(masses) + masses = masses[order] + Zvals = Zvals[order] + + # plot + plt.figure(figsize=(7.0, 4.0)) + finite = np.isfinite(Zvals) + if np.any(finite): + plt.plot(masses[finite], [max([z,-.5]) for z in Zvals[finite]], '-', lw=2) + plt.plot(masses[finite], [max([z,-.5]) for z in Zvals[finite]], 'o', ms=4) + # mark missing/bad as gaps (no line), but show markers if desired: + bad = ~finite + if np.any(bad): + plt.plot(masses[bad], np.zeros(np.count_nonzero(bad))*np.nan, 'o', alpha=0.0) # keep x extent + + plt.xlabel("reconstructed mass [MeV]") + plt.ylabel("significance") + plt.title(f"Significance vs mass @ ε²≈{chosen_eps2:.2e}, Val={args.val}") + plt.grid(True, alpha=0.25) + plt.tight_layout() + plt.savefig(args.out, dpi=160) + print(f"[write] {args.out}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_sig_vs_val.py b/tools/simp-search-tools/plot-making/misc/plot_sig_vs_val.py new file mode 100644 index 00000000..ec151649 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_sig_vs_val.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +# ======= plot_sig_vs_val.py ======= +# Usage examples: +# python plot_sig_vs_val.py --mass 80 --val-min 0 --val-max 25 --eps2-value 1e-7 +# python plot_sig_vs_val.py --mass-index 30 --val-min 0 --val-max 25 --eps2-index 3 +# +# Notes: +# - Choose a mass by MeV (rounded to nearest grid point) or by --mass-index. +# - Choose the interaction strength via --eps2-value (preferred) or --eps2-index. +# - Data files are expected as: combined_sig_I{I}_L{L}_V{val}.txt + +import argparse, os, re, glob, warnings +import numpy as np +import matplotlib.pyplot as plt + +def epsilon2_grid(N: int) -> np.ndarray: + j = np.arange(N, dtype=float) + return 10.0 ** (-4.0 - 6.0 * j / float(N)) + +_float_token_re = re.compile( + r'([+-]?(?:nan|inf))|([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)' +) +_float_line_re = re.compile(r'^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$') + +def extract_last_numeric_list(text: str) -> np.ndarray: + spans, stack = [], [] + for i, ch in enumerate(text): + if ch == '[': + stack.append(i) + elif ch == ']': + if stack: + start = stack.pop() + spans.append((start, i+1)) + if not spans: + raise ValueError("No bracketed list found") + start, end = spans[-1] + payload = text[start:end] + + values = [] + for m in _float_token_re.finditer(payload): + token = (m.group(1) or m.group(2)) + if token is None: + continue + t = token.lower() + if t in ('nan', '+nan', '-nan'): + values.append(np.nan) + elif t in ('inf', '+inf'): + values.append(np.inf) + elif t == '-inf': + values.append(-np.inf) + else: + try: + values.append(float(token)) + except Exception: + pass + return np.asarray(values, dtype=float) + +def parse_combined_file(path: str): + with open(path, "r") as f: + text = f.read() + bg_frac = None + for ln in text.splitlines(): + s = ln.strip() + if s and _float_line_re.match(s): + bg_frac = float(s) + break + if bg_frac is None: + raise RuntimeError("Could not locate background fraction as a bare float line") + Z_arr = extract_last_numeric_list(text) + return float(bg_frac), Z_arr + +def collect_mass_indices(base: str, L: int, val: int): + pat = os.path.join(base, f"combined_sig_I*_L{L}_V{val}.txt") + files = glob.glob(pat) + indices = [] + for fp in files: + m = re.search(r"_I(\d+)_L"+str(L)+r"_V"+str(val)+r"\.txt$", fp) + if m: + indices.append(int(m.group(1))) + return sorted(set(indices)) + +def mass_from_index(I: int, L: int) -> float: + return 200.0/float(L) * float(I) + +def index_from_mass(mass_mev: float, L: int) -> int: + # grid masses are 200/L * I for I=0..L + step = 200.0/float(L) + I = int(round(mass_mev / step)) + return max(0, min(I, L)) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--base", default="/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/outputText") + ap.add_argument("--out", default="sig_vs_val.png") + ap.add_argument("--L", type=int, default=50) + ap.add_argument("--val-min", type=int, default=0) + ap.add_argument("--val-max", type=int, default=25) + + # choose mass point by MeV or by index + grp = ap.add_mutually_exclusive_group(required=True) + grp.add_argument("--mass", type=float, help="Mass in MeV; will snap to nearest grid point.") + grp.add_argument("--mass-index", type=int, help="Mass grid index I (0..L).") + + # choose epsilon by value or index + ap.add_argument("--eps2-value", type=float, default=None, + help="Pick the ε² point closest to this value (preferred).") + ap.add_argument("--eps2-index", type=int, default=None, + help="Pick this ε² index (0-based) if --eps2-value not given.") + + # bad-value handling + ap.add_argument("--insane-abs-threshold", type=float, default=1e6, + help="Mask any |Z| above this; set <=0 to disable. Default 1e6.") + args = ap.parse_args() + + # pick the I index + if args.mass_index is not None: + I = args.mass_index + if not (0 <= I <= args.L): + raise SystemExit(f"--mass-index must be in [0,{args.L}]") + mass_mev = mass_from_index(I, args.L) + else: + I = index_from_mass(args.mass, args.L) + mass_mev = mass_from_index(I, args.L) + + # Discover epsilon grid from one available Val in range + probe_Z = None + probe_val = None + for val in range(args.val_min, args.val_max + 1): + path = os.path.join(args.base, f"combined_sig_I{I}_L{args.L}_V{val}.txt") + if os.path.exists(path): + try: + _, Z = parse_combined_file(path) + probe_Z = Z + probe_val = val + break + except Exception: + continue + if probe_Z is None: + raise SystemExit("Could not find any file to infer ε² grid. Check --base/--L/--val range and mass index.") + + N_eps = len(probe_Z) + eps2 = epsilon2_grid(N_eps) + + # choose epsilon index + if args.eps2_value is not None: + k = int(np.argmin(np.abs(np.log10(eps2) - np.log10(args.eps2_value)))) + chosen_eps2 = eps2[k] + else: + if args.eps2_index is None: + raise SystemExit("Provide either --eps2-value or --eps2-index") + if not (0 <= args.eps2_index < N_eps): + raise SystemExit(f"--eps2-index must be in [0,{N_eps-1}]") + k = args.eps2_index + chosen_eps2 = eps2[k] + + vals = [] + Zvals = [] + + for val in range(args.val_min, args.val_max + 1): + path = os.path.join(args.base, f"combined_sig_I{I}_L{args.L}_V{val}.txt") + if not os.path.exists(path): + print(f"[miss] {path}") + continue + try: + _, Z = parse_combined_file(path) + if len(Z) != N_eps: + print(f"[warn] length mismatch in {path}, skipping") + continue + z = float(Z[k]) + except Exception as e: + print(f"[warn] {path}: {e}") + continue + + # mask non-finite / absurd values + if not np.isfinite(z): + z = np.nan + elif args.insane_abs_threshold > 0 and abs(z) > args.insane_abs_threshold: + z = np.nan + + vals.append(val) + Zvals.append(z) + + if not vals: + raise SystemExit("No usable points in requested Val range.") + + vals = np.asarray(vals, dtype=float) + Zvals = np.asarray(Zvals, dtype=float) + + order = np.argsort(vals) + vals = vals[order] + Zvals = Zvals[order] + + # plot + plt.figure(figsize=(7.0, 4.0)) + finite = np.isfinite(Zvals) + if np.any(finite): + plt.plot(vals[finite], Zvals[finite], '-', lw=2) + plt.plot(vals[finite], Zvals[finite], 'o', ms=4) + bad = ~finite + if np.any(bad): + plt.plot(vals[bad], np.zeros(np.count_nonzero(bad))*np.nan, 'o', alpha=0.0) + + plt.xlabel("Val index") + plt.ylabel("significance") + plt.title(f"Significance vs Val @ mass≈{mass_mev:.1f} MeV, ε²≈{chosen_eps2:.2e} (I={I})") + plt.grid(True, alpha=0.25) + plt.tight_layout() + plt.savefig(args.out, dpi=160) + print(f"[write] {args.out}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_significance_surfaces.py b/tools/simp-search-tools/plot-making/misc/plot_significance_surfaces.py new file mode 100644 index 00000000..ec6a20ce --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_significance_surfaces.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +import argparse, os, re, ast, glob, numpy as np, matplotlib.pyplot as plt, math, warnings + +# -------- helpers -------- + +def epsilon2_grid(N: int) -> np.ndarray: + j = np.arange(N, dtype=float) + return 10.0 ** (-4.0 - 6.0 * j / float(N)) + +_float_token_re = re.compile( + r'([+-]?(?:nan|inf))|([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)' +) + +def extract_last_numeric_list(text: str) -> np.ndarray: + """Return the numeric list represented by the *last* [...] block in text. + Tolerates 'nan'/'inf' tokens and scientific notation. + """ + # find last [...] span by counting brackets + spans = [] + stack = [] + for i, ch in enumerate(text): + if ch == '[': + stack.append(i) + elif ch == ']': + if stack: + start = stack.pop() + spans.append((start, i+1)) + if not spans: + raise ValueError("No bracketed list found") + start, end = spans[-1] + payload = text[start:end] + + # Extract numeric tokens + values = [] + for m in _float_token_re.finditer(payload): + token = m.group(1) or m.group(2) + if token is None: + continue + t = token.lower() + if t in ('nan', '+nan', '-nan'): + values.append(np.nan) + elif t in ('inf', '+inf'): + values.append(np.inf) + elif t == '-inf': + values.append(-np.inf) + else: + try: + values.append(float(token)) + except Exception: + # ignore anything odd + pass + return np.asarray(values, dtype=float) + +_float_line_re = re.compile(r'^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$') + +def parse_combined_file(path: str): + """Robustly parse combined_sig file that may contain labels like 'Signal:' etc. + Returns: (sig_arr or None, bg_frac float, Z_arr). + """ + with open(path, "r") as f: + text = f.read() + + # background fraction: first line that's a bare float + bg_frac = None + for ln in text.splitlines(): + s = ln.strip() + if not s: + continue + if _float_line_re.match(s): + bg_frac = float(s) + break + if bg_frac is None: + raise RuntimeError("Could not locate background fraction as a bare float line") + + # significance array: last bracketed numeric list + Z_arr = extract_last_numeric_list(text) + + # signal array: first bracketed numeric list (optional) + sig_arr = None + try: + # Find first block + start = text.index('[') + depth = 0 + end = None + for i in range(start, len(text)): + if text[i] == '[': + depth += 1 + elif text[i] == ']': + depth -= 1 + if depth == 0: + end = i+1 + break + if end is not None: + # Parse like Z but from the first block + sig_arr = extract_last_numeric_list(text[start:end]) + except Exception: + pass + + return sig_arr, float(bg_frac), Z_arr + +def collect_mass_indices(base: str, L: int, val: int): + pat = os.path.join(base, f"combined_sig_I*_L{L}_V{val}.txt") + files = glob.glob(pat) + indices = [] + for fp in files: + m = re.search(r"_I(\d+)_L"+str(L)+r"_V"+str(val)+r"\.txt$", fp) + if m: + indices.append(int(m.group(1))) + return sorted(set(indices)) + +def mass_from_index(I: int, L: int) -> float: + return 200.0/float(L) * float(I) + +def edges_from_centers(centers: np.ndarray) -> np.ndarray: + if centers.size == 1: + return np.array([centers[0]-1.0, centers[0]+1.0]) + mids = 0.5*(centers[1:] + centers[:-1]) + first = centers[0] - (mids[0] - centers[0]) + last = centers[-1] + (centers[-1] - mids[-1]) + return np.r_[first, mids, last] + +# -------- main -------- + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--base", default="/sdf/group/hps/users/rodwyer1/run/reach_curves/optimization/outputText") + ap.add_argument("--outdir", default="./sig_maps") + ap.add_argument("--L", type=int, default=50) + ap.add_argument("--val-min", type=int, default=0) + ap.add_argument("--val-max", type=int, default=25) + args = ap.parse_args() + + os.makedirs(args.outdir, exist_ok=True) + + for val in range(args.val_min, args.val_max + 1): + mass_indices = collect_mass_indices(args.base, args.L, val) + if not mass_indices: + print(f"[warn] No files found for Val={val}. Skipping.") + continue + + sample_path = os.path.join(args.base, f"combined_sig_I{mass_indices[0]}_L{args.L}_V{val}.txt") + _, _, Z_sample = parse_combined_file(sample_path) + N_eps = len(Z_sample) + eps2 = epsilon2_grid(N_eps) + + masses = np.array([mass_from_index(I, args.L) for I in mass_indices], dtype=float) + Zmat = np.full((N_eps, len(masses)), np.nan, dtype=float) + + for j, I in enumerate(mass_indices): + path = os.path.join(args.base, f"combined_sig_I{I}_L{args.L}_V{val}.txt") + try: + _, _, Z = parse_combined_file(path) + except Exception as e: + print(f"[warn] Failed to parse {path}: {e}") + continue + if len(Z) != N_eps: + print(f"[warn] Length mismatch in {path} (got {len(Z)} != {N_eps}); skipping.") + continue + Zmat[:, j] = [max([zz,-.25]) for zz in Z] + + Zmask = np.ma.masked_invalid(Zmat) + # Matplotlib 3.1-compatible way to set 'bad' color: + import copy + cmap = copy.copy(plt.cm.get_cmap('viridis')) + cmap.set_bad('white') + + + plt.figure(figsize=(7.6, 5.2)) + x_edges = edges_from_centers(masses) + + logy = np.log10(eps2) + if len(eps2) > 1: + dy = np.diff(logy).mean() + y_edges = 10**(np.r_[logy[0]-0.5*dy, 0.5*(logy[1:]+logy[:-1]), logy[-1]+0.5*dy]) + else: + y_edges = np.array([eps2[0]/1.5, eps2[0]*1.5]) + + mesh = plt.pcolormesh(x_edges, y_edges, Zmask, shading='auto', cmap=cmap) + cbar = plt.colorbar(mesh) + cbar.set_label("significance") + + finite_Z = Zmask.compressed() + if finite_Z.size: + zmax = np.nanmax(finite_Z) + levels = np.arange(0, int(np.floor(zmax)) + 1, 1) + if len(levels) >= 1: + Xc, Yc = np.meshgrid(masses, eps2) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=UserWarning) + cs = plt.contour(Xc, Yc, Zmask.filled(np.nan), levels=levels, linewidths=0.8, colors='k') + plt.clabel(cs, inline=True, fontsize=8, fmt="%d") + + plt.yscale('log') + plt.xlabel("reconstructed mass [MeV]") + plt.ylabel(r"$\epsilon^2$") + plt.title(f"Significance vs mass, epsilon^2 (Val={val})") + + out_png = os.path.join(args.outdir, f"sig_map_Val{val}.png") + plt.savefig(out_png, dpi=160, bbox_inches="tight") + plt.close() + print(f"[write] {out_png}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_z0_dist.py b/tools/simp-search-tools/plot-making/misc/plot_z0_dist.py new file mode 100644 index 00000000..b378fb23 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_z0_dist.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +plot_z0_weighted.py + +Make epsilon-weighted histograms of ele.track_.z0_ and pos.track_.z0_ for events +passing the tight selection, at a given A' mass and epsilon. + +Weights per event are w(z) = prho(z) + pphi(z) from getProb(z, epsilon, mA, Val), +which already folds in realistic momentum distributions (via getAEnergy) and the +acceptance fraction F(z) (via getFrac with tight_selection). + +USAGE +----- +python3 plot_z0_weighted.py --mass 150 --epsilon 3e-5 --val 25 --out z0_150MeV_e3e-5.png +""" + +import argparse +import os +import sys +import numpy as np +import matplotlib.pyplot as plt + +# Import Rory's base module (must be in PYTHONPATH or the working directory) +try: + import decayLength5sel as base +except Exception as e: + sys.stderr.write("ERROR: Could not import 'decayLength51162025.py'. " + "Place this script in the same directory or add it to PYTHONPATH.\n") + raise + +def compute_weights(z_vals, epsilon, mass_mev, Val): + """ + For each z in z_vals, compute w(z) = prho(z) + pphi(z), + where prho, pphi are from base.getProb(z, epsilon, mass_mev, Val). + Returns a numpy array of weights aligned with z_vals. + """ + # Vectorized via list comprehension to preserve base.getProb behavior + weights = [] + for z in z_vals: + pr, pp = base.getProb(float(z), float(epsilon), float(mass_mev), int(Val)) + w = float(pr) + float(pp) + if not np.isfinite(w) or w < 0.0: + w = 0.0 + weights.append(w) + return np.asarray(weights, dtype=float) + +def main(): + parser = argparse.ArgumentParser(description="Weighted z0_ histograms for ele/pos after tight selection.") + parser.add_argument("--mass", type=float, required=True, help="A' mass in MeV (e.g. 150)") + parser.add_argument("--epsilon", type=float, required=True, help="Kinetic mixing (epsilon), not epsilon^2") + parser.add_argument("--val", type=int, default=25, help="Selection hyperparameter 'Val' (default=25)") + parser.add_argument("--bins", type=int, default=80, help="Number of bins for z0_ histograms (default=80)") + parser.add_argument("--range", type=float, nargs=2, default=None, + help="Explicit z0_ range as 'min max' (e.g. -20 20). If omitted, auto from data.") + parser.add_argument("--out", type=str, default=None, help="Output PNG filename. If omitted, an automatic name is used.") + args = parser.parse_args() + + # Resolve mass key and load cached events/denominator once + mkey = base._mass_key(args.mass) + events = base._events_cache(mkey) + + # Build tight selection mask + mask = base.tight_selection(events, args.val) + + # Extract arrays + z_evt = events["vertex.pos_.fZ"][mask] + ele_z0 = events.get("ele.track_.z0_")[mask] + pos_z0 = events.get("pos.track_.z0_")[mask] + + # Guard against NaNs/Infs + finite_mask = np.isfinite(z_evt) & np.isfinite(ele_z0) & np.isfinite(pos_z0) + z_evt = z_evt[finite_mask] + ele_z0 = ele_z0[finite_mask] + pos_z0 = pos_z0[finite_mask] + + if z_evt.size == 0: + sys.stderr.write("No events after selection. Nothing to plot.\n") + sys.exit(2) + + # Compute epsilon-weighted probability per event at its vertex z + weights = compute_weights(z_evt, args.epsilon, args.mass, args.val) + + # Histogram settings + if args.range is not None: + z0_min, z0_max = float(args.range[0]), float(args.range[1]) + else: + # Derive a robust range from the middle 99% to avoid extreme tails + both = np.concatenate([ele_z0, pos_z0]) + q1, q99 = np.quantile(both, [0.005, 0.995]) + pad = 0.05 * (q99 - q1 + 1e-9) + z0_min, z0_max = q1 - pad, q99 + pad + + # Make weighted histograms for ele and pos separately + hist_ele, edges = np.histogram(ele_z0, bins=args.bins, range=(z0_min, z0_max), weights=weights) + hist_pos, _ = np.histogram(pos_z0, bins=args.bins, range=(z0_min, z0_max), weights=weights) + centers = 0.5 * (edges[:-1] + edges[1:]) + + # Plot + plt.figure(figsize=(8, 4.6)) + plt.step(centers, hist_ele, where="mid", linewidth=1.6, label="electron z0_ (weighted)") + plt.step(centers, hist_pos, where="mid", linewidth=1.6, linestyle="--", label="positron z0_ (weighted)") + plt.xlabel("track z0_ [mm]") + plt.ylabel("weighted counts") + plt.title(f"Weighted z0_ after tight selection\nmA'≈{base._closest_available_mass(args.mass)} MeV, epsilon={args.epsilon:g}, Val={args.val}") + plt.grid(True, alpha=0.3) + plt.legend() + + # Output name + out_png = args.out + if not out_png: + safe_eps = str(args.epsilon).replace(".", "p").replace("-", "m") + out_png = f"z0_weighted_{int(round(args.mass))}MeV_eps{safe_eps}_Val{args.val}.png" + + plt.tight_layout() + plt.savefig(out_png, dpi=160, bbox_inches="tight") + print(f"[ok] wrote {out_png}") + print(f"[info] events used: {z_evt.size}") + print(f"[info] ele.hist sum={hist_ele.sum():.6g} pos.hist sum={hist_pos.sum():.6g}") + +if __name__ == "__main__": + main() + diff --git a/tools/simp-search-tools/plot-making/misc/plot_z0_signal_v_back.py b/tools/simp-search-tools/plot-making/misc/plot_z0_signal_v_back.py new file mode 100644 index 00000000..95c18272 --- /dev/null +++ b/tools/simp-search-tools/plot-making/misc/plot_z0_signal_v_back.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +plot_z0_signal_vs_bg.py + +Overlay unit-normalized z0_ histograms for signal vs background: + 1) electron track z0_ + 2) positron track z0_ + 3) min(|ele.track_.z0_|, |pos.track_.z0_|) + +Signal is epsilon-weighted using base.getProb(z, epsilon, mass, Val) at each +event's vertex z, then unit-normalized (density=True) for shape comparison. +Background is unit-normalized without epsilon weighting. + +Usage: + python3 plot_z0_signal_vs_bg.py --mass 150 --epsilon 3e-5 --Val 25 --bins 80 --outdir plots/ + # optional: --range -20 20 +""" + +import argparse +import os +import sys +import numpy as np +import matplotlib.pyplot as plt + +# --- import background helpers --- +try: + import bk_eff_selection as bg +except Exception as e: + sys.stderr.write("ERROR: Could not import bg_eff_fraction.py. Make sure it is in PYTHONPATH or the working directory.\n") + raise + +# --- import signal base safely --- +def import_signal_base(module_name="decayLength5sel"): + try: + base = __import__(module_name) + return base + except SystemExit as se: + sys.stderr.write( + "FATAL: Importing '{}' triggered SystemExit, probably from argparse at top level.\n" + "Please wrap the CLI in:\n" + " if __name__ == \"__main__\":\n" + " sys.exit(main())\n".format(module_name) + ) + raise + except Exception: + import importlib.util + here = os.path.dirname(os.path.abspath(__file__)) + candidate = os.path.join(here, module_name + ".py") + if os.path.exists(candidate): + spec = importlib.util.spec_from_file_location(module_name, candidate) + mod = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(mod) + return mod + except SystemExit: + sys.stderr.write( + "FATAL: Executing '{}' still triggered argparse at import.\n" + "Please guard the CLI with if __name__ == \"__main__\" in that file.\n".format(candidate) + ) + raise + else: + raise + +def _ensure_dir(d): + if d and not os.path.isdir(d): + os.makedirs(d, exist_ok=True) + +def _compute_signal_weights(base, z_evt, epsilon, mass_mev, Val): + w = [] + for z in z_evt: + pr, pp = base.getProb(float(z), float(epsilon), float(mass_mev), int(Val)) + ww = float(pr) + float(pp) + if not np.isfinite(ww) or ww < 0.0: + ww = 0.0 + w.append(ww) + return np.asarray(w, dtype=float) + +def _unit_hist(ax, data_sig, data_bg, bins, rrange, title, xlabel, weights_sig=None): + # Avoid NaNs/Infs + data_sig = np.asarray(data_sig) + data_bg = np.asarray(data_bg) + if weights_sig is not None: + weights_sig = np.asarray(weights_sig) + # align mask + m_sig = np.isfinite(data_sig) & np.isfinite(weights_sig) + data_sig = data_sig[m_sig] + weights_sig = weights_sig[m_sig] + else: + m_sig = np.isfinite(data_sig) + data_sig = data_sig[m_sig] + data_bg = data_bg[np.isfinite(data_bg)] + + # Auto range if not provided: robust middle 99% + if rrange is None: + if data_sig.size and data_bg.size: + both = np.concatenate([data_sig, data_bg]) + elif data_sig.size: + both = data_sig + elif data_bg.size: + both = data_bg + else: + both = np.array([]) + if both.size: + q1, q99 = np.quantile(both, [0.005, 0.995]) + pad = 0.05 * (q99 - q1 + 1e-9) + rrange = (q1 - pad, q99 + pad) + else: + rrange = (-10.0, 10.0) + + # Plot; density=True makes areas 1 regardless of total weight + ax.hist(data_sig, bins=bins, range=rrange, histtype="step", linewidth=1.6, + density=True, label="signal (eps-weighted shape)", weights=weights_sig) + ax.hist(data_bg, bins=bins, range=rrange, histtype="step", linewidth=1.6, + density=True, label="background") + ax.set_title(title) + ax.set_xlabel(xlabel) + ax.set_ylabel("unit-normalized density") + ax.grid(True, alpha=0.3) + ax.legend() + +def _load_background_arrays(mass_mev, Val): + import uproot + import awkward as ak + with uproot.open(bg.BACKGROUND_PATH) as f: + t = bg._open_first_tree(f) + arrays = t.arrays(bg.BRANCHES, library="ak") + invM = np.asarray(ak.to_numpy(arrays["vertex.invM_"])) + mwin = bg._mass_window_mask(invM, mass_mev) + events = {} + events["vertex.pos_.fZ"] = bg._extract_z_from_arrays(arrays) + events["psum"] = np.asarray(ak.to_numpy(arrays["psum"])) + events["vtx_proj_sig"] = np.asarray(ak.to_numpy(arrays["vtx_proj_sig"])) + tight = bg._tight_selection_mask(events, Val) + mask = mwin & tight + ele_z0 = np.asarray(ak.to_numpy(arrays["ele.track_.z0_"]))[mask] + pos_z0 = np.asarray(ak.to_numpy(arrays["pos.track_.z0_"]))[mask] + return ele_z0, pos_z0 + +def _load_signal_arrays(base, mass_mev, Val): + mkey = base._mass_key(mass_mev) + events = base._events_cache(mkey) + mask = base.tight_selection(events, Val) + ele_z0 = events["ele.track_.z0_"][mask] + pos_z0 = events["pos.track_.z0_"][mask] + z_evt = events["vertex.pos_.fZ"][mask] + return ele_z0, pos_z0, z_evt + +def main(): + ap = argparse.ArgumentParser(description="Overlay unit-normalized z0_ histograms for epsilon-weighted signal vs background.") + ap.add_argument("--mass", type=float, required=True, help="A' mass in MeV.") + ap.add_argument("--epsilon", type=float, required=True, help="epsilon (not epsilon^2). Used for signal weighting.") + ap.add_argument("--Val", type=int, default=25, help="Selection parameter for tight selection. Default 25.") + ap.add_argument("--bins", type=int, default=80, help="Number of bins. Default 80.") + ap.add_argument("--range", type=float, nargs=2, default=None, help="Explicit z0_ range: min max.") + ap.add_argument("--outdir", type=str, default=".", help="Output directory. Default current directory.") + args = ap.parse_args() + + _ensure_dir(args.outdir) + + base = import_signal_base() + mass_mev = float(args.mass) + Val = int(args.Val) + epsilon = float(args.epsilon) + + sig_ele, sig_pos, sig_z = _load_signal_arrays(base, mass_mev, Val) + bg_ele, bg_pos = _load_background_arrays(mass_mev, Val) + + # Build min(|z0_e|, |z0_p|) + sig_minabs = np.minimum(np.abs(sig_ele), np.abs(sig_pos)) + bg_minabs = np.minimum(np.abs(bg_ele), np.abs(bg_pos)) + + # Signal weights from epsilon, mass, z + weights_sig = _compute_signal_weights(base, sig_z, epsilon, mass_mev, Val) + + # 1) electron z0_ + fig1, ax1 = plt.subplots(figsize=(7.2, 4.4)) + _unit_hist(ax1, sig_ele, bg_ele, args.bins, tuple(args.range) if args.range else None, + title=f"electron z0_ (tight); mass {mass_mev:g} MeV, Val={Val}, eps={epsilon:g}", + xlabel="ele.track_.z0_ [mm]", weights_sig=weights_sig) + f1 = os.path.join(args.outdir, f"z0_ele_sig_vs_bg_{int(round(mass_mev))}MeV_V{Val}_eps{str(epsilon).replace('.','p')}.png") + fig1.tight_layout(); fig1.savefig(f1, dpi=160, bbox_inches="tight"); plt.close(fig1) + + # 2) positron z0_ + fig2, ax2 = plt.subplots(figsize=(7.2, 4.4)) + _unit_hist(ax2, sig_pos, bg_pos, args.bins, tuple(args.range) if args.range else None, + title=f"positron z0_ (tight); mass {mass_mev:g} MeV, Val={Val}, eps={epsilon:g}", + xlabel="pos.track_.z0_ [mm]", weights_sig=weights_sig) + f2 = os.path.join(args.outdir, f"z0_pos_sig_vs_bg_{int(round(mass_mev))}MeV_V{Val}_eps{str(epsilon).replace('.','p')}.png") + fig2.tight_layout(); fig2.savefig(f2, dpi=160, bbox_inches="tight"); plt.close(fig2) + + # 3) min(|z0_e|, |z0_p|) + fig3, ax3 = plt.subplots(figsize=(7.2, 4.4)) + _unit_hist(ax3, sig_minabs, bg_minabs, args.bins, tuple(args.range) if args.range else None, + title=f"min(|z0_e|,|z0_p|) (tight); mass {mass_mev:g} MeV, Val={Val}, eps={epsilon:g}", + xlabel="min(|ele.z0_|, |pos.z0_|) [mm]", weights_sig=weights_sig) + f3 = os.path.join(args.outdir, f"z0_minabs_sig_vs_bg_{int(round(mass_mev))}MeV_V{Val}_eps{str(epsilon).replace('.','p')}.png") + fig3.tight_layout(); fig3.savefig(f3, dpi=160, bbox_inches="tight"); plt.close(fig3) + + print("[ok] wrote:"); print(" ", f1); print(" ", f2); print(" ", f3) + print(f"[info] counts: sig(ele,pos) = {sig_ele.size}, {sig_pos.size} | bg(ele,pos) = {bg_ele.size}, {bg_pos.size}") + +if __name__ == "__main__": + main() +