Skip to content

Commit 0e836a1

Browse files
committed
backwards_ecal: add energy resolution benchmark
1 parent 8f14f31 commit 0e836a1

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed

benchmarks/backwards_ecal/backwards_ecal.org

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,122 @@ for energy in energies:
113113
))
114114
#+end_src
115115

116+
** Energy resolution
117+
118+
#+begin_src jupyter-python
119+
fig, axs = plt.subplots(2, 4, sharex=True, sharey=True, figsize=(15, 6))
120+
121+
fig.suptitle(PLOT_TITLE)
122+
123+
axs = np.ravel(np.array(axs))
124+
125+
sigmas_rel_FWHM_cb = {}
126+
fractions_below = {}
127+
128+
for ix, energy in enumerate(energies):
129+
for use_clusters in [False, True]:
130+
energy_value = float(energy.replace("GeV", "").replace("MeV", "e-3"))
131+
if use_clusters:
132+
clf_label = "leading cluster"
133+
else:
134+
clf_label = "sum all hits"
135+
def clf(events):
136+
if use_clusters:
137+
return ak.drop_none(ak.max(events["EcalEndcapNClusters.energy"], axis=-1)) / energy_value
138+
else:
139+
return ak.sum(events["EcalEndcapNRecHits.energy"], axis=-1) / energy_value
140+
e_pred = clf(e_eval[energy])
141+
142+
plt.sca(axs[ix])
143+
counts, bins, patches = plt.hist(e_pred, weights=np.full_like(e_pred, 1.0 / ak.num(e_pred, axis=0)), bins=np.linspace(0.01, 1.01, 101), label=rf"$e^-$ {clf_label}", hatch=None if use_clusters else r"xxx", alpha=0.8 if use_clusters else 1.)
144+
plt.title(f"{energy}")
145+
146+
e_over_p = (bins[1:] + bins[:-1]) / 2
147+
import scipy.stats
148+
def f(x, n, beta, m, loc, scale):
149+
return n * scipy.stats.crystalball.pdf(x, beta, m, loc, scale)
150+
p0 = (np.sum(counts[10:]), 2., 3., 0.95, 0.05)
151+
152+
try:
153+
import scipy.optimize
154+
par, pcov = scipy.optimize.curve_fit(f, e_over_p[5:], counts[5:], p0=p0, maxfev=10000)
155+
except RuntimeError:
156+
par = None
157+
plt.plot(e_over_p, f(e_over_p, *par), label=rf"Crystal Ball fit", color="tab:green" if use_clusters else "green", lw=0.8)
158+
159+
def summarize_fit(par):
160+
_, _, _, loc_cb, scale_cb = par
161+
# Calculate FWHM
162+
y_max = np.max(f(np.linspace(0., 1., 100), *par))
163+
f_prime = lambda x: f(x, *par) - y_max / 2
164+
x_plus, = scipy.optimize.root(f_prime, loc_cb + scale_cb).x
165+
x_minus, = scipy.optimize.root(f_prime, loc_cb - scale_cb).x
166+
color = "cyan" if use_clusters else "orange"
167+
plt.axvline(x_minus, ls="--", lw=0.75, color=patches[0].get_facecolor(), label=r"$\mu - $FWHM")
168+
plt.axvline(x_plus, ls=":", lw=0.75, color=patches[0].get_facecolor(), label=r"$\mu + $FWHM")
169+
fwhm = (x_plus - x_minus) / loc_cb
170+
sigma_rel_FWHM_cb = fwhm / 2 / np.sqrt(2 * np.log(2))
171+
172+
cutoff_x = loc_cb - fwhm
173+
fraction_below = np.sum(counts[e_over_p < cutoff_x]) / ak.num(e_pred, axis=0)
174+
175+
return sigma_rel_FWHM_cb, fraction_below
176+
177+
sigma_rel_FWHM_cb, fraction_below = summarize_fit(par)
178+
sigmas_rel_FWHM_cb.setdefault(clf_label, {})[energy] = sigma_rel_FWHM_cb
179+
fractions_below.setdefault(clf_label, {})[energy] = fraction_below
180+
181+
plt.legend()
182+
plt.xlabel("$E/p$", loc="right")
183+
plt.ylabel("Event yield", loc="top")
184+
185+
fig.savefig(output_dir / f"resolution_plots.pdf", bbox_inches="tight")
186+
plt.show()
187+
plt.close(fig)
188+
189+
plt.figure()
190+
energy_values = np.array([float(energy.replace("GeV", "").replace("MeV", "e-3")) for energy in energies])
191+
192+
for clf_label, sigma_rel_FWHM_cb in sigmas_rel_FWHM_cb.items():
193+
sigma_over_e = np.array([sigma_rel_FWHM_cb[energy] for energy in energies]) * 100 # convert to %
194+
195+
def f(energy, stochastic, constant):
196+
return np.sqrt((stochastic / np.sqrt(energy)) ** 2 + constant ** 2)
197+
cond = energy_values >= 0.5
198+
try:
199+
import scipy.optimize
200+
par, pcov = scipy.optimize.curve_fit(f, energy_values[cond], sigma_over_e[cond], maxfev=10000)
201+
except RuntimeError:
202+
par = None
203+
stochastic, constant = par
204+
205+
plt.plot(
206+
energy_values,
207+
sigma_over_e,
208+
marker=".",
209+
label=f"{clf_label}"
210+
)
211+
plt.plot(
212+
energy_values[cond],
213+
f(energy_values[cond], *par),
214+
color="black",
215+
ls="--",
216+
lw=0.5,
217+
label=f"{clf_label}, ${np.ceil(stochastic * 10) / 10:.1f}\% / \sqrt{{E}} \oplus {np.ceil(constant * 10) / 10:.1f}\%$",
218+
)
219+
plt.plot(
220+
energy_values,
221+
np.sqrt((1 / energy_values) ** 2 + (1 / np.sqrt(energy_values)) ** 2 + 1 ** 2),
222+
color="black", label=r"YR requirement $1\% / E \oplus 2.5\% / \sqrt{E} \oplus 1\%$",
223+
)
224+
plt.title(INPUT_PATH_FORMAT)
225+
plt.legend()
226+
plt.xlabel("Energy, GeV", loc="right")
227+
plt.ylabel(r"$\sigma_{E} / E$ derived from FWHM, %", loc="top")
228+
plt.savefig(output_dir / f"resolution.pdf", bbox_inches="tight")
229+
plt.show()
230+
#+end_src
231+
116232
** Pion rejection
117233

118234
#+begin_src jupyter-python

0 commit comments

Comments
 (0)