File indexing completed on 2026-04-25 07:38:13
0001
0002
0003 import uproot
0004 import numpy as np
0005 import matplotlib.pyplot as plt
0006 import argparse
0007
0008 def main():
0009 parser = argparse.ArgumentParser(description='Analyze invariant mass from secondary vertex data')
0010 parser.add_argument('--input', '-i', nargs='+', default=['podio_output.root'],
0011 help='Input ROOT file(s) (default: podio_output.root)')
0012 parser.add_argument('--output', '-o', default='invariant_mass.png',
0013 help='Output plot filename (default: invariant_mass.png)')
0014 parser.add_argument('--vertices-collection', default='SecondaryVerticesHelix',
0015 help='Name of the vertices collection (default: SecondaryVerticesHelix)')
0016 args = parser.parse_args()
0017
0018 print(f"Loading data from {len(args.input)} file(s): {args.input}")
0019
0020
0021 all_sec_x_raw = []
0022 all_sec_y_raw = []
0023 all_sec_z_raw = []
0024 all_sec_chi2_raw = []
0025 all_sec_ndf_raw = []
0026 all_px_raw = []
0027 all_py_raw = []
0028 all_pz_raw = []
0029 all_energy_raw = []
0030 all_assoc_begin = []
0031 all_assoc_end = []
0032 all_assoc_indices = []
0033
0034 for input_file in args.input:
0035 print(f" Loading {input_file}...")
0036 file = uproot.open(input_file)
0037 tree = file['events']
0038
0039
0040 vertices_collection = args.vertices_collection
0041 all_sec_x_raw.extend(tree[f'{vertices_collection}/{vertices_collection}.position.x'].array())
0042 all_sec_y_raw.extend(tree[f'{vertices_collection}/{vertices_collection}.position.y'].array())
0043 all_sec_z_raw.extend(tree[f'{vertices_collection}/{vertices_collection}.position.z'].array())
0044 all_sec_chi2_raw.extend(tree[f'{vertices_collection}/{vertices_collection}.chi2'].array())
0045 all_sec_ndf_raw.extend(tree[f'{vertices_collection}/{vertices_collection}.ndf'].array())
0046 all_px_raw.extend(tree['ReconstructedChargedParticles/ReconstructedChargedParticles.momentum.x'].array())
0047 all_py_raw.extend(tree['ReconstructedChargedParticles/ReconstructedChargedParticles.momentum.y'].array())
0048 all_pz_raw.extend(tree['ReconstructedChargedParticles/ReconstructedChargedParticles.momentum.z'].array())
0049 all_energy_raw.extend(tree['ReconstructedChargedParticles/ReconstructedChargedParticles.energy'].array())
0050 all_assoc_begin.extend(tree[f'{vertices_collection}/{vertices_collection}.associatedParticles_begin'].array())
0051 all_assoc_end.extend(tree[f'{vertices_collection}/{vertices_collection}.associatedParticles_end'].array())
0052 all_assoc_indices.extend(tree[f'_{vertices_collection}_associatedParticles/_{vertices_collection}_associatedParticles.index'].array())
0053
0054 file.close()
0055
0056 print(f"Loaded data from {len(all_sec_x_raw)} events")
0057
0058
0059 total_events = len(all_sec_x_raw)
0060 total_sec_vertices = sum(len(all_sec_x_raw[i]) for i in range(len(all_sec_x_raw)))
0061 total_particles = sum(len(all_px_raw[i]) for i in range(len(all_px_raw)))
0062
0063 print(f"\n=== INITIAL STATISTICS ===")
0064 print(f"Total events: {total_events}")
0065 print(f"Total secondary vertices: {total_sec_vertices}")
0066 print(f"Total reconstructed particles: {total_particles}")
0067 print(f"Total association entries: {sum(len(all_assoc_indices[i]) for i in range(len(all_assoc_indices)))}")
0068
0069
0070 invariant_masses = []
0071 decay_lengths_2d = []
0072 decay_lengths_3d = []
0073 vertex_quality = []
0074 n_tracks_per_vertex = []
0075
0076
0077 events_processed = 0
0078 vertices_processed = 0
0079 two_track_vertices = 0
0080 valid_associations = 0
0081 valid_invariant_masses = 0
0082 passed_quality_cuts = 0
0083 kaon_candidates = 0
0084
0085 print("\n=== PROCESSING WITH CORRECT ASSOCIATIONS ===")
0086
0087 for event_idx in range(len(all_sec_x_raw)):
0088
0089 event_sec_x = all_sec_x_raw[event_idx]
0090 event_sec_y = all_sec_y_raw[event_idx]
0091 event_sec_z = all_sec_z_raw[event_idx]
0092 event_chi2 = all_sec_chi2_raw[event_idx]
0093 event_ndf = all_sec_ndf_raw[event_idx]
0094
0095 event_px = all_px_raw[event_idx]
0096 event_py = all_py_raw[event_idx]
0097 event_pz = all_pz_raw[event_idx]
0098 event_energy = all_energy_raw[event_idx]
0099
0100 event_assoc_begin = all_assoc_begin[event_idx]
0101 event_assoc_end = all_assoc_end[event_idx]
0102 event_assoc_indices = all_assoc_indices[event_idx]
0103
0104
0105 if len(event_px) == 0 or len(event_sec_x) == 0:
0106 continue
0107
0108 events_processed += 1
0109
0110
0111 for vtx_idx in range(len(event_sec_x)):
0112 vertices_processed += 1
0113
0114 vtx_begin = event_assoc_begin[vtx_idx]
0115 vtx_end = event_assoc_end[vtx_idx]
0116
0117
0118 if vtx_begin < len(event_assoc_indices) and vtx_end <= len(event_assoc_indices):
0119 vertex_track_indices = event_assoc_indices[vtx_begin:vtx_end]
0120 n_tracks = len(vertex_track_indices)
0121 n_tracks_per_vertex.append(n_tracks)
0122
0123 print(f"Event {event_idx}, Vertex {vtx_idx}: {n_tracks} tracks, indices: {vertex_track_indices}")
0124
0125
0126 decay_2d = np.sqrt(event_sec_x[vtx_idx]**2 + event_sec_y[vtx_idx]**2)
0127 decay_3d = np.sqrt(event_sec_x[vtx_idx]**2 + event_sec_y[vtx_idx]**2 + event_sec_z[vtx_idx]**2)
0128 chi2_ndf = event_chi2[vtx_idx] / (event_ndf[vtx_idx] + 1e-10)
0129
0130
0131 if n_tracks == 2:
0132 two_track_vertices += 1
0133
0134 idx1, idx2 = vertex_track_indices[0], vertex_track_indices[1]
0135
0136
0137 if idx1 < len(event_px) and idx2 < len(event_px):
0138 valid_associations += 1
0139
0140
0141 p1 = [event_px[idx1], event_py[idx1], event_pz[idx1], event_energy[idx1]]
0142 p2 = [event_px[idx2], event_py[idx2], event_pz[idx2], event_energy[idx2]]
0143
0144 total_E = p1[3] + p2[3]
0145 total_px = p1[0] + p2[0]
0146 total_py = p1[1] + p2[1]
0147 total_pz = p1[2] + p2[2]
0148
0149 inv_mass_sq = total_E**2 - (total_px**2 + total_py**2 + total_pz**2)
0150
0151 if inv_mass_sq > 0:
0152 valid_invariant_masses += 1
0153 inv_mass = np.sqrt(inv_mass_sq)
0154
0155
0156 passes_vertex_quality = chi2_ndf < 3.0
0157 passes_decay_length = 0.05 < decay_2d < 2.0
0158
0159
0160 if passes_vertex_quality and passes_decay_length:
0161 passed_quality_cuts += 1
0162 invariant_masses.append(inv_mass)
0163 decay_lengths_2d.append(decay_2d)
0164 decay_lengths_3d.append(decay_3d)
0165 vertex_quality.append(chi2_ndf)
0166
0167
0168 if 0.4 <= inv_mass <= 0.6:
0169 kaon_candidates += 1
0170 print(f" -> KAON CANDIDATE: mass={inv_mass:.4f} GeV, decay_length={decay_2d:.3f} mm, χ²/ndf={chi2_ndf:.2f}")
0171 else:
0172 print(f" -> Failed cuts: χ²/ndf={chi2_ndf:.2f} (cut<3.0), decay_length={decay_2d:.3f}mm (cut: 0.05-2.0mm)")
0173 else:
0174 print(f" -> Invalid mass² = {inv_mass_sq}")
0175 else:
0176 print(f" -> Invalid track indices: {idx1}, {idx2} (max: {len(event_px)-1})")
0177 elif n_tracks > 0:
0178 print(f" -> {n_tracks}-track vertex (not 2-track)")
0179
0180
0181 invariant_masses = np.array(invariant_masses)
0182 decay_lengths_2d = np.array(decay_lengths_2d)
0183 decay_lengths_3d = np.array(decay_lengths_3d)
0184 vertex_quality = np.array(vertex_quality)
0185 n_tracks_per_vertex = np.array(n_tracks_per_vertex)
0186
0187 print(f"\n=== CUT FLOW RESULTS ===")
0188 print(f"Events processed: {events_processed:>8}")
0189 print(f"Vertices processed: {vertices_processed:>8}")
0190 print(f"Two-track vertices: {two_track_vertices:>8} ({100*two_track_vertices/vertices_processed:.1f}% of vertices)")
0191 print(f"Valid associations: {valid_associations:>8} ({100*valid_associations/two_track_vertices:.1f}% of 2-track vertices)")
0192 print(f"Valid invariant masses: {valid_invariant_masses:>8} ({100*valid_invariant_masses/valid_associations:.1f}% of valid associations)")
0193 print(f"Passed quality cuts: {passed_quality_cuts:>8} ({100*passed_quality_cuts/valid_invariant_masses:.1f}% of valid masses)")
0194 print(f"Kaon candidates (0.4-0.6 GeV): {kaon_candidates:>8} ({100*kaon_candidates/passed_quality_cuts:.1f}% of quality candidates)")
0195
0196 if len(invariant_masses) > 0:
0197 print(f"\n=== ANALYSIS RESULTS (AFTER CUTS) ===")
0198 print(f"Applied cuts: χ²/ndf < 3.0, 0.05 < decay_length < 2.0 mm")
0199 print(f"Mass range: {np.min(invariant_masses):.4f} - {np.max(invariant_masses):.4f} GeV")
0200 print(f"Mean mass: {np.mean(invariant_masses):.4f} ± {np.std(invariant_masses):.4f} GeV")
0201 print(f"Decay length range: {np.min(decay_lengths_2d):.3f} - {np.max(decay_lengths_2d):.3f} mm")
0202 print(f"Mean decay length: {np.mean(decay_lengths_2d):.3f} ± {np.std(decay_lengths_2d):.3f} mm")
0203 print(f"Vertex quality range: {np.min(vertex_quality):.2f} - {np.max(vertex_quality):.2f}")
0204
0205
0206 print(f"\n=== TRACK MULTIPLICITY ===")
0207 unique_multiplicities, counts = np.unique(n_tracks_per_vertex, return_counts=True)
0208 for mult, count in zip(unique_multiplicities, counts):
0209 percentage = 100 * count / len(n_tracks_per_vertex)
0210 print(f"{mult} tracks per vertex: {count:>8} ({percentage:>5.1f}%)")
0211
0212
0213 print(f"\n=== DECAY LENGTH CUTS ANALYSIS ===")
0214 decay_cuts = {
0215 'Very Short (<0.1mm)': (0, 0.1),
0216 'Short (0.1-0.5mm)': (0.1, 0.5),
0217 'Medium (0.5-1.0mm)': (0.5, 1.0),
0218 'Long (>1.0mm)': (1.0, np.inf)
0219 }
0220
0221 for cut_name, (min_cut, max_cut) in decay_cuts.items():
0222 mask = (decay_lengths_2d >= min_cut) & (decay_lengths_2d < max_cut)
0223 masses = invariant_masses[mask]
0224 kaon_mask = (masses >= 0.45) & (masses <= 0.55)
0225
0226 print(f"{cut_name:<25}: {len(masses):>6} pairs, {np.sum(kaon_mask):>3} kaon candidates")
0227
0228
0229 fig, ax = plt.subplots(1, 1, figsize=(10, 8))
0230
0231
0232 h = ax.hist2d(invariant_masses, decay_lengths_2d,
0233 bins=[np.linspace(0, 1, 21), 15],
0234 cmap='viridis', alpha=0.8, range=[[0, 1], None])
0235 plt.colorbar(h[3], ax=ax, label='Count')
0236 ax.axvline(0.4976, color='red', linestyle='--', linewidth=2, alpha=0.9, label='K⁰_S')
0237 ax.axvline(0.4937, color='orange', linestyle=':', linewidth=2, alpha=0.9, label='K±')
0238 ax.axvline(0.7755, color='purple', linestyle='-.', linewidth=2, alpha=0.9, label='ρ⁰')
0239 ax.set_xlabel('Invariant Mass (GeV)')
0240 ax.set_ylabel('2D Decay Length (mm)')
0241 ax.set_title('Mass vs Decay Length (2D Histogram)\n(0-1 GeV Mass Range)')
0242 ax.set_xlim(0, 1)
0243 ax.legend()
0244
0245 plt.tight_layout()
0246 plt.savefig(args.output, dpi=150, bbox_inches='tight')
0247 print(f"Plot saved to {args.output}")
0248
0249 else:
0250 print("No valid invariant masses calculated!")
0251
0252 if __name__ == "__main__":
0253 main()