Skip to content

Commit 7bc4dbc

Browse files
committed
Added additional bindings an a sample phase space writing script
1 parent b2de35b commit 7bc4dbc

File tree

9 files changed

+595
-45
lines changed

9 files changed

+595
-45
lines changed

.gitignore

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,24 @@ config.status
22
build
33
tests
44
.vscode
5+
.venv
56
docs/latex
6-
docs/html
7+
docs/html
8+
python/build/
9+
python/dist/
10+
python/**/*.egg-info/
11+
python/**/__pycache__/
12+
python/**/*.pyc
13+
python/**/*.pyo
14+
python/**/*.so
15+
python/**/*.dylib
16+
python/**/*.dll
17+
python/.eggs/
18+
python/*.egg
19+
python/.pytest_cache/
20+
python/.coverage
21+
python/htmlcov/
22+
python/.tox/
23+
python/.venv/
24+
python/venv/
25+
python/ENV/

python/examples/read_phase_space.py

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,29 @@
33
Example: List supported formats and read a phase space file.
44
55
Usage:
6-
python read_phase_space.py /path/to/file.IAEAphsp [--format IAEA] [--limit N]
6+
python read_phase_space.py <file> [options]
77
python read_phase_space.py --formats-only
88
9-
Arguments:
10-
path Path to a phase space file to read. If omitted with --formats-only,
11-
the script just lists formats and exits.
9+
Examples:
10+
python read_phase_space.py file.IAEAphsp
11+
python read_phase_space.py file.root --ROOT-format OpenGATE
12+
python read_phase_space.py file.root --ROOT-format TOPAS --limit 10
1213
13-
Options:
14-
--format NAME Explicit format name (e.g., IAEA, EGS, TOPAS, penEasy).
14+
This script passes all command line arguments through to ParticleZoo's C++ argument
15+
parser, which handles format-specific options automatically.
16+
17+
Note: The filename must come first, before any optional arguments.
18+
19+
Common options:
1520
--limit N Print at most N particles (default: all).
1621
--formats-only Only list formats and exit.
22+
23+
Format-specific options are automatically supported (e.g., --ROOT-format, --iaea-ignore-zlast, etc.)
24+
Run with --help to see all available options.
1725
"""
1826

1927
from __future__ import annotations
20-
import argparse
2128
import sys
22-
from typing import Optional
2329

2430
import particlezoo as pz
2531

@@ -33,39 +39,68 @@ def print_supported_formats() -> None:
3339
))
3440

3541

36-
def create_reader(path: str, fmt: Optional[str]) -> pz.PhaseSpaceFileReader:
37-
if fmt:
38-
return pz.create_reader_for_format(fmt, path)
39-
return pz.create_reader(path)
40-
41-
4242
def print_particle_summary(p: pz.Particle, idx: int) -> None:
4343
tname = pz.get_particle_type_name(p.type)
44+
# Convert to human-readable units for display
45+
ke_mev = p.kinetic_energy / pz.MeV
46+
x_cm = p.x / pz.cm
47+
y_cm = p.y / pz.cm
48+
z_cm = p.z / pz.cm
49+
4450
print(
45-
f"[{idx}] type={tname:<20} KE={p.kinetic_energy:.6g}"
46-
f" pos=({p.x:.6g},{p.y:.6g},{p.z:.6g})"
51+
f"[{idx}] type={tname:<20} KE={ke_mev:.6g} MeV"
52+
f" pos=({x_cm:.6g},{y_cm:.6g},{z_cm:.6g}) cm"
4753
f" dir=({p.px:.6g},{p.py:.6g},{p.pz:.6g})"
4854
f" w={p.weight:.6g} newHist={p.is_new_history}"
4955
)
5056

5157

52-
def main(argv: list[str]) -> int:
53-
ap = argparse.ArgumentParser(description="List formats and read a phase space file")
54-
ap.add_argument("path", nargs="?", help="Path to phase space file")
55-
ap.add_argument("--format", dest="format", help="Explicit format name to use")
56-
ap.add_argument("--limit", type=int, default=None, help="Print at most N particles")
57-
ap.add_argument("--formats-only", action="store_true", help="Only list formats and exit")
58-
args = ap.parse_args(argv)
5958

60-
print_supported_formats()
61-
if args.formats_only and not args.path:
59+
def main(argv: list[str]) -> int:
60+
# Parse arguments using ParticleZoo's C++ argument parser
61+
# This will handle --help, --formats, and all format-specific options automatically
62+
usage_message = (
63+
"Usage: read_phase_space.py <file> [options]\n\n"
64+
"Example script to read phase space files.\n\n"
65+
"Options:\n"
66+
" --limit N Print at most N particles\n"
67+
" --formats-only Only list formats and exit\n"
68+
)
69+
70+
# Check for --formats-only flag manually before parsing
71+
if "--formats-only" in argv:
72+
print_supported_formats()
6273
return 0
74+
75+
# Extract the limit option manually (not a format-specific option)
76+
limit = None
77+
clean_argv = []
78+
skip_next = False
79+
for i, arg in enumerate(argv):
80+
if skip_next:
81+
skip_next = False
82+
continue
83+
if arg == "--limit":
84+
if i + 1 < len(argv):
85+
try:
86+
limit = int(argv[i + 1])
87+
skip_next = True
88+
continue
89+
except ValueError:
90+
pass
91+
clean_argv.append(arg)
92+
93+
# Parse remaining arguments through C++ parser (requires at least 1 positional arg: the file path)
94+
options = pz.ArgParser.parse_args(clean_argv, usage_message, min_positional_args=1)
95+
96+
# Extract the file path (first positional argument)
97+
filepath = options.extract_positional(0)
98+
99+
print_supported_formats()
100+
print()
63101

64-
if not args.path:
65-
ap.error("path is required unless --formats-only is provided")
66-
67-
# Create reader
68-
reader = create_reader(args.path, args.format)
102+
# Create reader with parsed options - format detection and options handled automatically
103+
reader = pz.create_reader(filepath, options)
69104

70105
# File summary
71106
try:
@@ -89,7 +124,7 @@ def main(argv: list[str]) -> int:
89124
for idx, p in enumerate(reader):
90125
print_particle_summary(p, idx)
91126
count += 1
92-
if args.limit is not None and count >= args.limit:
127+
if limit is not None and count >= limit:
93128
break
94129

95130
print(f"\nRead {count} particles. Histories read: {reader.get_histories_read()}.")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example: Write a phase space file.
4+
5+
Usage:
6+
python write_phase_space.py <output_file> [options]
7+
8+
Examples:
9+
python write_phase_space.py output.IAEAphsp
10+
python write_phase_space.py output.root --ROOT-format OpenGATE
11+
python write_phase_space.py output.phsp --TOPAS-format binary
12+
13+
This script demonstrates how to create a phase space file writer and write particles.
14+
"""
15+
16+
from __future__ import annotations
17+
import sys
18+
import math
19+
20+
import particlezoo as pz
21+
22+
23+
def create_sample_particles(count: int = 100) -> list[pz.Particle]:
24+
"""Create a list of sample particles for demonstration."""
25+
particles = []
26+
27+
for i in range(count):
28+
# Create electrons with varying energies and positions
29+
# Simulate a simple beam diverging from the origin
30+
angle = (i / count) * 2 * pz.PI
31+
energy = (1.0 + (i % 10) * 0.5) * pz.MeV # 1.0 to 5.5 MeV
32+
33+
# Position on a plane at z=0
34+
x = math.cos(angle) * 0.5 * pz.cm # 0.5 cm radius
35+
y = math.sin(angle) * 0.5 * pz.cm
36+
z = 0.0 * pz.cm
37+
38+
# Direction slightly diverging
39+
theta = (i / count) * 0.1 * pz.radian # small divergence angle
40+
phi = angle
41+
dir_x = math.sin(theta) * math.cos(phi)
42+
dir_y = math.sin(theta) * math.sin(phi)
43+
dir_z = math.cos(theta)
44+
45+
# Create electron (PDG code 11)
46+
particle = pz.particle_from_pdg(
47+
pdg=11,
48+
kineticEnergy=energy,
49+
x=x, y=y, z=z,
50+
px=dir_x, py=dir_y, pz=dir_z,
51+
isNewHistory=(i % 5 == 0), # New history every 5 particles
52+
weight=1.0
53+
)
54+
particles.append(particle)
55+
56+
return particles
57+
58+
59+
def main(argv: list[str]) -> int:
60+
usage_message = (
61+
"Usage: write_phase_space.py <output_file> [options]\n\n"
62+
"Example script to write phase space files.\n"
63+
)
64+
65+
# Extract particle count option manually (not a format-specific option)
66+
particle_count = 100
67+
clean_argv = []
68+
skip_next = False
69+
for i, arg in enumerate(argv):
70+
if skip_next:
71+
skip_next = False
72+
continue
73+
if arg == "--count":
74+
if i + 1 < len(argv):
75+
try:
76+
particle_count = int(argv[i + 1])
77+
skip_next = True
78+
continue
79+
except ValueError:
80+
pass
81+
clean_argv.append(arg)
82+
83+
# Parse remaining arguments through C++ parser (requires at least 1 positional arg: the file path)
84+
options = pz.ArgParser.parse_args(clean_argv, usage_message, min_positional_args=1)
85+
86+
# Extract the file path (first positional argument)
87+
output_path = options.extract_positional(0)
88+
89+
print(f"Creating writer for: {output_path}")
90+
91+
# Optional: Set up fixed values (constant properties for all particles)
92+
# This is useful for optimization when certain values don't change
93+
fixed_values = pz.FixedValues()
94+
# Example: If all particles have the same Z position
95+
fixed_values.z_is_constant = True
96+
fixed_values.constant_z = 0.0 * pz.cm
97+
98+
# Create writer with parsed options - format detection and options handled automatically
99+
writer = pz.create_writer(output_path, options, fixed_values)
100+
101+
print(f"Format: {writer.get_phsp_format()}")
102+
print(f"Maximum supported particles: {writer.get_maximum_supported_particles()}")
103+
print(f"\nGenerating and writing {particle_count} particles...")
104+
105+
# Create and write sample particles
106+
particles = create_sample_particles(particle_count)
107+
for particle in particles:
108+
writer.write_particle(particle)
109+
110+
# Close the writer (flushes buffers and finalizes the file)
111+
writer.close()
112+
113+
print(f"\nSuccessfully wrote {writer.get_particles_written()} particles")
114+
print(f"Total histories: {writer.get_histories_written()}")
115+
print(f"Output file: {writer.get_file_name()}")
116+
117+
return 0
118+
119+
120+
if __name__ == "__main__":
121+
raise SystemExit(main(sys.argv[1:]))

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
88

99
[project]
1010
name = "particlezoo"
11-
version = "0.1.0"
11+
version = "1.1.0"
1212
description = "Python bindings for the ParticleZoo C++ library"
1313
readme = "README.md"
1414
requires-python = ">=3.8"

python/setup.py

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from pathlib import Path
2+
import subprocess
3+
import re
24
from setuptools import setup
35
from setuptools.command.build_ext import build_ext
46
from pybind11.setup_helpers import Pybind11Extension, build_ext as build_ext_pybind
@@ -23,19 +25,82 @@
2325
str(Path("..") / "src" / "IAEA" / "IAEAphspFile.cc"),
2426
]
2527

28+
define_macros = [("PYBIND11_DETAILED_ERROR_MESSAGES", "1")]
29+
extra_compile_args = ["-O3", "-fvisibility=hidden", "-std=c++20"]
30+
extra_link_args = []
31+
32+
# Try to read ROOT configuration from config.status
33+
config_status = proj / "config.status"
34+
use_root = False
35+
36+
if config_status.exists():
37+
print(f"Reading configuration from {config_status}")
38+
config_vars = {}
39+
with open(config_status, "r") as f:
40+
for line in f:
41+
# Parse Makefile-style variable assignments: VAR = value
42+
match = re.match(r'^(\w+)\s*=\s*(.*)$', line.strip())
43+
if match:
44+
var_name, var_value = match.groups()
45+
config_vars[var_name] = var_value.strip()
46+
47+
if config_vars.get("USE_ROOT") == "1":
48+
use_root = True
49+
root_cflags = config_vars.get("ROOT_CFLAGS", "").split()
50+
root_libs = config_vars.get("ROOT_LIBS", "").split()
51+
52+
if root_cflags or root_libs:
53+
print("ROOT support enabled (from config.status)")
54+
define_macros.append(("USE_ROOT", "1"))
55+
sources.append(str(Path("..") / "src" / "ROOT" / "ROOTphsp.cc"))
56+
extra_compile_args.extend(root_cflags)
57+
extra_link_args.extend(root_libs)
58+
else:
59+
print("WARNING: USE_ROOT=1 but no ROOT flags found")
60+
use_root = False
61+
62+
if not use_root and not config_status.exists():
63+
# Fallback: try to detect ROOT directly if config.status doesn't exist
64+
print("config.status not found, attempting to detect ROOT...")
65+
try:
66+
root_cflags = subprocess.check_output(
67+
["root-config", "--cflags"],
68+
stderr=subprocess.DEVNULL,
69+
text=True
70+
).strip().split()
71+
root_libs = subprocess.check_output(
72+
["root-config", "--libs"],
73+
stderr=subprocess.DEVNULL,
74+
text=True
75+
).strip().split()
76+
77+
if root_cflags and root_libs:
78+
print("ROOT detected - enabling ROOT support")
79+
define_macros.append(("USE_ROOT", "1"))
80+
sources.append(str(Path("..") / "src" / "ROOT" / "ROOTphsp.cc"))
81+
extra_compile_args.extend(root_cflags)
82+
extra_link_args.extend(root_libs)
83+
else:
84+
print("ROOT found but flags empty - building without ROOT support")
85+
except (subprocess.CalledProcessError, FileNotFoundError):
86+
print("ROOT not found - building without ROOT support")
87+
88+
if not use_root and config_status.exists():
89+
print("ROOT support disabled (per config.status)")
90+
2691
ext_modules = [
2792
Pybind11Extension(
2893
"particlezoo._pz",
2994
sources=[str(Path("src/pybind/module.cpp"))] + sources,
3095
include_dirs=include_dirs,
31-
# Use C++20
3296
cxx_std=20,
33-
define_macros=[("PYBIND11_DETAILED_ERROR_MESSAGES", "1")],
34-
extra_compile_args=["-O3", "-fvisibility=hidden", "-std=c++20"],
97+
define_macros=define_macros,
98+
extra_compile_args=extra_compile_args,
99+
extra_link_args=extra_link_args,
35100
)
36101
]
37102

38103
setup(
39104
cmdclass={"build_ext": build_ext_pybind},
40105
ext_modules=ext_modules,
41-
)
106+
)

0 commit comments

Comments
 (0)