Skip to content

Commit e10a64c

Browse files
authored
feat(qa): support for charge studies and reheat idempotence (#439)
1 parent 15bac20 commit e10a64c

7 files changed

Lines changed: 567 additions & 3 deletions

File tree

bin/qtl-reheat

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ declare -A REHEAT_METHODS=(
1616

1717
# default options
1818
dataset=train
19+
declare -A modes
20+
for key in submit; do
21+
modes[$key]=false
22+
done
1923

2024
# usage
2125
sep="================================================================"
@@ -37,6 +41,8 @@ $(for key in "${!REHEAT_METHODS[@]}"; do printf "%24s %-11s %-s\n" "" "$key" "${
3741
OPTIONAL OPTIONS:
3842
-d [DATASET] unique name for this dataset
3943
default: $dataset
44+
--submit submit the slurm jobs, rather than just
45+
printing the \`sbatch\` command
4046
""" >&2
4147
}
4248
if [ $# -lt 1 ]; then
@@ -48,7 +54,7 @@ fi
4854
inputDir=""
4955
outputDir=""
5056
cmd=""
51-
while getopts "i:o:c:d:h" opt; do
57+
while getopts "i:o:c:d:h-:" opt; do
5258
case $opt in
5359
i) inputDir=$OPTARG ;;
5460
o) outputDir=$OPTARG ;;
@@ -61,6 +67,12 @@ while getopts "i:o:c:d:h" opt; do
6167
usage
6268
exit 101
6369
;;
70+
-)
71+
for key in "${!modes[@]}"; do
72+
[ "$key" == "$OPTARG" ] && modes[$OPTARG]=true && break
73+
done
74+
[ -z "${modes[$OPTARG]-}" ] && printError "unknown option --$OPTARG" && exit 100
75+
;;
6476
*) exit 100 ;;
6577
esac
6678
done
@@ -159,6 +171,12 @@ EOF
159171
echo """
160172
SLURM SCRIPT: $slurmScript
161173
JOB LIST: $jobList
162-
Now submitting!
163174
"""
164-
sbatch $slurmScript
175+
if ${modes['submit']}; then
176+
echo "Now submitting!"
177+
sbatch $slurmScript
178+
else
179+
echo """Run this command to submit:
180+
sbatch $slurmScript
181+
"""
182+
fi
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__
2+
*.png
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Charge Analysis
2+
3+
From Bhawani Singh
4+
5+
```bash
6+
./analyze HIPO_FILE
7+
```
8+
Produces PNG files comparing the DAQ-gated FC charge determined from
9+
- directly from the `RUN::scaler` bank
10+
- from the livetime and ungated charge, by multiplication
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
#!/usr/bin/env python3
2+
##################################################################################
3+
# ADAPTED FROM BHAWANI SINGH's ORIGINAL SCRIPT:
4+
# /w/hallb-scshelf2102/clas12/singh/Softwares/QADB_studies/python/main2.py
5+
##################################################################################
6+
import numpy as np
7+
import os
8+
import sys
9+
import logging
10+
from glob import glob
11+
import matplotlib.pyplot as plt
12+
import hipolib
13+
14+
# Configure logging
15+
logging.basicConfig(
16+
level=logging.INFO,
17+
format='%(asctime)s - %(levelname)s - %(message)s'
18+
)
19+
logger = logging.getLogger(__name__)
20+
21+
# plt.style.use('seaborn-darkgrid')
22+
23+
def main():
24+
25+
if len(sys.argv) != 2:
26+
print(f'USAGE: {sys.argv[0]} [HIPO_FILE]')
27+
exit(2)
28+
hipo_file = sys.argv[1]
29+
30+
hipo_prefix = os.getenv('HIPO')
31+
if hipo_prefix == None:
32+
raise ValueError("HIPO env var not set")
33+
34+
logger.info(f"Processing file: {hipo_file}")
35+
36+
reader = hipolib.hreader(f'{hipo_prefix}/lib')
37+
reader.open_with_tag(hipo_file, 1) # filter by tag at open time
38+
reader.define('RUN::config')
39+
reader.define('RUN::scaler')
40+
41+
timestamps, fcups, fcupgateds, live_times = [], [], [], []
42+
43+
counter = 0
44+
while reader.next():
45+
if counter % 10000 == 0 and counter > 0:
46+
logger.info(f'Processing event # {counter}')
47+
counter += 1
48+
49+
if reader.getSize('RUN::config') == 0 or reader.getSize('RUN::scaler') == 0:
50+
# logger.warning(f"Skipping empty bank at event {counter}")
51+
continue
52+
53+
timestamp = reader.getEntry('RUN::config', 'timestamp')
54+
fcup = reader.getEntry('RUN::scaler', 'fcup')
55+
fcupgated = reader.getEntry('RUN::scaler', 'fcupgated')
56+
live_time = reader.getEntry('RUN::scaler', 'livetime')
57+
58+
timestamps.append(timestamp[0])
59+
fcups.append(fcup[0])
60+
fcupgateds.append(fcupgated[0])
61+
live_times.append(live_time[0])
62+
63+
logger.info(f"Processed {counter} events.")
64+
65+
# Sort data by timestamps
66+
sorted_data = sorted(zip(timestamps, fcups, fcupgateds, live_times))
67+
if not sorted_data:
68+
raise ValueError("No data to plot.")
69+
70+
timestamps, fcups, fcupgateds, live_times = zip(*sorted_data)
71+
72+
timestamps = np.array(timestamps)
73+
fcups = np.array(fcups)
74+
fcupgateds = np.array(fcupgateds)
75+
live_times = np.array(live_times)
76+
77+
run_number = os.path.splitext(os.path.basename(hipo_file))[0]
78+
79+
# ---------- Plot 1: Per-event data ----------
80+
# ---------- Plot 1: Per-event data ----------
81+
fig1, axs1 = plt.subplots(2, 2, figsize=(14, 8))
82+
fig1.suptitle(f'Run {run_number} - Event-Level Detector Data', fontsize=16)
83+
84+
plots1 = [
85+
(axs1[0, 0], fcups, 'FCUP', 'FCUP vs Timestamp', 'darkgreen', 'line'),
86+
(axs1[0, 1], fcupgateds, 'FCUP Gated', 'FCUP Gated vs Timestamp', 'darkorange', 'line'),
87+
(axs1[1, 0], live_times, 'Live Time', 'Live Time vs Timestamp', 'purple', 'scatter'),
88+
(axs1[1, 1], fcups * live_times, 'FCUP × Live Time', 'FCUP × Live Time vs Timestamp', 'steelblue', 'line'),
89+
]
90+
91+
for ax, data, label, title, color, style in plots1:
92+
if style == 'line':
93+
ax.plot(timestamps, data, label=label, color=color, linewidth=1.5)
94+
elif style == 'scatter':
95+
ax.scatter(timestamps, data, label=label, color=color, s=10, alpha=0.7)
96+
97+
ax.set_title(title, fontsize=12)
98+
ax.set_xlabel('Timestamp', fontsize=10)
99+
ax.set_ylabel(label, fontsize=10)
100+
ax.legend(fontsize=9)
101+
ax.grid(True, linestyle='--', alpha=0.6)
102+
ax.tick_params(axis='both', labelsize=9)
103+
104+
fig1.tight_layout(rect=[0, 0.03, 1, 0.95])
105+
fig1.savefig(f'fcup_vs_timestamp_{run_number}.png', bbox_inches='tight', dpi=300)
106+
plt.close(fig1)
107+
# ---------- Compute Chunked FCUP Gated with neighbor handling ----------
108+
chunk_size = 2000
109+
num_chunks = len(timestamps) // chunk_size
110+
111+
chunk_caseA, chunk_caseB, chunk_caseC, chunk_default = [], [], [], []
112+
cum_caseA, cum_caseB, cum_caseC, cum_default = [], [], [], []
113+
chunk_indices, skipped_counts = [], []
114+
115+
runA, runB, runC, runDef = 0, 0, 0, 0
116+
total_skipped = 0
117+
118+
corrected_livetimes_A = []
119+
corrected_livetimes_B = []
120+
corrected_livetimes_C = []
121+
122+
for i in range(num_chunks):
123+
start = i * chunk_size
124+
end = start + chunk_size
125+
if end >= len(fcups):
126+
break
127+
128+
# use np.diff for correct increments
129+
fcup_diff = np.diff(fcups[start:end])
130+
fcupgated_diff = np.diff(fcupgateds[start:end])
131+
live_sub = live_times[start+1:end]
132+
133+
sumA, sumB, sumC, sumDef = 0, 0, 0, 0
134+
skipped_in_chunk = 0
135+
136+
for j, lt in enumerate(live_sub):
137+
if lt > 0:
138+
# Case A
139+
sumA += lt * fcup_diff[j]
140+
corrected_livetimes_A.append(lt)
141+
# Case B
142+
sumB += lt * fcup_diff[j]
143+
corrected_livetimes_B.append(lt)
144+
# Case C
145+
sumC += lt * fcup_diff[j]
146+
corrected_livetimes_C.append(lt)
147+
# Default
148+
sumDef += fcupgated_diff[j]
149+
else:
150+
# ----- Case A/B nearest-neighbor substitution -----
151+
idx_candidates = []
152+
if j - 1 >= 0 and live_sub[j - 1] > 0:
153+
idx_candidates.append(j - 1)
154+
if j + 1 < len(live_sub) and live_sub[j + 1] > 0:
155+
idx_candidates.append(j + 1)
156+
157+
if idx_candidates:
158+
nn = min(
159+
idx_candidates,
160+
key=lambda k: abs(timestamps[start + 1 + k] - timestamps[start + 1 + j])
161+
)
162+
lt_nn = live_sub[nn]
163+
164+
# Case A
165+
sumA += lt_nn * fcup_diff[j]
166+
corrected_livetimes_A.append(lt_nn)
167+
168+
# Case B
169+
sumB += lt_nn * fcupgated_diff[nn]
170+
corrected_livetimes_B.append(lt_nn)
171+
172+
# Default
173+
sumDef += fcupgated_diff[j]
174+
else:
175+
skipped_in_chunk += 1
176+
total_skipped += 1
177+
logger.warning(
178+
f"No valid positive LT neighbor at chunk {i}, local index {j}, "
179+
f"timestamp {timestamps[start+1+j]}"
180+
)
181+
182+
# ----- Case C: mean of ±20 neighbors -----
183+
window = 10
184+
idx_range = range(max(0, j - window), min(len(live_sub), j + window + 1))
185+
neigh_lts = [live_sub[k] for k in idx_range if live_sub[k] > 0]
186+
if neigh_lts:
187+
lt_mean = np.mean(neigh_lts)
188+
sumC += lt_mean * fcup_diff[j]
189+
corrected_livetimes_C.append(lt_mean)
190+
191+
runA += sumA
192+
runB += sumB
193+
runC += sumC
194+
runDef += sumDef
195+
196+
chunk_caseA.append(sumA)
197+
chunk_caseB.append(sumB)
198+
chunk_caseC.append(sumC)
199+
chunk_default.append(sumDef)
200+
cum_caseA.append(runA)
201+
cum_caseB.append(runB)
202+
cum_caseC.append(runC)
203+
cum_default.append(runDef)
204+
chunk_indices.append(i)
205+
skipped_counts.append(skipped_in_chunk)
206+
207+
logger.info(f"Computed chunked FCUP Gated values with neighbor handling (Cases A, B, C).")
208+
logger.info(f"Total skipped events (no valid LT neighbor): {total_skipped}")
209+
210+
# ---------- Plot 2: Chunked FCUP Gated + Ratios + Skips + LT Distribution ----------
211+
fig2, (ax_top, ax_mid, ax_bottom, ax_ltdist) = plt.subplots(
212+
4, 1, figsize=(12, 14), sharex=False,
213+
gridspec_kw={'height_ratios': [3, 1, 1, 2]}
214+
)
215+
fig2.suptitle(f'Run {run_number} - Chunked FCUP Gated (Neighbor Handling)', fontsize=16)
216+
217+
# Top: cumulative sums
218+
ax_top.plot(chunk_indices, cum_caseA, label='Cumulative Case A (LT_nn × FCUPungated)', color='darkred', marker='o')
219+
#ax_top.plot(chunk_indices, cum_caseB, label='Cumulative Case B (LT_nn × FCUPungated_nn)', color='darkgreen', marker='s')
220+
ax_top.plot(chunk_indices, cum_caseC, label='Cumulative Case C (20-NN mean × FCUPungated)', color='darkorange', marker='d')
221+
ax_top.plot(chunk_indices, cum_default, label='Cumulative Default (FCUPgated)', color='blue', marker='^')
222+
ax_top.set_ylabel('Cumulative Σ', fontsize=11)
223+
ax_top.grid(True, linestyle='--', alpha=0.6)
224+
ax_top.legend(fontsize=10)
225+
ax_top.tick_params(axis='both', labelsize=10)
226+
227+
# Middle: ratios wrt default
228+
ratioA = np.divide(cum_caseA, cum_default, out=np.full_like(cum_caseA, np.nan, dtype=float), where=np.array(cum_default) != 0)
229+
#ratioB = np.divide(cum_caseB, cum_default, out=np.full_like(cum_caseB, np.nan, dtype=float), where=np.array(cum_default) != 0)
230+
ratioC = np.divide(cum_caseC, cum_default, out=np.full_like(cum_caseC, np.nan, dtype=float), where=np.array(cum_default) != 0)
231+
232+
ax_mid.plot(chunk_indices, ratioA, label='Case A / Default', color='darkred', marker='o')
233+
#ax_mid.plot(chunk_indices, ratioB, label='Case B / Default', color='darkgreen', marker='s')
234+
ax_mid.plot(chunk_indices, ratioC, label='Case C / Default', color='darkorange', marker='d')
235+
ax_mid.axhline(1.0, color='black', linestyle='--', linewidth=1)
236+
ax_mid.set_ylabel('Ratio', fontsize=11)
237+
ax_mid.grid(True, linestyle='--', alpha=0.6)
238+
ax_mid.legend(fontsize=10)
239+
ax_mid.tick_params(axis='both', labelsize=10)
240+
241+
# Bottom-1: skipped events count
242+
ax_bottom.bar(chunk_indices, skipped_counts, color='gray', alpha=0.7)
243+
ax_bottom.set_xlabel(f'Chunk Index (Each = {chunk_size} events)', fontsize=11)
244+
ax_bottom.set_ylabel('# Skipped', fontsize=11)
245+
ax_bottom.grid(True, linestyle='--', alpha=0.6)
246+
ax_bottom.tick_params(axis='both', labelsize=10)
247+
248+
# Bottom-2: livetime distributions
249+
bins = 100
250+
mean_raw, sigma_raw = np.mean(live_times), np.std(live_times)
251+
mean_A, sigma_A = np.mean(corrected_livetimes_A), np.std(corrected_livetimes_A)
252+
mean_B, sigma_B = np.mean(corrected_livetimes_B), np.std(corrected_livetimes_B)
253+
mean_C, sigma_C = np.mean(corrected_livetimes_C), np.std(corrected_livetimes_C)
254+
255+
ax_ltdist.hist(live_times, bins=bins, alpha=0.4,
256+
label=f'Raw LT (μ={mean_raw:.3f}, σ={sigma_raw:.3f})', color='purple')
257+
ax_ltdist.hist(corrected_livetimes_A, bins=bins, alpha=0.4,
258+
label=f'Case A LT (μ={mean_A:.3f}, σ={sigma_A:.3f})', color='red')
259+
#ax_ltdist.hist(corrected_livetimes_B, bins=bins, alpha=0.4,
260+
# label=f'Case B LT (μ={mean_B:.3f}, σ={sigma_B:.3f})', color='green')
261+
ax_ltdist.hist(corrected_livetimes_C, bins=bins, alpha=0.4,
262+
label=f'Case C LT (μ={mean_C:.3f}, σ={sigma_C:.3f})', color='orange')
263+
264+
ax_ltdist.set_xlabel('Live Time', fontsize=11)
265+
ax_ltdist.set_ylabel('Counts', fontsize=11)
266+
ax_ltdist.legend(fontsize=9)
267+
ax_ltdist.grid(True, linestyle='--', alpha=0.6)
268+
ax_ltdist.tick_params(axis='both', labelsize=10)
269+
270+
fig2.tight_layout(rect=[0, 0.03, 1, 0.95])
271+
fig2.savefig(f'chunked_fcupgated_comparison_{run_number}.png', bbox_inches='tight', dpi=300)
272+
plt.close(fig2)
273+
274+
275+
if __name__ == "__main__":
276+
main()
277+
logger.info("HipoReader example completed.")

0 commit comments

Comments
 (0)