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
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# ruff: noqa: SIM103
|
|
3
|
+
"""
|
|
4
|
+
CLI tool to filter LHE files based on various criteria.
|
|
5
|
+
|
|
6
|
+
This tool filters Les Houches Event (LHE) files based on process ID,
|
|
7
|
+
particle PDG IDs (incoming/outgoing), and event numbers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import pylhe
|
|
17
|
+
|
|
18
|
+
from lheutils.cli.util import create_base_parser
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def matches_process_filter(
|
|
22
|
+
event: pylhe.LHEEvent,
|
|
23
|
+
process_ids: Optional[set[int]],
|
|
24
|
+
exclude_process_ids: Optional[set[int]],
|
|
25
|
+
) -> bool:
|
|
26
|
+
"""Check if event matches process ID filters."""
|
|
27
|
+
if process_ids is not None and event.eventinfo.pid not in process_ids:
|
|
28
|
+
return False
|
|
29
|
+
if exclude_process_ids is not None and event.eventinfo.pid in exclude_process_ids:
|
|
30
|
+
return False
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def matches_particle_filter(
|
|
35
|
+
event: pylhe.LHEEvent,
|
|
36
|
+
incoming_pdgids: Optional[set[int]],
|
|
37
|
+
exclude_incoming_pdgids: Optional[set[int]],
|
|
38
|
+
outgoing_pdgids: Optional[set[int]],
|
|
39
|
+
exclude_outgoing_pdgids: Optional[set[int]],
|
|
40
|
+
) -> bool:
|
|
41
|
+
"""Check if event matches particle PDG ID filters."""
|
|
42
|
+
# Get incoming particles (status -1)
|
|
43
|
+
incoming_particles = [p for p in event.particles if p.status == -1]
|
|
44
|
+
incoming_ids = {p.id for p in incoming_particles}
|
|
45
|
+
|
|
46
|
+
# Get outgoing particles (status 1)
|
|
47
|
+
outgoing_particles = [p for p in event.particles if p.status == 1]
|
|
48
|
+
outgoing_ids = {p.id for p in outgoing_particles}
|
|
49
|
+
|
|
50
|
+
# Check incoming particle filters
|
|
51
|
+
if incoming_pdgids is not None and not incoming_ids.intersection(incoming_pdgids):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
if exclude_incoming_pdgids is not None and incoming_ids.intersection(
|
|
55
|
+
exclude_incoming_pdgids
|
|
56
|
+
):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Check outgoing particle filters
|
|
60
|
+
if outgoing_pdgids is not None and not outgoing_ids.intersection(outgoing_pdgids):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if exclude_outgoing_pdgids is not None and outgoing_ids.intersection(
|
|
64
|
+
exclude_outgoing_pdgids
|
|
65
|
+
):
|
|
66
|
+
return False
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def matches_event_filter(
|
|
71
|
+
event_index: int,
|
|
72
|
+
include_event_ranges: Optional[list[tuple[int, int]]],
|
|
73
|
+
exclude_event_ranges: Optional[list[tuple[int, int]]],
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""Check if event matches event number filters."""
|
|
76
|
+
# event_index is 0-based, but user input is 1-based
|
|
77
|
+
event_number = event_index + 1
|
|
78
|
+
|
|
79
|
+
# Check include ranges
|
|
80
|
+
if include_event_ranges is not None:
|
|
81
|
+
matches_include = False
|
|
82
|
+
for start, end in include_event_ranges:
|
|
83
|
+
if end == -1: # Open upper bound
|
|
84
|
+
if event_number >= start:
|
|
85
|
+
matches_include = True
|
|
86
|
+
break
|
|
87
|
+
elif start <= event_number <= end:
|
|
88
|
+
matches_include = True
|
|
89
|
+
break
|
|
90
|
+
if not matches_include:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Check exclude ranges
|
|
94
|
+
if exclude_event_ranges is not None:
|
|
95
|
+
for start, end in exclude_event_ranges:
|
|
96
|
+
if end == -1: # Open upper bound
|
|
97
|
+
if event_number >= start:
|
|
98
|
+
return False
|
|
99
|
+
elif start <= event_number <= end:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def filter_lhe_file(
|
|
106
|
+
input_file: str,
|
|
107
|
+
rwgt: bool,
|
|
108
|
+
weights: bool,
|
|
109
|
+
output_file: Optional[str] = None,
|
|
110
|
+
process_ids: Optional[set[int]] = None,
|
|
111
|
+
exclude_process_ids: Optional[set[int]] = None,
|
|
112
|
+
incoming_pdgids: Optional[set[int]] = None,
|
|
113
|
+
exclude_incoming_pdgids: Optional[set[int]] = None,
|
|
114
|
+
outgoing_pdgids: Optional[set[int]] = None,
|
|
115
|
+
exclude_outgoing_pdgids: Optional[set[int]] = None,
|
|
116
|
+
include_event_ranges: Optional[list[tuple[int, int]]] = None,
|
|
117
|
+
exclude_event_ranges: Optional[list[tuple[int, int]]] = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Filter an LHE file based on the given criteria."""
|
|
120
|
+
try:
|
|
121
|
+
# Read the input LHE file
|
|
122
|
+
if input_file == "-":
|
|
123
|
+
lhefile = pylhe.LHEFile.frombuffer(sys.stdin)
|
|
124
|
+
else:
|
|
125
|
+
lhefile = pylhe.LHEFile.fromfile(input_file)
|
|
126
|
+
|
|
127
|
+
# Filter events
|
|
128
|
+
def _generator() -> Iterable[pylhe.LHEEvent]:
|
|
129
|
+
for event_index, event in enumerate(lhefile.events):
|
|
130
|
+
# Apply all filters
|
|
131
|
+
if (
|
|
132
|
+
matches_process_filter(event, process_ids, exclude_process_ids)
|
|
133
|
+
and matches_particle_filter(
|
|
134
|
+
event,
|
|
135
|
+
incoming_pdgids,
|
|
136
|
+
exclude_incoming_pdgids,
|
|
137
|
+
outgoing_pdgids,
|
|
138
|
+
exclude_outgoing_pdgids,
|
|
139
|
+
)
|
|
140
|
+
and matches_event_filter(
|
|
141
|
+
event_index, include_event_ranges, exclude_event_ranges
|
|
142
|
+
)
|
|
143
|
+
):
|
|
144
|
+
yield event
|
|
145
|
+
|
|
146
|
+
# Create filtered LHE file
|
|
147
|
+
filtered_lhefile = pylhe.LHEFile(init=lhefile.init, events=_generator())
|
|
148
|
+
|
|
149
|
+
# Output the result
|
|
150
|
+
if output_file:
|
|
151
|
+
filtered_lhefile.tofile(output_file, rwgt=rwgt, weights=weights)
|
|
152
|
+
else:
|
|
153
|
+
# Write to stdout
|
|
154
|
+
filtered_lhefile.write(sys.stdout, rwgt=rwgt, weights=weights)
|
|
155
|
+
|
|
156
|
+
except FileNotFoundError:
|
|
157
|
+
if input_file == "-":
|
|
158
|
+
print("Error: Unable to read from stdin", file=sys.stderr)
|
|
159
|
+
else:
|
|
160
|
+
print(f"Error: File '{input_file}' not found", file=sys.stderr)
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
source = "stdin" if input_file == "-" else f"file '{input_file}'"
|
|
164
|
+
print(f"Error processing {source}: {e}", file=sys.stderr)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def parse_int_list(value: str) -> set[int]:
|
|
169
|
+
"""Parse comma-separated list of integers."""
|
|
170
|
+
try:
|
|
171
|
+
return {int(x.strip()) for x in value.split(",")}
|
|
172
|
+
except ValueError as e:
|
|
173
|
+
err = f"Invalid integer list: {value}"
|
|
174
|
+
raise argparse.ArgumentTypeError(err) from e
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def parse_range_list(value: str) -> list[tuple[int, int]]:
|
|
178
|
+
"""Parse comma-separated list of integers and ranges.
|
|
179
|
+
|
|
180
|
+
Returns list of (start, end) tuples where -1 indicates open bound.
|
|
181
|
+
|
|
182
|
+
Supports:
|
|
183
|
+
- Individual numbers: 5 -> [(5, 5)]
|
|
184
|
+
- Ranges: 5-10 -> [(5, 10)] (inclusive)
|
|
185
|
+
- Lower bound: 5- -> [(5, -1)] (from 5 to end)
|
|
186
|
+
- Upper bound: -10 -> [(1, 10)] (from start to 10)
|
|
187
|
+
- Mixed: 1,5-10,15-,20,-25 -> [(1, 1), (5, 10), (15, -1), (20, 20), (1, 25)]
|
|
188
|
+
"""
|
|
189
|
+
result = []
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
for vitem in value.split(","):
|
|
193
|
+
item = vitem.strip()
|
|
194
|
+
|
|
195
|
+
if "-" not in item:
|
|
196
|
+
# Single number
|
|
197
|
+
num = int(item)
|
|
198
|
+
result.append((num, num))
|
|
199
|
+
elif item.startswith("-"):
|
|
200
|
+
# Upper bound: -N
|
|
201
|
+
upper = int(item[1:])
|
|
202
|
+
result.append((1, upper))
|
|
203
|
+
elif item.endswith("-"):
|
|
204
|
+
# Lower bound: N- (open range)
|
|
205
|
+
lower = int(item[:-1])
|
|
206
|
+
result.append((lower, -1)) # -1 indicates open upper bound
|
|
207
|
+
else:
|
|
208
|
+
# Range: N-M
|
|
209
|
+
parts = item.split("-")
|
|
210
|
+
if len(parts) == 2:
|
|
211
|
+
lower, upper = int(parts[0]), int(parts[1])
|
|
212
|
+
if lower > upper:
|
|
213
|
+
err = f"Invalid range: {item} (start > end)"
|
|
214
|
+
raise ValueError(err)
|
|
215
|
+
result.append((lower, upper))
|
|
216
|
+
else:
|
|
217
|
+
err = f"Invalid range format: {item}"
|
|
218
|
+
raise ValueError(err)
|
|
219
|
+
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
err = f"Invalid range specification: {value} ({e})"
|
|
222
|
+
raise argparse.ArgumentTypeError(err) from e
|
|
223
|
+
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main() -> None:
|
|
228
|
+
"""Main CLI function."""
|
|
229
|
+
parser = create_base_parser(
|
|
230
|
+
description="Filter LHE files based on process ID, particle PDG IDs, and event numbers",
|
|
231
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
232
|
+
epilog="""
|
|
233
|
+
Examples:
|
|
234
|
+
lhefilter input.lhe -o filtered.lhe --process-p 81,82
|
|
235
|
+
lhefilter input.lhe --PROCESS 91 --incoming 21 --outgoing 11,-11
|
|
236
|
+
lhefilter input.lhe --events 1,5,10 --outgoing 13,-13
|
|
237
|
+
lhefilter input.lhe --EVENTS 7 --incoming 2,-2
|
|
238
|
+
lhefilter input.lhe.gz --out 6,-6 | gzip > filtered.lhe.gz
|
|
239
|
+
lhefilter input.lhe --events 10-20 --outgoing 11,-11
|
|
240
|
+
lhefilter input.lhe --events 50- --EVENTS 55-60
|
|
241
|
+
cat input.lhe | lhefilter --outgoing 11,-11
|
|
242
|
+
zcat input.lhe.gz | lhefilter --process-p 81 --outgoing 13,-13
|
|
243
|
+
|
|
244
|
+
Process ID filters:
|
|
245
|
+
--process-p ID[,ID...] Include only events with these process IDs
|
|
246
|
+
--PROCESS/-P ID[,ID...] Exclude events with these process IDs
|
|
247
|
+
|
|
248
|
+
Particle PDG ID filters:
|
|
249
|
+
--incoming/-in ID[,ID...] Include events containing these incoming particles
|
|
250
|
+
--INCOMING/-IN ID[,ID...] Exclude events containing these incoming particles
|
|
251
|
+
--outgoing/-out ID[,ID...] Include events containing these outgoing particles
|
|
252
|
+
--OUTGOING/-OUT ID[,ID...] Exclude events containing these outgoing particles
|
|
253
|
+
|
|
254
|
+
Event filters:
|
|
255
|
+
--events RANGE[,RANGE...] Include events in these ranges (1-indexed)
|
|
256
|
+
Supports: N (single), N-M (range), N- (from N to end), -M (up to M)
|
|
257
|
+
--EVENTS RANGE[,RANGE...] Exclude events in these ranges (1-indexed)
|
|
258
|
+
Supports: N (single), N-M (range), N- (from N to end), -M (up to M)
|
|
259
|
+
|
|
260
|
+
Note: Multiple filters are combined with AND logic.
|
|
261
|
+
PDG ID 0 can be used as wildcard for any particle.
|
|
262
|
+
Open ranges (N-) use -1 internally to represent no upper bound.
|
|
263
|
+
""",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
"input", nargs="?", default="-", help="Input LHE file (default: stdin)"
|
|
268
|
+
)
|
|
269
|
+
parser.add_argument("-o", "--output", help="Output file (default: write to stdout)")
|
|
270
|
+
|
|
271
|
+
# Process ID filters
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
"--process-p",
|
|
274
|
+
type=parse_int_list,
|
|
275
|
+
metavar="ID[,ID...]",
|
|
276
|
+
help="Include only events with these process IDs",
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--PROCESS",
|
|
280
|
+
"-P",
|
|
281
|
+
type=parse_int_list,
|
|
282
|
+
metavar="ID[,ID...]",
|
|
283
|
+
help="Exclude events with these process IDs",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Incoming particle filters
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"--incoming",
|
|
289
|
+
"-in",
|
|
290
|
+
type=parse_int_list,
|
|
291
|
+
metavar="PDGID[,PDGID...]",
|
|
292
|
+
help="Include events containing these incoming particles",
|
|
293
|
+
)
|
|
294
|
+
parser.add_argument(
|
|
295
|
+
"--INCOMING",
|
|
296
|
+
"-IN",
|
|
297
|
+
type=parse_int_list,
|
|
298
|
+
metavar="PDGID[,PDGID...]",
|
|
299
|
+
help="Exclude events containing these incoming particles",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Outgoing particle filters
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--outgoing",
|
|
305
|
+
"-out",
|
|
306
|
+
type=parse_int_list,
|
|
307
|
+
metavar="PDGID[,PDGID...]",
|
|
308
|
+
help="Include events containing these outgoing particles",
|
|
309
|
+
)
|
|
310
|
+
parser.add_argument(
|
|
311
|
+
"--OUTGOING",
|
|
312
|
+
"-OUT",
|
|
313
|
+
type=parse_int_list,
|
|
314
|
+
metavar="PDGID[,PDGID...]",
|
|
315
|
+
help="Exclude events containing these outgoing particles",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Event range filters
|
|
319
|
+
parser.add_argument(
|
|
320
|
+
"--events",
|
|
321
|
+
type=parse_range_list,
|
|
322
|
+
metavar="RANGE[,RANGE...]",
|
|
323
|
+
help="Include events in these ranges (1-indexed). Supports: N (single), N-M (range), N- (from N), -M (up to M)",
|
|
324
|
+
)
|
|
325
|
+
parser.add_argument(
|
|
326
|
+
"--EVENTS",
|
|
327
|
+
type=parse_range_list,
|
|
328
|
+
metavar="RANGE[,RANGE...]",
|
|
329
|
+
help="Exclude events in these ranges (1-indexed). Supports: N (single), N-M (range), N- (from N), -M (up to M)",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
parser.add_argument(
|
|
333
|
+
"--rwgt",
|
|
334
|
+
action="store_true",
|
|
335
|
+
help="Use rwgt section if present in the input file",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
parser.add_argument(
|
|
339
|
+
"--no-weights",
|
|
340
|
+
action="store_true",
|
|
341
|
+
help="Do not preserve event weights in output file",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
args = parser.parse_args()
|
|
345
|
+
|
|
346
|
+
# Validate input file (skip validation for stdin)
|
|
347
|
+
if args.input != "-":
|
|
348
|
+
input_path = Path(args.input)
|
|
349
|
+
if not input_path.exists():
|
|
350
|
+
print(f"Error: Input file '{args.input}' does not exist", file=sys.stderr)
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
# Call the filtering function
|
|
354
|
+
filter_lhe_file(
|
|
355
|
+
rwgt=args.rwgt,
|
|
356
|
+
weights=not args.no_weights,
|
|
357
|
+
input_file=args.input,
|
|
358
|
+
output_file=args.output,
|
|
359
|
+
process_ids=args.process_p,
|
|
360
|
+
exclude_process_ids=args.PROCESS,
|
|
361
|
+
incoming_pdgids=args.incoming,
|
|
362
|
+
exclude_incoming_pdgids=args.INCOMING,
|
|
363
|
+
outgoing_pdgids=args.outgoing,
|
|
364
|
+
exclude_outgoing_pdgids=args.OUTGOING,
|
|
365
|
+
include_event_ranges=args.events,
|
|
366
|
+
exclude_event_ranges=args.EVENTS,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
main()
|
lheutils/cli/lheinfo.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLI tool to display information about LHE files.
|
|
4
|
+
|
|
5
|
+
This tool analyzes Les Houches Event (LHE) files and displays relevant information
|
|
6
|
+
including number of events, process information, particle combinations, etc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import warnings
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TextIO, Union
|
|
17
|
+
|
|
18
|
+
import yaml # type: ignore[import-untyped]
|
|
19
|
+
|
|
20
|
+
import pylhe
|
|
21
|
+
from pylhe.cli.util import dataclass_with_properties_to_dict
|
|
22
|
+
|
|
23
|
+
from lheutils.cli.util import create_base_parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LHEChannel:
|
|
28
|
+
"""Information about a single channel in an LHE file."""
|
|
29
|
+
|
|
30
|
+
incoming_pdgid: list[int]
|
|
31
|
+
outgoing_pdgid: list[int]
|
|
32
|
+
num_events: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class LHEProcess:
|
|
37
|
+
"""Information about a single process in an LHE file."""
|
|
38
|
+
|
|
39
|
+
procId: int
|
|
40
|
+
xSection: float
|
|
41
|
+
error: float
|
|
42
|
+
channels: list[LHEChannel]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class LHEInfo:
|
|
47
|
+
"""Information about a single LHE file."""
|
|
48
|
+
|
|
49
|
+
filepath: str
|
|
50
|
+
beamA: int
|
|
51
|
+
energyA: float
|
|
52
|
+
beamB: int
|
|
53
|
+
energyB: float
|
|
54
|
+
weight_groups: dict[str, int]
|
|
55
|
+
num_events: int
|
|
56
|
+
negative_weighted_events: int
|
|
57
|
+
process_info: list[LHEProcess]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def negative_weighted_events_ratio(self) -> float:
|
|
61
|
+
"""Ratio of negative weighted events to total events."""
|
|
62
|
+
return (
|
|
63
|
+
self.negative_weighted_events / self.num_events
|
|
64
|
+
if self.num_events > 0
|
|
65
|
+
else 0.0
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
lines = []
|
|
70
|
+
lines.append("-" * 60)
|
|
71
|
+
lines.append(f"File: {self.filepath}")
|
|
72
|
+
|
|
73
|
+
# Beam information
|
|
74
|
+
lines.append(f"Beam A: {self.beamA} @ {self.energyA} GeV")
|
|
75
|
+
lines.append(f"Beam B: {self.beamB} @ {self.energyB} GeV")
|
|
76
|
+
# Weight groups
|
|
77
|
+
if self.weight_groups:
|
|
78
|
+
lines.append(" Weight Groups:")
|
|
79
|
+
for name, count in self.weight_groups.items():
|
|
80
|
+
lines.append(f" {name}: {count} weights")
|
|
81
|
+
# Number of events
|
|
82
|
+
lines.append(
|
|
83
|
+
f"Number of events: {self.num_events} (negative: {self.negative_weighted_events_ratio:.2%})"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Process information
|
|
87
|
+
processes = self.process_info
|
|
88
|
+
if processes:
|
|
89
|
+
for proc in processes:
|
|
90
|
+
lines.append(
|
|
91
|
+
f"Process {proc.procId} cross-section: ({proc.xSection:.3e} +- {proc.error:.3e}) pb"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
channels = proc.channels
|
|
95
|
+
if channels:
|
|
96
|
+
# Sort channels by num_events in descending order
|
|
97
|
+
sorted_channels = sorted(
|
|
98
|
+
channels, key=lambda ch: ch.num_events, reverse=True
|
|
99
|
+
)
|
|
100
|
+
for channel in sorted_channels:
|
|
101
|
+
percentage = 100 * channel.num_events / self.num_events
|
|
102
|
+
lines.append(
|
|
103
|
+
f" {channel.incoming_pdgid} -> {channel.outgoing_pdgid}: {channel.num_events:,} events ({percentage:.1f}%)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_lheinfo(filepath_or_fileobj: Union[str, TextIO]) -> LHEInfo:
|
|
110
|
+
# Read LHE file
|
|
111
|
+
if isinstance(filepath_or_fileobj, str):
|
|
112
|
+
lhefile = pylhe.LHEFile.fromfile(filepath_or_fileobj)
|
|
113
|
+
file_display_name = filepath_or_fileobj
|
|
114
|
+
else:
|
|
115
|
+
lhefile = pylhe.LHEFile.frombuffer(filepath_or_fileobj)
|
|
116
|
+
file_display_name = "<stdin>"
|
|
117
|
+
init_info = lhefile.init.initInfo
|
|
118
|
+
|
|
119
|
+
initial_final_combinations: Counter[
|
|
120
|
+
tuple[int, tuple[int, ...], tuple[int, ...]]
|
|
121
|
+
] = Counter()
|
|
122
|
+
|
|
123
|
+
num_events = 0
|
|
124
|
+
num_negative_weighted_events = 0
|
|
125
|
+
for event in lhefile.events:
|
|
126
|
+
num_events += 1
|
|
127
|
+
if event.eventinfo.weight < 0:
|
|
128
|
+
num_negative_weighted_events += 1
|
|
129
|
+
initial = []
|
|
130
|
+
final = []
|
|
131
|
+
|
|
132
|
+
for particle in event.particles:
|
|
133
|
+
if particle.status == -1: # Incoming particles
|
|
134
|
+
initial.append(particle.id)
|
|
135
|
+
elif particle.status == 1: # Outgoing particles
|
|
136
|
+
final.append(particle.id)
|
|
137
|
+
|
|
138
|
+
# Count initial particles
|
|
139
|
+
initial_tuple = tuple(sorted(initial))
|
|
140
|
+
|
|
141
|
+
# Count final particles
|
|
142
|
+
final_tuple = tuple(sorted(final))
|
|
143
|
+
|
|
144
|
+
# Count initial -> final combinations
|
|
145
|
+
combination = (event.eventinfo.pid, initial_tuple, final_tuple)
|
|
146
|
+
initial_final_combinations[combination] += 1
|
|
147
|
+
|
|
148
|
+
return LHEInfo(
|
|
149
|
+
filepath=file_display_name,
|
|
150
|
+
beamA=init_info.beamA,
|
|
151
|
+
energyA=init_info.energyA,
|
|
152
|
+
beamB=init_info.beamB,
|
|
153
|
+
energyB=init_info.energyB,
|
|
154
|
+
weight_groups={
|
|
155
|
+
name: len(wg.weights) for name, wg in lhefile.init.weightgroup.items()
|
|
156
|
+
},
|
|
157
|
+
num_events=num_events,
|
|
158
|
+
negative_weighted_events=num_negative_weighted_events,
|
|
159
|
+
process_info=[
|
|
160
|
+
LHEProcess(
|
|
161
|
+
procId=proc.procId,
|
|
162
|
+
xSection=proc.xSection,
|
|
163
|
+
error=proc.error,
|
|
164
|
+
channels=[
|
|
165
|
+
LHEChannel(
|
|
166
|
+
incoming_pdgid=list(incoming_pdgid),
|
|
167
|
+
outgoing_pdgid=list(outgoing_pdgid),
|
|
168
|
+
num_events=count,
|
|
169
|
+
)
|
|
170
|
+
for (
|
|
171
|
+
pid,
|
|
172
|
+
incoming_pdgid,
|
|
173
|
+
outgoing_pdgid,
|
|
174
|
+
), count in initial_final_combinations.items()
|
|
175
|
+
if pid == proc.procId
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
for proc in lhefile.init.procInfo
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class LHESummary:
|
|
185
|
+
"""Summary information from multiple LHE files."""
|
|
186
|
+
|
|
187
|
+
files: list[LHEInfo]
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def total_events(self) -> int:
|
|
191
|
+
total_events = 0
|
|
192
|
+
for lheinfo in self.files:
|
|
193
|
+
total_events += lheinfo.num_events
|
|
194
|
+
return total_events
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def total_negative_weighted_events(self) -> int:
|
|
198
|
+
total_negative_weighted_events = 0
|
|
199
|
+
for lheinfo in self.files:
|
|
200
|
+
total_negative_weighted_events += lheinfo.negative_weighted_events
|
|
201
|
+
return total_negative_weighted_events
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def negative_weighted_events_ratio(self) -> float:
|
|
205
|
+
"""Ratio of negative weighted events to total events."""
|
|
206
|
+
return (
|
|
207
|
+
self.total_negative_weighted_events / self.total_events
|
|
208
|
+
if self.total_events > 0
|
|
209
|
+
else 0.0
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def __str__(self) -> str:
|
|
213
|
+
lines = []
|
|
214
|
+
for lheinfo in self.files:
|
|
215
|
+
lines.append(str(lheinfo))
|
|
216
|
+
lines.append("=" * 60)
|
|
217
|
+
lines.append(
|
|
218
|
+
f"Total number of events: {self.total_events} (negative: {self.negative_weighted_events_ratio:.2%})"
|
|
219
|
+
)
|
|
220
|
+
lines.append("=" * 60)
|
|
221
|
+
return "\n".join(lines)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_lhesummary(
|
|
225
|
+
filepaths_or_fileobjs: list[Union[str, TextIO]],
|
|
226
|
+
) -> LHESummary:
|
|
227
|
+
lheinfos = []
|
|
228
|
+
# Analyze all files
|
|
229
|
+
for filepath_or_fileobj in filepaths_or_fileobjs:
|
|
230
|
+
lheinfo = get_lheinfo(filepath_or_fileobj)
|
|
231
|
+
lheinfos.append(lheinfo)
|
|
232
|
+
|
|
233
|
+
return LHESummary(files=lheinfos)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main() -> None:
|
|
237
|
+
"""Main CLI function."""
|
|
238
|
+
parser = create_base_parser(
|
|
239
|
+
description="Display information about LHE files",
|
|
240
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
241
|
+
epilog="""
|
|
242
|
+
Examples:
|
|
243
|
+
lheinfo file.lhe # Analyze single file (plain format)
|
|
244
|
+
cat file.lhe | lheinfo # Read from stdin
|
|
245
|
+
lheinfo *.lhe # Analyze multiple files
|
|
246
|
+
lheinfo file1.lhe --format=json # Output results in JSON format
|
|
247
|
+
lheinfo file1.lhe --format=yaml # Output results in YAML format
|
|
248
|
+
""",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
parser.add_argument(
|
|
252
|
+
"files",
|
|
253
|
+
nargs="*",
|
|
254
|
+
help="LHE file(s) to analyze (or read from stdin if not provided)",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
"--format",
|
|
259
|
+
choices=["plain", "json", "yaml"],
|
|
260
|
+
default="plain",
|
|
261
|
+
help="Output format (default: plain)",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
args = parser.parse_args()
|
|
265
|
+
|
|
266
|
+
# Check if reading from stdin
|
|
267
|
+
use_stdin = not args.files and not sys.stdin.isatty()
|
|
268
|
+
|
|
269
|
+
file_inputs: list[Union[str, TextIO]] = []
|
|
270
|
+
if use_stdin:
|
|
271
|
+
# Read from stdin
|
|
272
|
+
file_inputs += [sys.stdin]
|
|
273
|
+
else:
|
|
274
|
+
# Expand file paths
|
|
275
|
+
for pattern in args.files:
|
|
276
|
+
path = Path(pattern)
|
|
277
|
+
if path.exists():
|
|
278
|
+
if path.is_file():
|
|
279
|
+
file_inputs.append(str(path))
|
|
280
|
+
else:
|
|
281
|
+
warnings.warn(f"{pattern} is not a file", UserWarning, stacklevel=2)
|
|
282
|
+
if not file_inputs:
|
|
283
|
+
print("Error: No valid files found and no stdin data", file=sys.stderr)
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
summary = get_lhesummary(file_inputs)
|
|
287
|
+
if args.format == "json":
|
|
288
|
+
print(
|
|
289
|
+
json.dumps(
|
|
290
|
+
dataclass_with_properties_to_dict(summary),
|
|
291
|
+
indent=2,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
elif args.format == "yaml":
|
|
295
|
+
print(
|
|
296
|
+
yaml.dump(
|
|
297
|
+
dataclass_with_properties_to_dict(summary),
|
|
298
|
+
sort_keys=False,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
print(str(summary))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
main()
|