lheutils 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lheutils/__init__.py +1 -0
- lheutils/_version.py +34 -0
- lheutils/cli/lhe2lhe.py +159 -0
- lheutils/cli/lhecheck.py +619 -0
- lheutils/cli/lhediff.py +561 -0
- lheutils/cli/lhefilter.py +371 -0
- lheutils/cli/lheinfo.py +306 -0
- lheutils/cli/lhemerge.py +172 -0
- lheutils/cli/lheshow.py +118 -0
- lheutils/cli/lhesplit.py +132 -0
- lheutils/cli/lhestack.py +233 -0
- lheutils/cli/lheunstack.py +78 -0
- lheutils/cli/util.py +18 -0
- lheutils-0.0.2.dist-info/METADATA +73 -0
- lheutils-0.0.2.dist-info/RECORD +18 -0
- lheutils-0.0.2.dist-info/WHEEL +4 -0
- lheutils-0.0.2.dist-info/entry_points.txt +10 -0
- lheutils-0.0.2.dist-info/licenses/LICENSE +190 -0
lheutils/cli/lhecheck.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLI tool to validate LHE files and check momentum conservation.
|
|
4
|
+
|
|
5
|
+
This tool validates that LHE files can be loaded properly and checks
|
|
6
|
+
momentum conservation for each event up to a specified precision.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# APN TODO this should also check mother daughter momentum conservations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import math
|
|
13
|
+
import sys
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional, TextIO, Union
|
|
19
|
+
|
|
20
|
+
from typing_extensions import Self
|
|
21
|
+
|
|
22
|
+
import pylhe
|
|
23
|
+
|
|
24
|
+
from lheutils.cli.util import create_base_parser
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def positive_float(value: str) -> float:
|
|
28
|
+
"""Custom argparse type for positive floats."""
|
|
29
|
+
try:
|
|
30
|
+
fvalue = float(value)
|
|
31
|
+
except ValueError:
|
|
32
|
+
err = f"Invalid float value: '{value}'"
|
|
33
|
+
raise argparse.ArgumentTypeError(err) from None
|
|
34
|
+
|
|
35
|
+
if fvalue <= 0:
|
|
36
|
+
err = f"Value must be positive, got: {fvalue}"
|
|
37
|
+
raise argparse.ArgumentTypeError(err)
|
|
38
|
+
|
|
39
|
+
return fvalue
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class LHECheckArgs:
|
|
44
|
+
positive_mass: bool
|
|
45
|
+
positive_mass_abs: float
|
|
46
|
+
onshell: bool
|
|
47
|
+
onshell_rel: float
|
|
48
|
+
onshell_abs: float
|
|
49
|
+
total_momentum: bool
|
|
50
|
+
total_momentum_rel: float
|
|
51
|
+
total_momentum_abs: float
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class LHECheckAccumulatedSummary:
|
|
56
|
+
total_files_checked: int
|
|
57
|
+
total_events_with_violations: int
|
|
58
|
+
total_positive_mass_violations: int
|
|
59
|
+
total_onshell_violations: int
|
|
60
|
+
total_total_momentum_violations: int
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def total_violations(self) -> int:
|
|
64
|
+
return (
|
|
65
|
+
self.total_positive_mass_violations
|
|
66
|
+
+ self.total_onshell_violations
|
|
67
|
+
+ self.total_total_momentum_violations
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def __add__(
|
|
71
|
+
self, other: "LHECheckAccumulatedSummary"
|
|
72
|
+
) -> "LHECheckAccumulatedSummary":
|
|
73
|
+
return LHECheckAccumulatedSummary(
|
|
74
|
+
total_files_checked=self.total_files_checked + other.total_files_checked,
|
|
75
|
+
total_events_with_violations=self.total_events_with_violations
|
|
76
|
+
+ other.total_events_with_violations,
|
|
77
|
+
total_positive_mass_violations=self.total_positive_mass_violations
|
|
78
|
+
+ other.total_positive_mass_violations,
|
|
79
|
+
total_onshell_violations=self.total_onshell_violations
|
|
80
|
+
+ other.total_onshell_violations,
|
|
81
|
+
total_total_momentum_violations=self.total_total_momentum_violations
|
|
82
|
+
+ other.total_total_momentum_violations,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __iadd__(self, other: "LHECheckAccumulatedSummary") -> Self:
|
|
86
|
+
self.total_files_checked += other.total_files_checked
|
|
87
|
+
self.total_events_with_violations += other.total_events_with_violations
|
|
88
|
+
self.total_positive_mass_violations += other.total_positive_mass_violations
|
|
89
|
+
self.total_onshell_violations += other.total_onshell_violations
|
|
90
|
+
self.total_total_momentum_violations += other.total_total_momentum_violations
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def print(self, *args: Any, **kwargs: Any) -> None:
|
|
94
|
+
strings = []
|
|
95
|
+
strings.append(f"Total files checked: {self.total_files_checked}")
|
|
96
|
+
strings.append(
|
|
97
|
+
f"Total events with violations: {self.total_events_with_violations}"
|
|
98
|
+
)
|
|
99
|
+
strings.append(f"Total violations: {self.total_violations}")
|
|
100
|
+
strings.append(
|
|
101
|
+
f" Positive mass violations: {self.total_positive_mass_violations}"
|
|
102
|
+
)
|
|
103
|
+
strings.append(f" On-shell mass violations: {self.total_onshell_violations}")
|
|
104
|
+
strings.append(
|
|
105
|
+
f" Total momentum violations: {self.total_total_momentum_violations}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
print("\n".join(strings), *args, **kwargs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class LHEMomentum:
|
|
113
|
+
px: float
|
|
114
|
+
py: float
|
|
115
|
+
pz: float
|
|
116
|
+
e: float
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class LHECheckTotalMomentaViolations:
|
|
121
|
+
incoming: list[LHEMomentum]
|
|
122
|
+
outgoing: list[LHEMomentum]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def total_incoming(self) -> LHEMomentum:
|
|
126
|
+
total_px = sum(p.px for p in self.incoming)
|
|
127
|
+
total_py = sum(p.py for p in self.incoming)
|
|
128
|
+
total_pz = sum(p.pz for p in self.incoming)
|
|
129
|
+
total_e = sum(p.e for p in self.incoming)
|
|
130
|
+
return LHEMomentum(px=total_px, py=total_py, pz=total_pz, e=total_e)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def total_outgoing(self) -> LHEMomentum:
|
|
134
|
+
total_px = sum(p.px for p in self.outgoing)
|
|
135
|
+
total_py = sum(p.py for p in self.outgoing)
|
|
136
|
+
total_pz = sum(p.pz for p in self.outgoing)
|
|
137
|
+
total_e = sum(p.e for p in self.outgoing)
|
|
138
|
+
return LHEMomentum(px=total_px, py=total_py, pz=total_pz, e=total_e)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def differences(self) -> LHEMomentum:
|
|
142
|
+
total_in = self.total_incoming
|
|
143
|
+
total_out = self.total_outgoing
|
|
144
|
+
return LHEMomentum(
|
|
145
|
+
px=abs(total_in.px - total_out.px),
|
|
146
|
+
py=abs(total_in.py - total_out.py),
|
|
147
|
+
pz=abs(total_in.pz - total_out.pz),
|
|
148
|
+
e=abs(total_in.e - total_out.e),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def rel_differences(self) -> LHEMomentum:
|
|
153
|
+
diff = self.differences
|
|
154
|
+
|
|
155
|
+
refpx = max(
|
|
156
|
+
[abs(p.px) for p in self.incoming + self.outgoing] + [1e-12]
|
|
157
|
+
) # Prevent division by zero
|
|
158
|
+
refpy = max([abs(p.py) for p in self.incoming + self.outgoing] + [1e-12])
|
|
159
|
+
refpz = max([abs(p.pz) for p in self.incoming + self.outgoing] + [1e-12])
|
|
160
|
+
refe = max([abs(p.e) for p in self.incoming + self.outgoing] + [1e-12])
|
|
161
|
+
|
|
162
|
+
return LHEMomentum(
|
|
163
|
+
px=diff.px / refpx, # Prevent division by zero
|
|
164
|
+
py=diff.py / refpy,
|
|
165
|
+
pz=diff.pz / refpz,
|
|
166
|
+
e=diff.e / refe,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def is_violation(
|
|
170
|
+
self, absolute_threshold: float, relative_threshold: float
|
|
171
|
+
) -> bool:
|
|
172
|
+
diffs = self.differences
|
|
173
|
+
rel_diffs = self.rel_differences
|
|
174
|
+
return (
|
|
175
|
+
not (diffs.px < absolute_threshold or rel_diffs.px < relative_threshold)
|
|
176
|
+
or not (diffs.py < absolute_threshold or rel_diffs.py < relative_threshold)
|
|
177
|
+
or not (diffs.pz < absolute_threshold or rel_diffs.pz < relative_threshold)
|
|
178
|
+
or not (diffs.e < absolute_threshold or rel_diffs.e < relative_threshold)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def print(self, *args: Any, **kwargs: Any) -> LHECheckAccumulatedSummary:
|
|
182
|
+
incoming = self.total_incoming
|
|
183
|
+
outgoing = self.total_outgoing
|
|
184
|
+
diffs = self.differences
|
|
185
|
+
rel_diffs = self.rel_differences
|
|
186
|
+
|
|
187
|
+
lines = []
|
|
188
|
+
lines.append(f" {'Metric':<12} {'px':<12} {'py':<12} {'pz':<12} {'E':<12}")
|
|
189
|
+
lines.append(
|
|
190
|
+
f" {'-' * 12:<12} {'-' * 12:<12} {'-' * 12:<12} {'-' * 12:<12} {'-' * 12:<12}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
metrics = [
|
|
194
|
+
("Incoming", incoming.px, incoming.py, incoming.pz, incoming.e),
|
|
195
|
+
("Outgoing", outgoing.px, outgoing.py, outgoing.pz, outgoing.e),
|
|
196
|
+
("Abs Diff", diffs.px, diffs.py, diffs.pz, diffs.e),
|
|
197
|
+
("Rel Diff", rel_diffs.px, rel_diffs.py, rel_diffs.pz, rel_diffs.e),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
for metric, px_val, py_val, pz_val, e_val in metrics:
|
|
201
|
+
lines.append(
|
|
202
|
+
f" {metric:<12} {px_val:<12.4e} {py_val:<12.4e} {pz_val:<12.4e} {e_val:<12.4e}"
|
|
203
|
+
)
|
|
204
|
+
print("\n".join(lines), *args, **kwargs)
|
|
205
|
+
return LHECheckAccumulatedSummary(
|
|
206
|
+
total_files_checked=0,
|
|
207
|
+
total_events_with_violations=0,
|
|
208
|
+
total_positive_mass_violations=0,
|
|
209
|
+
total_onshell_violations=0,
|
|
210
|
+
total_total_momentum_violations=1,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class LHECheckOnShellViolation:
|
|
216
|
+
px: float
|
|
217
|
+
py: float
|
|
218
|
+
pz: float
|
|
219
|
+
e: float
|
|
220
|
+
m: float
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def p(self) -> float:
|
|
224
|
+
# TODO check fail on negative mass?!
|
|
225
|
+
return math.sqrt(abs(self.e**2 - (self.px**2 + self.py**2 + self.pz**2)))
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def difference(self) -> float:
|
|
229
|
+
return abs(self.p - self.m)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def rel_difference(self) -> float:
|
|
233
|
+
return self.difference / max(
|
|
234
|
+
abs(self.m), abs(self.p), 1e-12
|
|
235
|
+
) # Prevent division by zero
|
|
236
|
+
|
|
237
|
+
def is_violation(
|
|
238
|
+
self, absolute_threshold: float, relative_threshold: float
|
|
239
|
+
) -> bool:
|
|
240
|
+
return not (
|
|
241
|
+
self.difference < absolute_threshold
|
|
242
|
+
or self.rel_difference < relative_threshold
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def print(self, *args: Any, **kwargs: Any) -> LHECheckAccumulatedSummary:
|
|
246
|
+
lines = []
|
|
247
|
+
lines.append("✗ On-shell mass violation:")
|
|
248
|
+
lines.append(f" px: {self.px:>12.4e}")
|
|
249
|
+
lines.append(f" py: {self.py:>12.4e}")
|
|
250
|
+
lines.append(f" pz: {self.pz:>12.4e}")
|
|
251
|
+
lines.append(f" e: {self.e:>12.4e}")
|
|
252
|
+
lines.append(f" |p|: {self.p:>12.4e}")
|
|
253
|
+
lines.append(f" m: {self.m:>12.4e}")
|
|
254
|
+
lines.append(
|
|
255
|
+
f" ||p| - |m||: {self.difference:.4e} (rel: {self.rel_difference:.4e})"
|
|
256
|
+
)
|
|
257
|
+
print("\n".join(lines), *args, **kwargs)
|
|
258
|
+
return LHECheckAccumulatedSummary(
|
|
259
|
+
total_files_checked=0,
|
|
260
|
+
total_events_with_violations=0,
|
|
261
|
+
total_positive_mass_violations=0,
|
|
262
|
+
total_onshell_violations=1,
|
|
263
|
+
total_total_momentum_violations=0,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass
|
|
268
|
+
class LHECheckPositiveMassViolation:
|
|
269
|
+
px: float
|
|
270
|
+
py: float
|
|
271
|
+
pz: float
|
|
272
|
+
e: float
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def p2(self) -> float:
|
|
276
|
+
return self.px**2 + self.py**2 + self.pz**2
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def e2(self) -> float:
|
|
280
|
+
return self.e**2
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def m2(self) -> float:
|
|
284
|
+
return self.e2 - self.p2
|
|
285
|
+
|
|
286
|
+
def is_violation(self, absolute_threshold: float) -> bool:
|
|
287
|
+
return self.p2 - self.e2 > absolute_threshold**2
|
|
288
|
+
|
|
289
|
+
def print(self, *args: Any, **kwargs: Any) -> LHECheckAccumulatedSummary:
|
|
290
|
+
lines = []
|
|
291
|
+
lines.append("✗ Positive mass violation:")
|
|
292
|
+
lines.append(f" px: {self.px:>12.4e}")
|
|
293
|
+
lines.append(f" py: {self.py:>12.4e}")
|
|
294
|
+
lines.append(f" pz: {self.pz:>12.4e}")
|
|
295
|
+
lines.append(f" e: {self.e:>12.4e}")
|
|
296
|
+
lines.append(f" p²: {self.p2:>12.4e}")
|
|
297
|
+
lines.append(f" e²: {self.e2:>12.4e}")
|
|
298
|
+
lines.append(f" m²: {self.m2:>12.4e} (negative - unphysical)")
|
|
299
|
+
print("\n".join(lines), *args, **kwargs)
|
|
300
|
+
return LHECheckAccumulatedSummary(
|
|
301
|
+
total_files_checked=0,
|
|
302
|
+
total_events_with_violations=0,
|
|
303
|
+
total_positive_mass_violations=1,
|
|
304
|
+
total_onshell_violations=0,
|
|
305
|
+
total_total_momentum_violations=0,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class LHECheckParticleViolation:
|
|
311
|
+
particle_index: int
|
|
312
|
+
particle_pdgid: int
|
|
313
|
+
on_shell_violations: Optional[LHECheckOnShellViolation]
|
|
314
|
+
positive_mass_violation: Optional[LHECheckPositiveMassViolation]
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def total_violations(self) -> int:
|
|
318
|
+
return sum(
|
|
319
|
+
v is not None
|
|
320
|
+
for v in [self.on_shell_violations, self.positive_mass_violation]
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def print(self, *args: Any, **kwargs: Any) -> LHECheckAccumulatedSummary:
|
|
324
|
+
ret = LHECheckAccumulatedSummary(
|
|
325
|
+
total_files_checked=0,
|
|
326
|
+
total_events_with_violations=0,
|
|
327
|
+
total_positive_mass_violations=0,
|
|
328
|
+
total_onshell_violations=0,
|
|
329
|
+
total_total_momentum_violations=0,
|
|
330
|
+
)
|
|
331
|
+
print(
|
|
332
|
+
f"✗ Particle {self.particle_index} (id={self.particle_pdgid}) violations:",
|
|
333
|
+
*args,
|
|
334
|
+
**kwargs,
|
|
335
|
+
)
|
|
336
|
+
if self.on_shell_violations is not None:
|
|
337
|
+
ret += self.on_shell_violations.print(*args, **kwargs)
|
|
338
|
+
if self.positive_mass_violation is not None:
|
|
339
|
+
ret += self.positive_mass_violation.print(*args, **kwargs)
|
|
340
|
+
return ret
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dataclass
|
|
344
|
+
class LHECheckEventViolation:
|
|
345
|
+
event_index: int
|
|
346
|
+
particle_violations: list[LHECheckParticleViolation]
|
|
347
|
+
total_momentum_violations: Optional[LHECheckTotalMomentaViolations]
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def total_violations(self) -> int:
|
|
351
|
+
count = sum(p.total_violations for p in self.particle_violations)
|
|
352
|
+
if self.total_momentum_violations is not None:
|
|
353
|
+
count += 1
|
|
354
|
+
return count
|
|
355
|
+
|
|
356
|
+
def print(self, *args: object, **kwargs: object) -> LHECheckAccumulatedSummary:
|
|
357
|
+
ret = LHECheckAccumulatedSummary(
|
|
358
|
+
total_files_checked=0,
|
|
359
|
+
total_events_with_violations=0,
|
|
360
|
+
total_positive_mass_violations=0,
|
|
361
|
+
total_onshell_violations=0,
|
|
362
|
+
total_total_momentum_violations=0,
|
|
363
|
+
)
|
|
364
|
+
print(f"✗ Event {self.event_index} violations:")
|
|
365
|
+
for pviolation in self.particle_violations:
|
|
366
|
+
ret += pviolation.print(*args, **kwargs)
|
|
367
|
+
if self.total_momentum_violations is not None:
|
|
368
|
+
ret += self.total_momentum_violations.print(*args, **kwargs)
|
|
369
|
+
return ret
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass
|
|
373
|
+
class LHECheck:
|
|
374
|
+
file: str
|
|
375
|
+
check_events: Iterable[LHECheckEventViolation]
|
|
376
|
+
|
|
377
|
+
def print(self, *args: Any, **kwargs: Any) -> LHECheckAccumulatedSummary:
|
|
378
|
+
ret = LHECheckAccumulatedSummary(
|
|
379
|
+
total_events_with_violations=0,
|
|
380
|
+
total_positive_mass_violations=0,
|
|
381
|
+
total_onshell_violations=0,
|
|
382
|
+
total_total_momentum_violations=0,
|
|
383
|
+
total_files_checked=1,
|
|
384
|
+
)
|
|
385
|
+
printed_header = False
|
|
386
|
+
for event in self.check_events:
|
|
387
|
+
if not printed_header:
|
|
388
|
+
print("-" * 60, *args, **kwargs)
|
|
389
|
+
print(f"File: {self.file}", *args, **kwargs)
|
|
390
|
+
printed_header = True
|
|
391
|
+
ret += event.print(*args, **kwargs)
|
|
392
|
+
return ret
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def get_lhecheck(
|
|
396
|
+
filepath_or_fileobj: Union[str, TextIO],
|
|
397
|
+
lhecargs: LHECheckArgs,
|
|
398
|
+
) -> LHECheck:
|
|
399
|
+
# Read LHE file
|
|
400
|
+
if isinstance(filepath_or_fileobj, str):
|
|
401
|
+
lhefile = pylhe.LHEFile.fromfile(filepath_or_fileobj)
|
|
402
|
+
file_display_name = filepath_or_fileobj
|
|
403
|
+
else:
|
|
404
|
+
lhefile = pylhe.LHEFile.frombuffer(filepath_or_fileobj)
|
|
405
|
+
file_display_name = "<stdin>"
|
|
406
|
+
|
|
407
|
+
def _generator() -> Iterable[LHECheckEventViolation]:
|
|
408
|
+
for event_index, event in enumerate(lhefile.events, start=1):
|
|
409
|
+
lhecheck_event = LHECheckEventViolation(
|
|
410
|
+
event_index=event_index,
|
|
411
|
+
particle_violations=[],
|
|
412
|
+
total_momentum_violations=None,
|
|
413
|
+
)
|
|
414
|
+
lhe_check_total_momenta = LHECheckTotalMomentaViolations(
|
|
415
|
+
incoming=[
|
|
416
|
+
LHEMomentum(
|
|
417
|
+
px=particle.px, py=particle.py, pz=particle.pz, e=particle.e
|
|
418
|
+
)
|
|
419
|
+
for particle in event.particles
|
|
420
|
+
if particle.status == -1
|
|
421
|
+
], # Incoming
|
|
422
|
+
outgoing=[
|
|
423
|
+
LHEMomentum(
|
|
424
|
+
px=particle.px, py=particle.py, pz=particle.pz, e=particle.e
|
|
425
|
+
)
|
|
426
|
+
for particle in event.particles
|
|
427
|
+
if particle.status == 1
|
|
428
|
+
], # Outgoing
|
|
429
|
+
)
|
|
430
|
+
if lhecargs.total_momentum and lhe_check_total_momenta.is_violation(
|
|
431
|
+
lhecargs.total_momentum_abs, lhecargs.total_momentum_rel
|
|
432
|
+
):
|
|
433
|
+
lhecheck_event.total_momentum_violations = lhe_check_total_momenta
|
|
434
|
+
|
|
435
|
+
for particle_index, particle in enumerate(event.particles, start=1):
|
|
436
|
+
lhe_particle_check = LHECheckParticleViolation(
|
|
437
|
+
particle_index=particle_index,
|
|
438
|
+
particle_pdgid=particle.id,
|
|
439
|
+
on_shell_violations=None,
|
|
440
|
+
positive_mass_violation=None,
|
|
441
|
+
)
|
|
442
|
+
if lhecargs.positive_mass:
|
|
443
|
+
lhe_check_mass = LHECheckPositiveMassViolation(
|
|
444
|
+
px=particle.px, py=particle.py, pz=particle.pz, e=particle.e
|
|
445
|
+
)
|
|
446
|
+
if lhe_check_mass.is_violation(lhecargs.positive_mass_abs):
|
|
447
|
+
lhe_particle_check.positive_mass_violation = lhe_check_mass
|
|
448
|
+
if lhecargs.onshell and particle.status in [
|
|
449
|
+
-1,
|
|
450
|
+
1,
|
|
451
|
+
]: # Incoming or outgoing particles
|
|
452
|
+
lhe_check_onshell = LHECheckOnShellViolation(
|
|
453
|
+
px=particle.px,
|
|
454
|
+
py=particle.py,
|
|
455
|
+
pz=particle.pz,
|
|
456
|
+
e=particle.e,
|
|
457
|
+
m=particle.m,
|
|
458
|
+
)
|
|
459
|
+
if lhe_check_onshell.is_violation(
|
|
460
|
+
lhecargs.onshell_abs, lhecargs.onshell_rel
|
|
461
|
+
):
|
|
462
|
+
lhe_particle_check.on_shell_violations = lhe_check_onshell
|
|
463
|
+
if lhe_particle_check.total_violations > 0:
|
|
464
|
+
lhecheck_event.particle_violations.append(lhe_particle_check)
|
|
465
|
+
if lhecheck_event.total_violations > 0:
|
|
466
|
+
yield lhecheck_event
|
|
467
|
+
|
|
468
|
+
return LHECheck(
|
|
469
|
+
file=file_display_name,
|
|
470
|
+
check_events=_generator(),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@dataclass
|
|
475
|
+
class LHECheckSummary:
|
|
476
|
+
files: list[LHECheck]
|
|
477
|
+
|
|
478
|
+
def print(self, *args: Any, **kwargs: Any) -> LHECheckAccumulatedSummary:
|
|
479
|
+
ret = LHECheckAccumulatedSummary(
|
|
480
|
+
total_files_checked=0,
|
|
481
|
+
total_events_with_violations=0,
|
|
482
|
+
total_positive_mass_violations=0,
|
|
483
|
+
total_onshell_violations=0,
|
|
484
|
+
total_total_momentum_violations=0,
|
|
485
|
+
)
|
|
486
|
+
for lhecheck in self.files:
|
|
487
|
+
ret += lhecheck.print(*args, **kwargs)
|
|
488
|
+
return ret
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def get_lhechecksummary(
|
|
492
|
+
filepaths_or_fileobjs: list[Union[str, TextIO]],
|
|
493
|
+
lhecargs: LHECheckArgs,
|
|
494
|
+
) -> LHECheckSummary:
|
|
495
|
+
lhechecks = []
|
|
496
|
+
for filepath_or_fileobj in filepaths_or_fileobjs:
|
|
497
|
+
lhecheck = get_lhecheck(filepath_or_fileobj, lhecargs)
|
|
498
|
+
lhechecks.append(lhecheck)
|
|
499
|
+
return LHECheckSummary(files=lhechecks)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def main() -> None:
|
|
503
|
+
"""Main CLI function."""
|
|
504
|
+
parser = create_base_parser(
|
|
505
|
+
description="Validate LHE files and check momentum conservation",
|
|
506
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
507
|
+
epilog="""
|
|
508
|
+
Examples:
|
|
509
|
+
lhecheck file.lhe # Check with default thresholds (1e-6)
|
|
510
|
+
cat file.lhe | lhecheck # Read from stdin
|
|
511
|
+
lhecheck file.lhe --no-momentum # Skip momentum conservation checks
|
|
512
|
+
lhecheck file.lhe --no-onshell # Skip on-shell mass checks
|
|
513
|
+
lhecheck file.lhe -a 1e-10 -r 1e-8 -v # Custom thresholds with verbose output
|
|
514
|
+
cat file.lhe | lhecheck -v # Read from stdin with verbose JSON output
|
|
515
|
+
""",
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
parser.add_argument(
|
|
519
|
+
"files",
|
|
520
|
+
nargs="*",
|
|
521
|
+
help="LHE file(s) to validate (or read from stdin if not provided)",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
parser.add_argument(
|
|
525
|
+
"--no-total-momentum",
|
|
526
|
+
action="store_true",
|
|
527
|
+
help="Skip total momentum conservation checks",
|
|
528
|
+
)
|
|
529
|
+
parser.add_argument(
|
|
530
|
+
"--total-momentum-rel",
|
|
531
|
+
type=positive_float,
|
|
532
|
+
default=1e-6,
|
|
533
|
+
help="Relative threshold for total momentum conservation (default: 1e-6)",
|
|
534
|
+
)
|
|
535
|
+
parser.add_argument(
|
|
536
|
+
"--total-momentum-abs",
|
|
537
|
+
type=positive_float,
|
|
538
|
+
default=1e-6,
|
|
539
|
+
help="Absolute threshold for total momentum conservation (default: 1e-6)",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
parser.add_argument(
|
|
543
|
+
"--no-onshell",
|
|
544
|
+
action="store_true",
|
|
545
|
+
help="Skip on-shell mass checks",
|
|
546
|
+
)
|
|
547
|
+
parser.add_argument(
|
|
548
|
+
"--onshell-rel",
|
|
549
|
+
type=positive_float,
|
|
550
|
+
default=1e-6,
|
|
551
|
+
help="Relative threshold for on-shell mass checks (default: 1e-6)",
|
|
552
|
+
)
|
|
553
|
+
parser.add_argument(
|
|
554
|
+
"--onshell-abs",
|
|
555
|
+
type=positive_float,
|
|
556
|
+
default=1e-6,
|
|
557
|
+
help="Absolute threshold for on-shell mass checks (default: 1e-6)",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
parser.add_argument(
|
|
561
|
+
"--no-positive-mass",
|
|
562
|
+
action="store_true",
|
|
563
|
+
help="Skip positive mass checks",
|
|
564
|
+
)
|
|
565
|
+
parser.add_argument(
|
|
566
|
+
"--positive-mass-abs",
|
|
567
|
+
type=positive_float,
|
|
568
|
+
default=1e-6,
|
|
569
|
+
help="Absolute threshold for positive mass checks (default: 1e-6)",
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
args = parser.parse_args()
|
|
573
|
+
|
|
574
|
+
# Check if reading from stdin
|
|
575
|
+
use_stdin = not args.files and not sys.stdin.isatty()
|
|
576
|
+
|
|
577
|
+
file_inputs: list[Union[str, TextIO]] = []
|
|
578
|
+
if use_stdin:
|
|
579
|
+
# Read from stdin
|
|
580
|
+
file_inputs += [sys.stdin]
|
|
581
|
+
else:
|
|
582
|
+
# Expand file paths
|
|
583
|
+
for pattern in args.files:
|
|
584
|
+
path = Path(pattern)
|
|
585
|
+
if path.exists():
|
|
586
|
+
if path.is_file():
|
|
587
|
+
file_inputs.append(str(path))
|
|
588
|
+
else:
|
|
589
|
+
warnings.warn(f"{pattern} is not a file", UserWarning, stacklevel=2)
|
|
590
|
+
else:
|
|
591
|
+
warnings.warn(f"{pattern} not found", UserWarning, stacklevel=2)
|
|
592
|
+
|
|
593
|
+
if not file_inputs:
|
|
594
|
+
print("Error: No valid files found and no stdin data", file=sys.stderr)
|
|
595
|
+
sys.exit(1)
|
|
596
|
+
|
|
597
|
+
lhecargs = LHECheckArgs(
|
|
598
|
+
positive_mass=not args.no_positive_mass,
|
|
599
|
+
positive_mass_abs=args.positive_mass_abs,
|
|
600
|
+
onshell=not args.no_onshell,
|
|
601
|
+
onshell_rel=args.onshell_rel,
|
|
602
|
+
onshell_abs=args.onshell_abs,
|
|
603
|
+
total_momentum=not args.no_total_momentum,
|
|
604
|
+
total_momentum_rel=args.total_momentum_rel,
|
|
605
|
+
total_momentum_abs=args.total_momentum_abs,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
lhecheck_summary = get_lhechecksummary(file_inputs, lhecargs)
|
|
609
|
+
lheas = lhecheck_summary.print()
|
|
610
|
+
if lheas.total_violations != 0:
|
|
611
|
+
print("=" * 60)
|
|
612
|
+
lheas.print()
|
|
613
|
+
|
|
614
|
+
# Exit with appropriate code
|
|
615
|
+
sys.exit(0 if lheas.total_violations == 0 else 1)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
if __name__ == "__main__":
|
|
619
|
+
main()
|