From 3d69997f5327bf84c460b1554abece48a36bc5ed Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sat, 10 Oct 2026 16:01:22 +0530 Subject: [PATCH 1/8] Make Geant4 dataset downloads resumable and fail loudly A failed dataset download was silently swallowed: concurrent.futures.wait() never inspects future results, so install_datasets() reported success even when a dataset was missing. Observed with G4NDL (1.06 GB), which then had to be re-fetched from scratch on the next run. - surface worker exceptions via as_completed()/result() instead of wait() - retry with HTTP Range resume, so a dropped connection continues from the last received byte; the MD5 check runs on the complete file - read in 1 MiB chunks instead of 1 KiB and count len(chunk), not the nominal chunk size, in the progress bar Datasets now live with the active Python environment by default (sys.prefix/share/geant4_python_application), overridable through the GEANT4_PYTHON_APPLICATION_DIR environment variable. --- .../files/datasets.py | 39 +++++++++++++++---- .../files/directories.py | 10 ++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/geant4_python_application/files/datasets.py b/src/geant4_python_application/files/datasets.py index 503e4b64..4b03d4c7 100644 --- a/src/geant4_python_application/files/datasets.py +++ b/src/geant4_python_application/files/datasets.py @@ -7,6 +7,7 @@ import subprocess import tarfile import tempfile +import time from collections import namedtuple from pathlib import Path @@ -155,17 +156,37 @@ def _get_total_download_size(datasets_to_download: list[Dataset] = datasets) -> return sum(f.result() for f in concurrent.futures.as_completed(futures)) +chunk_size = 1024 * 1024 +max_attempts = 5 + + def _download_extract_dataset(dataset: Dataset, pbar: tqdm): filename = dataset.filename - urlpath = f"{url}/{filename}.{dataset.version}.tar.gz" - r = requests.get(urlpath, stream=True) - r.raise_for_status() + urlpath = _dataset_url(dataset) - chunk_size = 1024 with tempfile.TemporaryFile() as f: - for chunk in r.iter_content(chunk_size=chunk_size): - f.write(chunk) - pbar.update(chunk_size) + downloaded = 0 + for attempt in range(max_attempts): + # resume where the previous attempt died instead of restarting a multi-GB download + headers = {"Range": f"bytes={downloaded}-"} if downloaded else {} + try: + r = requests.get(urlpath, stream=True, headers=headers, timeout=60) + r.raise_for_status() + if downloaded and r.status_code != 206: + # server ignored the range request, so start over + f.seek(0) + f.truncate() + pbar.update(-downloaded) + downloaded = 0 + for chunk in r.iter_content(chunk_size=chunk_size): + f.write(chunk) + downloaded += len(chunk) + pbar.update(len(chunk)) + break + except requests.RequestException: + if attempt == max_attempts - 1: + raise + time.sleep(2**attempt) f.seek(0) md5 = hashlib.md5() @@ -249,7 +270,9 @@ def install_datasets(show_progress: bool = True): executor.submit(_download_extract_dataset, dataset, pbar) for dataset in datasets_to_download ] - concurrent.futures.wait(futures) + for future in concurrent.futures.as_completed(futures): + # surface download/extraction errors instead of reporting a partial install as success + future.result() if show_progress: total_size_gb = sum( diff --git a/src/geant4_python_application/files/directories.py b/src/geant4_python_application/files/directories.py index 7ffcfda4..cf46585c 100644 --- a/src/geant4_python_application/files/directories.py +++ b/src/geant4_python_application/files/directories.py @@ -2,6 +2,7 @@ import os import pathlib +import sys import tempfile import platformdirs @@ -14,7 +15,14 @@ _dirs = platformdirs.AppDirs(_app_name, _app_author) -_application_directory = _dirs.user_data_dir +# Keep datasets with the active Python environment by default. This makes a +# venv/conda installation relocatable as one unit instead of silently sharing +# data from the user's global Application Support directory. The environment +# variable retains an explicit deployment-time override. +_application_directory = os.environ.get( + "GEANT4_PYTHON_APPLICATION_DIR", + os.path.join(sys.prefix, "share", _app_name), +) def application_directory(path: str | None = None, *, temp: bool = False) -> str: From bd5c07285a0768e8d3e7dbffb79ba4a9e9a754bf Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sat, 10 Oct 2026 16:01:30 +0530 Subject: [PATCH 2/8] Extend the Python API and port the Geant4 examples Add selectable Geant4 reference physics lists and extra physics constructors, uniform magnetic and electric fields, native scoring meshes (NativeScoringMesh), a primary generator wrapper, optical materials, MPI event splitting, and adapters for CAD (STL/tessellated GDML), VTK, Parquet, ROOT summaries and HepMC2 primaries. Port 40 Geant4 11.4.2 examples (basic, extended and advanced) to Python, add a developer guide and a feature-support matrix that separates native Geant4 execution from Python adapters, and cover the new API in the test suite. Hit records tolerate event-field selections that omit time or position. --- .gitignore | 2 + PYTHON_DEVELOPER_GUIDE.md | 1601 +++++++++++++++++ docs/FEATURE_SUPPORT.md | 33 + .../geant4-examples/00_qt_viewer_smoke.py | 27 + .../01_reference_physics_lists.py | 46 + examples/geant4-examples/02_gps_sources.py | 49 + .../geant4-examples/03_scoring_meshes_hits.py | 46 + .../04_magnetic_electric_fields.py | 42 + .../05_optical_scintillation.py | 40 + examples/geant4-examples/06_callbacks.py | 50 + examples/geant4-examples/07_custom_physics.py | 40 + examples/geant4-examples/08_dna_medical.py | 35 + .../09_cad_vtk_root_generators.py | 73 + .../geant4-examples/10_mpi_distributed.py | 49 + .../geant4-examples/11_native_scoring_mesh.py | 23 + .../Geant4-11.4.2-examples-python/AUDIT.md | 40 + .../COMPATIBILITY.md | 78 + .../Geant4-11.4.2-examples-python/README.md | 61 + .../advanced/GoldFoilExperiment/gold_foil.py | 141 ++ .../NanoparticleTherapy/brain_tumor.py | 137 ++ .../NanoparticleTherapy/nano_therapy.py | 137 ++ .../basic/B1/b1.py | 62 + .../basic/B2/b2.py | 61 + .../basic/B3/b3.py | 55 + .../basic/B4/b4.py | 63 + .../basic/B5/b5.py | 54 + .../extended/analysis/AnaEx01/ana_ex01.py | 45 + .../extended/analysis/AnaEx02/ana_ex02.py | 16 + .../extended/analysis/AnaEx03/ana_ex03.py | 19 + .../electromagnetic/TestEm1/test_em1.py | 19 + .../extended/eventgenerator/HepMC/hepmc.py | 39 + .../extended/eventgenerator/exgps/exgps.py | 37 + .../particleGun/particle_gun.py | 66 + .../extended/field/field01/field01.py | 17 + .../extended/field/field02/field02.py | 33 + .../extended/hadronic/Hadr00/hadr00.py | 18 + .../medical/GammaTherapy/gamma_therapy.py | 19 + .../electronScattering/electron_scattering.py | 19 + .../extended/optical/LXe/lxe.py | 44 + .../extended/optical/OpNovice/op_novice.py | 30 + .../extended/optical/wls/wls.py | 44 + .../extended/parameterisations/Par01/par01.py | 33 + .../extended/physicslists/factory/factory.py | 30 + .../radioactivedecay/rdecay01/rdecay01.py | 18 + .../extended/runAndEvent/RE01/re01.py | 45 + .../Geant4-11.4.2-examples-python/run_gdml.py | 40 + .../include/geant4_application/Application.h | 6 +- .../geant4_application/DetectorConstruction.h | 13 + .../include/geant4_application/PhysicsList.h | 2 +- .../PrimaryGeneratorAction.h | 2 + .../src/ActionInitialization.cpp | 5 +- src/geant4_application/src/Application.cpp | 163 +- src/geant4_application/src/DataModel.cpp | 3 +- .../src/DetectorConstruction.cpp | 83 + src/geant4_application/src/PhysicsList.cpp | 7 +- .../src/PrimaryGeneratorAction.cpp | 27 +- .../src/SteppingVerbose.cpp | 5 +- src/geant4_python_application/__init__.py | 15 + src/geant4_python_application/cad.py | 112 ++ src/geant4_python_application/detector.py | 30 + src/geant4_python_application/distributed.py | 45 + src/geant4_python_application/events.py | 5 +- src/geant4_python_application/generator.py | 63 + src/geant4_python_application/geometry.py | 52 + src/geant4_python_application/io.py | 136 ++ src/geant4_python_application/optical.py | 197 ++ src/geant4_python_application/scoring.py | 118 ++ src/python/module.cpp | 16 +- tests/test_analysis.py | 22 +- tests/test_application.py | 14 + tests/test_capabilities.py | 146 ++ tests/test_module.py | 1 + tests/test_run.py | 26 + tools/audit_examples.py | 147 ++ 74 files changed, 5109 insertions(+), 28 deletions(-) create mode 100644 PYTHON_DEVELOPER_GUIDE.md create mode 100644 docs/FEATURE_SUPPORT.md create mode 100644 examples/geant4-examples/00_qt_viewer_smoke.py create mode 100644 examples/geant4-examples/01_reference_physics_lists.py create mode 100644 examples/geant4-examples/02_gps_sources.py create mode 100644 examples/geant4-examples/03_scoring_meshes_hits.py create mode 100644 examples/geant4-examples/04_magnetic_electric_fields.py create mode 100644 examples/geant4-examples/05_optical_scintillation.py create mode 100644 examples/geant4-examples/06_callbacks.py create mode 100644 examples/geant4-examples/07_custom_physics.py create mode 100644 examples/geant4-examples/08_dna_medical.py create mode 100644 examples/geant4-examples/09_cad_vtk_root_generators.py create mode 100644 examples/geant4-examples/10_mpi_distributed.py create mode 100644 examples/geant4-examples/11_native_scoring_mesh.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/README.md create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py create mode 100644 examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py create mode 100644 src/geant4_python_application/cad.py create mode 100644 src/geant4_python_application/distributed.py create mode 100644 src/geant4_python_application/generator.py create mode 100644 src/geant4_python_application/geometry.py create mode 100644 src/geant4_python_application/io.py create mode 100644 src/geant4_python_application/optical.py create mode 100644 src/geant4_python_application/scoring.py create mode 100644 tests/test_capabilities.py create mode 100755 tools/audit_examples.py diff --git a/.gitignore b/.gitignore index 09728090..0178deca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ _version.py +.DS_Store # Byte-compiled / optimized / DLL files __pycache__/ @@ -124,6 +125,7 @@ celerybeat.pid # Environments .env .venv +.venv-test/ env/ venv/ ENV/ diff --git a/PYTHON_DEVELOPER_GUIDE.md b/PYTHON_DEVELOPER_GUIDE.md new file mode 100644 index 00000000..2f4ad057 --- /dev/null +++ b/PYTHON_DEVELOPER_GUIDE.md @@ -0,0 +1,1601 @@ +# Geant4 with Python: a beginner's developer guide + +This guide teaches the Geant4 concepts used by `geant4-python-application` and +shows how to build small simulations in Python. It is intentionally practical: +the Python package provides a high-level application, while the Geant4 kernel +does the particle transport in native C++. + +It is not a complete replacement for the official Geant4 Application +Developer Guide. Use that guide for detailed physics, geometry, and toolkit +reference. Use this guide for the Python interface and this repository's +supported workflow. + +## 1. The simulation idea + +A Geant4 simulation normally has these pieces: + +1. **Geometry**: the world, solids, logical volumes, materials, and placements. +2. **Physics**: the processes that act on particles. +3. **Primary generator**: the particles, energies, positions, and directions + injected into the geometry. +4. **Run and event actions**: code that runs at run/event boundaries. +5. **Scoring**: the quantities extracted from steps, tracks, or volumes. + +In this Python package, geometry is supplied as GDML, physics and generators +are configured through `Application`, and the built-in native actions record +events, tracks, and steps into an Awkward Array. + +The normal lifecycle is: + +```text +create Application -> configure -> initialize -> BeamOn/run -> analyze output +``` + +## 2. Installation + +The package requires Python and a compatible Geant4 installation. From the +repository root: + +```bash +pip install . +``` + +For development, install Geant4 first. The repository's README contains a +source-build example. Your Geant4 installation should include the data files +needed by the physics lists. Check the installation with: + +```bash +python - <<'PY' +import geant4_python_application as g4 + +print("Geant4:", g4.geant4_version) +print("MT available:", g4.Application.multithreading_available()) +print("Physics lists:", g4.Application.available_physics_lists()[:5]) +PY +``` + +## 3. Your first simulation + +Create `first_simulation.py`: + +```python +import geant4_python_application as g4 + +with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + events = app.run(10) + +print(events.fields) +print("number of events:", len(events)) +print(events[:2]) +``` + +`basic_gdml` is a small air world containing a water box. The context manager +starts and stops the worker process safely. `run(10)` transports ten events. + +The result is an `awkward.Array`, which is useful because each event can have a +different number of tracks, steps, and primary particles. + +## 4. Choosing the physics list + +A physics list selects the electromagnetic, decay, hadronic, optical, and +other processes used during transport: + +```python +import geant4_python_application as g4 + +print(g4.Application.available_physics_lists()) + +with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT") as app: + events = app.run(100) +``` + +`custom` is the package's built-in list. Reference lists such as `FTFP_BERT` +come from Geant4. Do not compare results from different lists as though they +were the same physical model. + +Optional constructors can be added before initialization: + +```python +with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT") as app: + app.add_physics("G4EmDNAPhysics_option2") + events = app.run(100) +``` + +## 5. Primary particles + +The convenient generator API configures a particle gun or GPS source: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.generator.use_gps().particle("gamma").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + events = app.run(1000) +``` + +The command interface exposes ordinary Geant4 UI commands: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.commands([ + "/gun/particle e-", + "/gun/energy 10 MeV", + "/gun/position 0 0 -10 cm", + "/gun/direction 0 0 1", + ]) + events = app.run(100) +``` + +For advanced GPS distributions, use `/gps/...` commands through +`app.generator.commands(...)`. + +## 6. Geometry with GDML + +This package does not expose arbitrary Python subclasses of +`G4VUserDetectorConstruction`. Instead, provide a GDML string or file. A +minimal GDML detector has: + +- `` constants +- `` or references to Geant4 NIST materials +- `` +- `` containing logical volumes and physical placements +- a `` that identifies the world volume + +The built-in example uses NIST materials: + +```xml + +``` + +Run your own GDML file like this: + +```python +from pathlib import Path +import geant4_python_application as g4 + +gdml = Path("detector.gdml").read_text() + +with g4.Application(gdml=gdml) as app: + print(app.detector.logical_volumes) + print(app.detector.physical_volumes) + events = app.run(100) +``` + +Inspect geometry before running: + +```python +with g4.Application(gdml=gdml) as app: + print(app.detector.materials) + print(app.detector.logical_volumes) + print(app.detector.physical_volumes) + app.detector.check_overlaps() +``` + +The exact names used for scoring and sensitive volumes are the GDML physical +or logical names, so inspect them rather than guessing. + +## 7. Fields and sensitive volumes + +Uniform global fields can be assigned before the run: + +```python +with g4.Application(gdml=gdml) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.5) # tesla + app.detector.electric_field = (0.0, 0.0, 0.5) # kV/cm + events = app.run(100) +``` + +To focus on a detector volume's steps: + +```python +with g4.Application(gdml=gdml) as app: + app.detector.sensitive_volumes = {"gasVolume"} + events = app.run(100) +``` + +This does not create a custom Python `ProcessHits` implementation. The package +records native Geant4 steps and provides Python-side filtering and scoring. + +## 8. Understanding the event output + +Common fields include event IDs, primary information, tracks, and steps: + +```python +print(events.fields) +print(events.id) +print(events.track.fields) +print(events.track.step.fields) +``` + +Because this is a jagged structure, use Awkward operations rather than assuming +every event has the same number of tracks: + +```python +import awkward as ak + +track_counts = ak.num(events.track.id, axis=1) +print(track_counts) +``` + +The default event fields can be reduced for smaller output with: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.set_event_fields({"run", "id", "track_id", "step_energy", "step_volume"}) + events = app.run(100) +``` + +## 9. Scoring energy deposition + +Post-run scoring is often the easiest way to start: + +```python +import awkward as ak +import geant4_python_application as g4 + +with g4.Application(gdml=g4.basic_gdml) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 10 MeV") + events = app.run(100) + +edep = g4.Scoring.energy_deposit(events) +print("energy per event in keV:", ak.to_numpy(edep)) + +edep_box = g4.Scoring.energy_deposit(events, volume="box") +mesh, edges = g4.Scoring.energy_mesh(events, bins=(20, 20, 20)) +``` + +For native Geant4 scoring meshes: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + mesh = g4.NativeScoringMesh(app, name="dose") + mesh.box(half_size=(250, 250, 250), bins=(20, 20, 20), unit="mm") + app.run(1000) + mesh.dump("dose.out") +``` + +## 10. Saving results + +```python +from pathlib import Path +from geant4_python_application import io + +io.events_to_parquet(events, Path("events.parquet")) +``` + +ROOT output is available when `uproot` is installed: + +```python +io.events_to_root(events, "events.root") +``` + +## 11. Visualization + +Visualization requires installing/building the package with Qt/OpenGL support: + +```bash +pip install . -Ccmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON +``` + +Then: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + app.visualize() +``` + +Visualization is for inspecting geometry and tracks; production runs should +usually be performed in batch mode without the viewer. + +## 12. Multithreading and MPI + +Use native Geant4 CPU multithreading when the linked Geant4 was built with +multithreading: + +```python +with g4.Application(gdml=g4.basic_gdml, n_threads=8) as app: + events = app.run(100000) +``` + +Check availability with: + +```python +print(g4.Application.multithreading_available()) +``` + +For MPI, launch multiple Python processes and split the event count by rank: + +```python +from geant4_python_application import distributed as dist + +rank, size = dist.get_rank_size() +_, count = dist.split_count(100000, rank, size) + +with g4.Application(gdml=g4.basic_gdml, seed=1000 + rank) as app: + events = app.run(count) + g4.io.events_to_parquet(events, f"events_rank_{rank}.parquet") +``` + +Run it with: + +```bash +mpiexec -n 4 python mpi_run.py +``` + +MPI splitting and file merging are intentionally left to the application. Use +unique seeds and unique output files for each rank. + +## 13. Reproducibility and validation + +Set a seed for repeatable runs: + +```python +with g4.Application(gdml=g4.basic_gdml, seed=1234) as app: + events = app.run(1000) +``` + +Results can still vary with different Geant4 versions, physics lists, +thread counts, platforms, and random-engine behavior. Validate a simulation by +checking geometry overlaps, inspecting particle/energy distributions, and +running enough events for the statistical uncertainty to be meaningful. + +## 14. What this interface does not currently expose + +The current package is a high-level Python application rather than a complete +Python binding for every Geant4 class. It does not currently provide arbitrary +Python subclasses of detector construction, sensitive detectors, physics +processes, fields, or action classes. It also does not provide CUDA/GPU +transport. For those use cases, use native C++ Geant4 or a lower-level binding +such as `geant4_pybind`. + +## 15. A recommended learning path + +Work through these stages in order: + +1. Run `basic_gdml` with ten events. +2. Change the particle, energy, position, and direction. +3. Inspect `events.fields`, tracks, and steps. +4. Score energy deposition in a named volume. +5. Edit or generate a GDML detector. +6. Add fields and optical physics. +7. Save results to Parquet and analyze them with NumPy/Awkward. +8. Increase event counts and enable Geant4 multithreading. +9. Use MPI splitting for independent distributed jobs. + +The repository's numbered examples in `examples/geant4-examples/` correspond to +these topics and are useful as runnable companion exercises. + +# Part II - Building a complete application + +## 16. The application state machine + +The Python `Application` is a thin controller around a native Geant4 +application. It starts a child process, sends configuration calls to the +native application, and returns data to the parent Python process. This design +keeps the Geant4 kernel isolated and means that configuration must happen in +the order expected by Geant4. + +The useful states are: + +| State | What you can do | +| --- | --- | +| Created | Set Python constructor options such as GDML, physics, seed, and thread count. | +| Started | Configure the manager, physics, detector, and generator. | +| Initialized | Inspect the initialized detector and run events. | +| Running | Geant4 transports events and records native actions. | +| Finished | Analyze or persist the returned Awkward Array in Python. | + +Most configuration must occur before the first call to `run`. For example, +physics constructors cannot be added after initialization: + +```python +with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT") as app: + app.add_physics("G4EmExtraPhysics") + app.detector.sensitive_volumes = {"box"} + app.initialize() + events = app.run(100) +``` + +Calling `run` automatically initializes a correctly configured application, so +explicit `initialize()` is optional unless you want to validate setup first. + +## 17. A complete reproducible example + +The following example is a good template for new projects: + +```python +from pathlib import Path + +import awkward as ak +import geant4_python_application as g4 + + +def main(): + output = Path("events.parquet") + + with g4.Application( + gdml=g4.basic_gdml, + physics="FTFP_BERT", + seed=2025, + n_threads=4, + ) as app: + app.set_event_fields({ + "run", "id", "track_id", "track_particle", + "step_energy", "step_volume", "step_position", + }) + app.generator.particle("gamma").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + app.detector.sensitive_volumes = {"box"} + events = app.run(10_000) + + deposited = g4.Scoring.energy_deposit(events, volume="box") + print("events:", len(events)) + print("mean deposited energy [keV]:", float(ak.mean(deposited))) + g4.io.events_to_parquet(events, output) + + +if __name__ == "__main__": + main() +``` + +Keep the simulation and analysis stages conceptually separate. The simulation +should produce a stable event record; analysis should be rerunnable without +transporting particles again. + +## 18. Units, coordinates, and naming + +Geant4 is unit-aware internally. In this interface, generator and field helper +methods take explicit unit strings, while GDML also declares units on numeric +values. Never silently mix millimetres, centimetres, metres, or tesla. + +```python +app.generator.position(0, 0, -10, "cm") +app.generator.energy(5, "MeV") +app.detector.magnetic_field = (0, 0, 1.0) # tesla +``` + +Positions in the event output and Python scoring mesh are expressed in +millimetres, and deposited energies are expressed in keV. Volume names are +case-sensitive strings originating in GDML. + +A useful naming convention is: + +```text +World world physical volume +detectorVolume logical volume +detector physical placement +``` + +Use unique, descriptive names. A name that is convenient in a GDML file is also +the name you will use later for filtering, scoring, and diagnostics. + +## 19. Geometry design with GDML + +### 19.1 The geometry hierarchy + +The hierarchy is: + +```text +solid -> logical volume -> physical volume -> world +``` + +A solid describes shape and dimensions. A logical volume combines a solid and +a material. A physical volume places a logical volume inside its mother. The +world is the top-level physical geometry used by the setup. + +### 19.2 A small custom detector + +Save this as `detector.gdml` and pass its text to the application: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +Then validate and run: + +```python +from pathlib import Path +import geant4_python_application as g4 + +gdml = Path("detector.gdml").read_text() +with g4.Application(gdml=gdml) as app: + print(app.detector.materials) + print(app.detector.physical_volumes) + app.detector.check_overlaps() + events = app.run(100) +``` + +### 19.3 Overlaps and navigation errors + +Overlapping daughter volumes can cause physically incorrect navigation. Check +overlaps after loading a detector and before large production runs. If a track +behaves unexpectedly, inspect the volume names, placement coordinates, mother +volume, and units first. + +## 20. Materials and optical properties + +The repository's GDML examples use Geant4 NIST material names such as +`G4_AIR`, `G4_WATER`, and `G4_Si`. This is the simplest way to begin. For a +custom material, define its density and composition in GDML according to the +Geant4 GDML schema. + +Optical transport needs both optical physics and optical material properties. +For example, a material that should transmit photons needs a refractive index +table. Merely setting `optical=True` does not invent optical properties: + +```python +from geant4_python_application import optical as gopt + +with g4.Application(gdml=gopt.optical_water_gdml, optical=True) as app: + app.commands(gopt.scintillation_commands(yield_factor=1.0)) + events = app.run(1_000) +``` + +Inspect the output particle field to distinguish optical photons from the +primary particle and other secondaries. + +## 21. Particle sources in detail + +### 21.1 Particle-gun workflow + +Use a particle gun when every event has a simple, controlled primary: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.generator.particle("proton") + app.generator.energy(100, "MeV") + app.generator.position(0, 0, -20, "cm") + app.generator.direction(0, 0, 1) + events = app.run(100) +``` + +### 21.2 General Particle Source + +Use GPS commands for distributions in position, angle, or energy: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.generator.use_gps() + app.generator.commands([ + "/gps/particle gamma", + "/gps/ene/type Mono", + "/gps/ene/mono 1 MeV", + "/gps/pos/type Point", + "/gps/pos/centre 0 0 -10 cm", + "/gps/direction 0 0 1", + ]) + events = app.run(100) +``` + +### 21.3 Event-by-event primary arrays + +For a predefined beam, provide an Awkward Array containing primary fields. This +is useful when the source comes from a file or another simulation: + +```python +import awkward as ak + +primaries = ak.Array({ + "particle": ["gamma", "gamma", "e-"], + "energy": [1.0, 2.0, 5.0], + "position": {"x": [0, 0, 0], "y": [0, 0, 0], "z": [-10, -10, -10]}, + "direction": {"x": [0, 0, 0], "y": [0, 0, 0], "z": [1, 1, 1]}, +}) + +with g4.Application(gdml=g4.basic_gdml) as app: + events = app.run(primaries) +``` + +Test array-driven primaries in serial mode first. The repository currently +documents limitations for this input path in multithreaded runs. + +## 22. Physics choices and statistical thinking + +A simulation result is determined by geometry, materials, source, physics list, +production cuts, random seed, and event count. Changing any of these changes +the result or its interpretation. + +Use a small run for functional validation, then a larger run for physics: + +```python +for n_events in (1, 10, 1_000, 100_000): + with g4.Application(gdml=g4.basic_gdml, seed=100 + n_events) as app: + events = app.run(n_events) + print(n_events, len(events)) +``` + +Do not interpret a single Monte Carlo event as a measurement. Report the sample +size and an uncertainty or confidence interval for the quantity of interest. + +## 23. Tracking, steps, and secondaries + +An event contains primary vertices and can contain many tracks. A track is +advanced through a sequence of steps. A step can end because of geometry, a +physics interaction, a field integration limit, or another tracking condition. + +Useful questions to ask in analysis are: + +```python +print(events.track.particle) +print(events.track.creator_process) +print(events.track.step.volume) +print(events.track.step.process) +``` + +For example, select positive-energy steps and inspect their volumes: + +```python +hits = events.hits() +detector_hits = hits[hits.volume == "box"] +``` + +The exact Awkward slicing expression can depend on the nesting shape, so begin +with `events.fields`, `events.track.fields`, and `events.track.step.fields`. + +## 24. Actions and callbacks in this package + +Native C++ actions are installed by the package and record run, event, track, +and step data. Python callbacks are an offline analysis layer: + +```python +def on_event(event): + print("event", event.id) + + +with g4.Application(gdml=g4.basic_gdml) as app: + events = app.run_with_callbacks(10, on_event=on_event) +``` + +Callbacks do not replace native Geant4 `G4UserRunAction`, `G4UserEventAction`, +or `G4VSensitiveDetector` subclasses. They execute after transport over the +returned records. This makes them convenient for Python analysis, but not +suitable for changing transport behavior during a step. + +## 25. Control commands and diagnostics + +Any command accepted by the configured Geant4 application can be sent through +`app.command` or `app.commands`: + +```python +with g4.Application(gdml=g4.basic_gdml) as app: + print(app.list_commands("/run")) + app.command("/run/verbose 1") + events = app.run(1) +``` + +Use verbose output sparingly. Large verbose logs can dominate runtime and make +it harder to see the actual error. Prefer small event counts while debugging. + +## 26. Visualization as a debugging tool + +The Qt viewer is most useful for answering geometry questions: is the world +present, is the target where expected, and does the primary start in the world? +Build with visualization enabled, then run a few events. Keep visualization +separate from production output and do not use it to estimate physics results. + +## 27. Production run design + +For a production campaign: + +1. Freeze the GDML, physics list, package version, and Geant4 version. +2. Validate the geometry and source with a one-event visualization run. +3. Run a small serial reference sample. +4. Compare a multithreaded sample with the same configuration. +5. Split large jobs into deterministic MPI ranks with unique seeds. +6. Write one output file per rank. +7. Merge and analyze outside the transport process. + +Do not write all ranks to the same file unless the output format and writer are +explicitly designed for concurrent access. + +# Part III - Advanced topics and boundaries + +## 28. Multithreading in detail + +`n_threads=0` selects the serial Geant4 run manager. A positive value selects +the multithreaded run manager and requests that many worker threads: + +```python +with g4.Application(gdml=g4.basic_gdml, n_threads=8) as app: + events = app.run(100_000) +``` + +The linked Geant4 library must have been compiled with multithreading. More +threads are not always faster: geometry initialization, memory pressure, +output construction, and CPU oversubscription can reduce the gain. + +Benchmark with the same event count and seed policy. Validate event counts and +aggregate quantities, but do not require event ordering to be identical across +thread counts. + +## 29. MPI and hybrid execution + +MPI creates independent Python/Geant4 processes. Each rank should construct its +own application and run only its assigned events. A common hybrid strategy is +one MPI rank per node with several Geant4 worker threads per rank: + +```python +rank, size = dist.get_rank_size() +_, count = dist.split_count(1_000_000, rank, size) + +with g4.Application( + gdml=g4.basic_gdml, + n_threads=8, + seed=10_000 + rank, +) as app: + events = app.run(count) + g4.io.events_to_parquet(events, f"events_{rank:04d}.parquet") +``` + +This package provides rank discovery and event splitting, not an automatic +Geant4 MPI manager, collective reduction, or file merger. + +## 30. Performance and memory + +Recording every step for every track can produce large nested arrays. For large +runs: + +- request only the event fields needed for analysis; +- run in batches and write one file per batch; +- avoid Python callbacks for every step unless necessary; +- use native scoring meshes when a full step record is not needed; +- benchmark thread counts on the target machine. + +The `Application` worker process also means that event data is transferred back +to the parent Python process. Treat that transfer as part of the cost of a +high-volume run. + +## 31. Testing a simulation + +A useful test suite has three layers: + +### Configuration tests + +Check that the GDML loads, expected volumes exist, overlaps are absent, and the +selected physics list is available. + +### Transport smoke tests + +Run one to ten events and assert that the result contains the expected fields +and event count. + +### Physics regression tests + +Run a fixed seed and compare distributions or aggregate values with tolerances. +Avoid exact event-by-event comparisons when changing thread count or platform. + +```python +def test_smoke(): + with g4.Application(gdml=g4.basic_gdml, seed=7) as app: + events = app.run(5) + assert len(events) == 5 + assert "id" in events.fields +``` + +## 32. Common failure modes + +**The application says the data files are missing.** Install the Geant4 datasets +and verify the environment used by Python is the same environment used to +install Geant4. + +**`n_threads` fails.** Check `g4.Application.multithreading_available()` and +use `n_threads=0` with a serial Geant4 build. + +**A volume cannot be found.** Print `app.detector.logical_volumes` and +`app.detector.physical_volumes`; GDML names are case-sensitive. + +**Energy deposition is empty.** Confirm that step fields were requested, that +the particle enters the volume, and that the volume filter uses the actual +physical volume name. + +**The detector overlaps.** Check half-length versus full-length conventions, +mother volume dimensions, placement coordinates, and units. + +**The run is unexpectedly slow.** Reduce fields, disable verbose output, use a +smaller diagnostic run, and benchmark thread counts. Optical photon tracking +can be substantially more expensive than the primary transport. + +## 33. Mapping native Geant4 concepts to this Python interface + +| Native Geant4 concept | Python-interface equivalent | +| --- | --- | +| `G4RunManager` | `Application` and its native manager setup | +| `G4VUserDetectorConstruction` | GDML supplied to `Application` | +| `G4VUserPhysicsList` | `physics="..."` and curated `add_physics(...)` | +| `G4VUserPrimaryGeneratorAction` | `app.generator` or `/gun` and `/gps` commands | +| `G4RunManager::BeamOn` | `app.run(n_events)` | +| `G4UserRunAction` / event / track / step actions | Built-in native recording plus offline Python callbacks | +| Sensitive detector / hits | Recorded steps, `events.hits()`, and Python scoring | +| Command-based scoring | `NativeScoringMesh` | +| Geant4 visualization manager | `app.visualize()` with Qt/OpenGL build support | +| Geant4 MT run manager | `n_threads=N` | +| MPI job splitting | `distributed.get_rank_size()` and `split_count()` | + +## 34. Scope boundary + +This guide is deliberately detailed about the supported interface, but the +interface is not a one-to-one Python binding of the entire Geant4 class library. +If your project requires custom C++ user actions, arbitrary physics processes, +custom field equations, native digitizers, parallel worlds, advanced biasing, +or CUDA transport, you will need native Geant4 C++ or a lower-level binding. + +That boundary is useful: use this package when a GDML detector, configured +physics list, standard generator, native transport, and Python analysis are a +good fit. Move to native C++ when transport-time customization is central to +the experiment. + +## 35. Suggested projects + +After completing the examples, build these projects in order: + +1. A monoenergetic gamma beam through the water box, measuring deposited energy. +2. A silicon target with an electron beam and a magnetic field. +3. An optical scintillator with a refractive-index table and optical-photon + count. +4. A GDML detector imported from a CAD/STL workflow. +5. A multithreaded dose map with Parquet output. +6. An MPI campaign that writes one result file per rank and performs a separate + merge and uncertainty analysis. + +For each project, write down the geometry, material, source, physics list, +field, scoring definition, event count, seed policy, and validation checks. + +# Part IV - A thesis-style learning curriculum + +## 36. How to read this book + +This book is organized as a progression rather than a catalogue of APIs. Read +the first five chapters in order. After that, choose a project and return to +the relevant chapters while implementing it. Every serious simulation should +be understandable in four layers: + +1. **Physical question** - what quantity are you trying to estimate? +2. **Monte Carlo model** - what particles, materials, processes, and geometry + represent that question? +3. **Software implementation** - which GDML, Python configuration, and scoring + code implement the model? +4. **Evidence** - what checks show that the result is correct and precise? + +Do not begin by copying a large detector. Begin with a small experiment for +which you can predict the direction of the answer. A simulation becomes useful +when it is falsifiable: you should know what result would convince you that +the geometry, physics, or source is wrong. + +## 37. Monte Carlo foundations + +Geant4 transports individual particles through matter. A run with `N` primary +events is a random sample from a physical model. If `X_i` is the scored value +from event `i`, the sample mean is: + +```text +mean(X) = (X_1 + X_2 + ... + X_N) / N +``` + +For independent events, the standard error of the mean decreases approximately +as `1 / sqrt(N)`. Doubling the precision therefore requires roughly four times +as many events. This is why a visually impressive one-event display is not a +measurement. + +For a quantity with sample variance `s^2`, estimate the standard error as: + +```text +standard_error = s / sqrt(N) +``` + +In Python: + +```python +import numpy as np + +values = ak.to_numpy(g4.Scoring.energy_deposit(events)) +mean = values.mean() +error = values.std(ddof=1) / np.sqrt(len(values)) +print(f"{mean:.4g} +/- {error:.3g} keV") +``` + +This statistical error does not include systematic uncertainty from geometry, +material data, physics-list choice, calibration, or an incorrect source model. + +## 38. The first laboratory: a beam and a target + +### Objective + +Build a controlled experiment in which a monoenergetic electron crosses a water +target. Record the steps and estimate deposited energy per event. + +### Procedure + +```python +from pathlib import Path + +import awkward as ak +import numpy as np +import geant4_python_application as g4 + + +def run_experiment(n_events=1000): + with g4.Application(gdml=g4.basic_gdml, seed=42) as app: + app.set_event_fields({ + "run", "id", "track_particle", "track_id", + "step_energy", "step_volume", "step_position", + }) + app.commands([ + "/gun/particle e-", + "/gun/energy 1 MeV", + "/gun/position 0 0 -300 mm", + "/gun/direction 0 0 1", + ]) + return app.run(n_events) + + +events = run_experiment() +edep = ak.to_numpy(g4.Scoring.energy_deposit(events, volume="box")) +print("N =", len(edep)) +print("mean [keV] =", np.mean(edep)) +print("standard deviation [keV] =", np.std(edep, ddof=1)) +``` + +### Interpretation + +The result is not just a number. Record the particle, energy, target material, +target dimensions, physics list, seed, event count, and volume name alongside +it. A result without configuration metadata cannot be reproduced. + +### Exercises + +1. Change the electron energy from 1 MeV to 10 MeV. +2. Replace the electron with a gamma ray and explain the changed distribution. +3. Move the source outside the world and observe the diagnostic behavior. +4. Set the target volume as sensitive and compare the resulting step records. + +## 39. Designing a detector model + +### 39.1 Start with the question + +If the question is “how much energy reaches the sensor?”, the geometry needs a +sensor volume and a scoring definition. If the question is “what is the track +length in gas?”, the geometry needs the gas volume and the analysis must sum +step lengths or use an appropriate recorded field. Geometry should be driven by +the observable, not by visual complexity. + +### 39.2 World volume + +The world must contain every possible particle position and secondary track. +Make it large enough that particles do not begin outside it, but do not make it +unnecessarily enormous when using detailed navigation. Use a simple material +such as air or vacuum unless the physical question requires something else. + +### 39.3 Detector volume + +Give each important detector component a unique physical name. Put the material +on the logical volume and the position on the physical placement. This lets you +reuse a logical volume and place it several times while retaining distinct +physical names for analysis. + +### 39.4 Geometry validation checklist + +Before physics validation, check: + +- world dimensions contain the source and expected secondaries; +- all solids have the intended full lengths and half lengths; +- daughter placements are inside their mothers; +- rotations use the intended convention; +- material names resolve; +- repeated placements have unique analysis names; +- overlap checks pass; +- a one-event visualization looks correct. + +## 40. Materials as physical models + +Material density affects stopping power and interaction probability. Elemental +composition affects cross sections and secondary production. Optical properties +affect refraction, reflection, absorption, and photon yield. A material name is +therefore not merely a label. + +For each custom material, document: + +- density and temperature; +- elemental or component composition; +- state and pressure where relevant; +- optical property tables and their energy/wavelength domain; +- the source of the values; +- the version of the data used. + +Do not extrapolate optical tables silently beyond their defined domain. If a +simulation depends on a material parameter, include a validation plot or a +small numerical check in the project repository. + +## 41. Selecting a physics list responsibly + +A reference physics list is a coherent collection of models and processes. It +is usually safer than assembling individual processes without understanding +their interactions. Select a list based on particle types, energy range, +target materials, and required precision. + +Use this decision process: + +1. Identify all primary and likely secondary particle species. +2. Identify the lowest and highest relevant energies. +3. Identify whether electromagnetic, hadronic, decay, optical, or DNA physics + is required. +4. Choose a documented reference list that covers the domain. +5. Run a small sensitivity comparison against another plausible list. +6. Record the choice and rationale. + +Adding a constructor such as `G4EmDNAPhysics_option2` is an expert operation. +Confirm that the geometry, energy range, chemistry assumptions, and output +interpretation are appropriate before using it for a scientific conclusion. + +## 42. Source modelling and phase space + +The primary source is often the largest systematic uncertainty. Specify: + +- particle species and charge; +- energy distribution; +- position distribution; +- direction and angular distribution; +- event time if relevant; +- correlations among primaries; +- normalization and exposure. + +A source file may contain correlated variables. Preserve those correlations when +turning it into an Awkward Array. Do not independently shuffle energy and +direction unless the physical source is truly uncorrelated. + +For multiple primaries per event, preserve the event boundaries. One event with +ten correlated primary vertices is not equivalent to ten independent events. + +## 43. Data model and analysis architecture + +The event output has a nested structure because physics is nested: + +```text +run + event + primary vertices + tracks + steps +``` + +Use the event as the unit of statistical analysis. If you flatten all steps and +compute a mean without event weights, events with many steps receive more +influence than events with few steps. Decide whether you need an event-level, +track-level, or step-level observable before flattening. + +### Event-level example + +```python +edep_per_event = g4.Scoring.energy_deposit(events) +mean_edep = ak.mean(edep_per_event) +``` + +### Step-level example + +```python +hits = events.hits(volume="box") +positive_steps = ak.flatten(hits.energy, axis=None) +``` + +These answer different questions. State the choice in the analysis document. + +## 44. Scoring design patterns + +### 44.1 Recorded-step scoring + +Use recorded steps when you need positions, processes, track IDs, or custom +Python analysis. This is flexible but can consume substantial memory. + +### 44.2 Native mesh scoring + +Use `NativeScoringMesh` when you need a spatial energy-deposition map and do not +need every track and step in Python. This delegates scoring to native Geant4. + +### 44.3 Hybrid scoring + +Use a small diagnostic run with full steps, then a large production run with +minimal fields or a native mesh. This gives both interpretability and scale. + +### 44.4 Efficiency and normalization + +If you simulate only a biased or filtered sample, the result needs the correct +weighting. The present high-level package does not expose every Geant4 biasing +class, so treat event filtering as an analysis selection, not as an unbiased +estimate of the original flux. + +## 45. Validation as a scientific workflow + +Validation has three distinct meanings: + +1. **Software validation**: does the program run and produce structurally valid + output? +2. **Physics verification**: does the implementation reproduce a known result + or limiting case? +3. **Experimental validation**: does the simulation agree with measured data + within the combined uncertainty? + +For every new detector, create a validation table: + +| Test | Expected behavior | Result | Status | +| --- | --- | --- | --- | +| Geometry load | All named volumes exist | ... | pass/fail | +| Overlap check | No overlaps in target region | ... | pass/fail | +| Empty field | Straight neutral-particle path | ... | pass/fail | +| Energy conservation | Balance within tolerance | ... | pass/fail | +| Physics comparison | Agreement with reference | ... | pass/fail | +| Thread comparison | Aggregate agreement | ... | pass/fail | + +Keep failed tests visible. A simulation project becomes trustworthy when its +failures are discoverable rather than hidden. + +## 46. Uncertainty studies + +Vary one assumption at a time and compare the resulting distributions: + +- physics list; +- material density or composition; +- detector dimensions; +- source energy or angular spread; +- field strength; +- production settings; +- event count; +- thread count and rank count. + +Use independent seeds for independent systematic scenarios. Use a common-random- +number strategy only when you understand the covariance benefit and can explain +it in the analysis. + +## 47. A capstone project: a small dose study + +### Research question + +Estimate the energy deposited in a water target by a monoenergetic gamma beam, +compare two photon energies, and report a statistical uncertainty. + +### Project structure + +```text +dose-study/ + detector.gdml + run.py + analyze.py + results/ + README.md +``` + +### Run script + +```python +from pathlib import Path +import geant4_python_application as g4 + + +def run_energy(energy_mev, n_events, seed): + gdml = Path("detector.gdml").read_text() + with g4.Application(gdml=gdml, physics="FTFP_BERT", seed=seed) as app: + app.set_event_fields({"id", "step_energy", "step_volume"}) + app.commands([ + "/gun/particle gamma", + f"/gun/energy {energy_mev} MeV", + "/gun/position 0 0 -300 mm", + "/gun/direction 0 0 1", + ]) + events = app.run(n_events) + g4.io.events_to_parquet(events, f"results/events_{energy_mev}MeV.parquet") + return g4.Scoring.energy_deposit(events, volume="target") + + +if __name__ == "__main__": + for energy in (1, 5): + run_energy(energy, n_events=100_000, seed=1000 + energy) +``` + +### Analysis questions + +1. Does the mean deposited energy increase monotonically with beam energy? +2. How does the fraction of zero-deposition events change? +3. Is the difference larger than the combined statistical uncertainty? +4. Does changing from serial to four threads change the aggregate result? +5. What detector or source assumptions dominate the difference? + +The capstone is complete only when the answer includes configuration metadata, +plots or tables, uncertainties, and a statement of limitations. + +## 48. From Python to native Geant4 + +Eventually you may need a feature outside this package's binding boundary. The +conceptual translation remains useful: + +- Python GDML configuration corresponds to native detector construction. +- `app.run` corresponds to `BeamOn`. +- Python scoring corresponds to native run/event/step actions or scorers. +- Offline callbacks correspond to analysis after an event record exists. +- A reference physics-list name corresponds to native physics-list factory use. + +When moving to C++, first reproduce the Python result with the same geometry, +source, physics list, seed policy, and event count. Then introduce the native +customization one feature at a time. This makes it possible to distinguish a +binding difference from a physics-model difference. + +## 49. Final competence checklist + +A reader is ready to use this package independently when they can: + +- explain the run, event, track, and step hierarchy; +- create and validate a GDML detector; +- choose and justify a physics list; +- define a reproducible source; +- configure fields and optical transport where supported; +- choose an event-level or step-level observable; +- produce an uncertainty estimate; +- save and reload results; +- compare serial, multithreaded, and MPI runs; +- diagnose geometry, data, and source failures; +- state which features require native Geant4; +- document a simulation so another person can reproduce it. + +That is the difference between making a Geant4 script run and conducting a +defensible Monte Carlo study. + +# Part V - How to write a Geant4 Python program + +## 50. The anatomy of a simulation program + +A maintainable simulation program usually contains five layers: + +```text +configuration -> detector/source setup -> transport -> scoring -> analysis +``` + +Keep these layers visible in the code. A useful first file is: + +```text +my_simulation/ + run_simulation.py # creates the application and runs events + analyze_results.py # reads output and computes physics quantities + detector.gdml # geometry and material references + results/ # generated output, not source code +``` + +Do not put a 500,000-event run at module import time. Put execution inside +`main()` and protect it with `if __name__ == "__main__":`. This is important +when launching multiple processes with MPI and makes the file importable from +tests or notebooks. + +## 51. Write the smallest runnable program + +Start by writing this exact skeleton: + +```python +import geant4_python_application as g4 + + +def main(): + with g4.Application(gdml=g4.basic_gdml, seed=1234) as app: + events = app.run(10) + print("events:", len(events)) + + +if __name__ == "__main__": + main() +``` + +Line by line: + +- `import ... as g4` loads the public Python interface. +- `main()` gives the program a clear entry point. +- `Application(...)` creates the simulation controller. +- `gdml=...` supplies the detector description. +- `seed=1234` makes the random sequence reproducible for this configuration. +- `with` starts the native application and stops it even if an exception occurs. +- `app.run(10)` transports ten primary events. +- `events` is a nested `awkward.Array`, not a normal list of Python objects. +- The `__main__` guard prevents accidental execution during import. + +Run it from the project directory: + +```bash +python run_simulation.py +``` + +If this fails, do not add physics complexity yet. First fix installation, +dataset, and environment problems with this smallest program. + +## 52. Add one feature at a time + +The safest way to write a simulation is incremental: + +```python +def main(): + with g4.Application(gdml=g4.basic_gdml, seed=1234) as app: + # 1. Configure the source. + app.generator.use_gun() + app.generator.particle("gamma") + app.generator.energy(1, "MeV") + app.generator.position(0, 0, -30, "cm") + app.generator.direction(0, 0, 1) + + # 2. Run a tiny smoke test. + events = app.run(1) + + print(events) +``` + +Run one event, inspect it, and only then increase the event count. This creates +a short feedback loop for source and geometry mistakes. + +## 53. Write a reusable configuration function + +Once the first script works, move configuration into named functions: + +```python +import geant4_python_application as g4 + + +def configure_source(app, particle="gamma", energy_mev=1.0): + app.generator.use_gun() + app.generator.particle(particle) + app.generator.energy(energy_mev, "MeV") + app.generator.position(0, 0, -30, "cm") + app.generator.direction(0, 0, 1) + + +def configure_detector(app): + app.detector.sensitive_volumes = {"box"} + app.detector.check_overlaps() + + +def run_simulation(n_events, seed=1234): + with g4.Application( + gdml=g4.basic_gdml, + physics="FTFP_BERT", + seed=seed, + ) as app: + configure_source(app) + configure_detector(app) + return app.run(n_events) + + +def main(): + events = run_simulation(100) + print("generated", len(events), "events") + + +if __name__ == "__main__": + main() +``` + +The important design choice is that `configure_source` receives an existing +application. It does not create a second application. Geant4 permits only one +kernel/application instance in the worker process. + +## 54. Write a real command-line program + +For research work, hard-coded values should become command-line options: + +```python +import argparse +from pathlib import Path + +import geant4_python_application as g4 + + +def parse_args(): + parser = argparse.ArgumentParser(description="Run a gamma transport study") + parser.add_argument("--events", type=int, default=1000) + parser.add_argument("--energy", type=float, default=1.0) + parser.add_argument("--seed", type=int, default=1234) + parser.add_argument("--threads", type=int, default=0) + parser.add_argument("--gdml", type=Path) + parser.add_argument("--output", type=Path, default=Path("events.parquet")) + return parser.parse_args() + + +def main(): + args = parse_args() + gdml = args.gdml.read_text() if args.gdml else g4.basic_gdml + + with g4.Application( + gdml=gdml, + physics="FTFP_BERT", + n_threads=args.threads, + seed=args.seed, + ) as app: + app.generator.use_gun().particle("gamma").energy(args.energy, "MeV") + app.generator.position(0, 0, -30, "cm").direction(0, 0, 1) + events = app.run(args.events) + + args.output.parent.mkdir(parents=True, exist_ok=True) + g4.io.events_to_parquet(events, args.output) + print(f"wrote {len(events)} events to {args.output}") + + +if __name__ == "__main__": + main() +``` + +Examples: + +```bash +python run_simulation.py --events 100 --energy 1 +python run_simulation.py --events 100000 --energy 5 --threads 8 +python run_simulation.py --gdml detector.gdml --output results/run.parquet +``` + +Command-line arguments turn a script into a reproducible experiment: the +command itself records the most important choices. + +## 55. Write the analysis program separately + +Do not rerun Geant4 just to change a plot or a summary statistic. Save the +event data and write a separate analysis program: + +```python +from pathlib import Path + +import awkward as ak +import numpy as np +import geant4_python_application as g4 + + +def main(): + events = g4.io.events_from_parquet(Path("results/events.parquet")) + edep = ak.to_numpy(g4.Scoring.energy_deposit(events, volume="box")) + mean = float(np.mean(edep)) + error = float(np.std(edep, ddof=1) / np.sqrt(len(edep))) + print(f"N={len(edep)}") + print(f"mean deposited energy = {mean:.5g} +/- {error:.3g} keV") + + +if __name__ == "__main__": + main() +``` + +This separation lets you improve the analysis without changing the simulated +sample. It also makes code review easier: transport code answers “what was +simulated?” and analysis code answers “how was it summarized?” + +## 56. Write a detector as a parameterized project + +For a detector that changes between studies, keep the GDML template and the +Python run code separate. Generate a GDML string from a small configuration +object, or keep several checked-in GDML files with explicit names: + +```python +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class DetectorConfig: + path: Path + target_name: str + + +def load_detector(config: DetectorConfig) -> str: + text = config.path.read_text() + if not text.strip(): + raise ValueError(f"empty GDML file: {config.path}") + return text +``` + +Validate the configuration before starting a long run. Fail early when a file +is missing, a volume name is wrong, or the event count is negative. + +## 57. Write tests before scaling up + +Add tests that exercise the code without requiring a large physics sample: + +```python +def test_source_and_event_count(): + with g4.Application(gdml=g4.basic_gdml, seed=9) as app: + app.generator.particle("gamma").energy(1, "MeV") + events = app.run(3) + + assert len(events) == 3 + assert "id" in events.fields +``` + +Add a geometry test: + +```python +def test_detector_names(): + with g4.Application(gdml=g4.basic_gdml) as app: + assert "box" in app.detector.physical_volumes + app.detector.check_overlaps() +``` + +Tests are not a substitute for physics validation, but they prevent ordinary +programming changes from silently breaking the experiment. + +## 58. A complete writer's template + +Use this template as the starting point for a serious new simulation: + +```python +#!/usr/bin/env python3 +"""Transport primary particles through a GDML detector and save events.""" + +import argparse +from pathlib import Path + +import geant4_python_application as g4 + + +def build_parser(): + parser = argparse.ArgumentParser() + parser.add_argument("--events", type=int, default=1000) + parser.add_argument("--seed", type=int, default=1234) + parser.add_argument("--threads", type=int, default=0) + parser.add_argument("--output", type=Path, default=Path("events.parquet")) + return parser + + +def configure(app): + app.set_event_fields({ + "run", "id", "primaries", "track_particle", + "track_creator_process", "step_energy", "step_volume", + }) + app.generator.use_gps() + app.generator.particle("gamma").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + app.detector.sensitive_volumes = {"box"} + app.detector.check_overlaps() + + +def run(args): + with g4.Application( + gdml=g4.basic_gdml, + physics="FTFP_BERT", + n_threads=args.threads, + seed=args.seed, + ) as app: + configure(app) + return app.run(args.events) + + +def main(): + args = build_parser().parse_args() + if args.events <= 0: + raise SystemExit("--events must be positive") + events = run(args) + args.output.parent.mkdir(parents=True, exist_ok=True) + g4.io.events_to_parquet(events, args.output) + print(f"saved {len(events)} events to {args.output}") + + +if __name__ == "__main__": + main() +``` + +When adapting this template, change one section at a time and keep the +simulation executable after each change. This is how a short educational script +becomes a dependable research program. diff --git a/docs/FEATURE_SUPPORT.md b/docs/FEATURE_SUPPORT.md new file mode 100644 index 00000000..70eb7699 --- /dev/null +++ b/docs/FEATURE_SUPPORT.md @@ -0,0 +1,33 @@ +# Geant4 feature support audit + +This matrix distinguishes native Geant4 execution from Python adapters. A +feature is not marked supported merely because a similarly named proof script +exists. + +| Capability | Status | Current implementation | +|---|---|---| +| Detector construction | Partial | Complete GDML strings/files; no Python subclass of `G4VUserDetectorConstruction` | +| Run/event/track/step actions | Partial | Native built-in C++ actions record events; user callbacks run after transport | +| Multiple primary vertices | Supported | Nested Awkward `primaries` records generate and preserve all vertices in each event | +| Sensitive detectors/hits | Partial | Logical volumes can be marked sensitive and all steps are recorded; no custom Python `ProcessHits` or native typed hit collection | +| Parameterised geometry | Partial | Expanded placements can be generated in Python/GDML; no live `G4VPVParameterisation` callback | +| Physics lists/processes | Partial | Reference lists and a curated set of `G4VPhysicsConstructor` classes; no arbitrary Python process/particle subclass | +| Fields/equations/steppers | Partial | Uniform global B and E with native Geant4 integration; no user equation/stepper binding | +| Optical materials/processes | Supported (GDML) | RINDEX, scintillation, WLS and optical surfaces execute natively; no Python PMT/SD callback | +| Repeated geometry | Adapter | Regular layouts can be expanded to ordinary GDML placements; no live parameterisation callback | +| Native scoring meshes | Supported (energy) | `NativeScoringMesh` drives `G4ScoringManager`; Python `Scoring` remains a separate post-run tool | +| MPI/distributed | Partial | `mpi4py` rank discovery and event splitting; no Geant4 MPI manager or automatic merge | +| DNA | Partial | DNA physics and chemistry constructors are selectable; chemistry scheduler/time-step actions are not exposed | +| ROOT | Adapter | `uproot` summary I/O, not Geant4 ROOT analysis classes | +| VTK | Adapter | Legacy VTK mesh export, not Geant4 VTK visualization classes | +| CAD | Adapter | ASCII STL/tessellated GDML, not STEP/IGES or a CAD kernel | +| Pythia/HepMC | Partial adapter | Minimal HepMC2 primary import; no Pythia native integration | +| DICOM/medical | Partial | GDML medical geometries and dose post-processing; no DICOM image/voxel pipeline | + +## Binding work still required + +Python subclassing of Geant4 virtual interfaces is complicated by the package's +worker-process architecture: arbitrary Python callables must execute safely in +the simulation process and cannot be treated as ordinary pickleable state. New +live callback APIs therefore require an explicit callback transport/lifetime +design, not only pybind11 trampoline declarations. diff --git a/examples/geant4-examples/00_qt_viewer_smoke.py b/examples/geant4-examples/00_qt_viewer_smoke.py new file mode 100644 index 00000000..c03216e0 --- /dev/null +++ b/examples/geant4-examples/00_qt_viewer_smoke.py @@ -0,0 +1,27 @@ +"""00 - Qt interactive viewer (requires display, e.g. XQuartz on macOS). + +Build with Qt first: + CMAKE_PREFIX_PATH=/usr/local:/opt/homebrew \ + python -m pip install -e . --no-build-isolation \ + -Ccmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON + +Then run this script. A Qt window opens; enter Geant4 commands in its +command panel. Close the window to return (app.visualize blocks). +Run: python 00_qt_viewer_smoke.py +""" +from __future__ import annotations + + +def main() -> None: + import geant4_python_application as g4 + + print(f"visualization_available = {g4.Application.visualization_available()}") + assert g4.Application.visualization_available(), "Reinstall with Qt (see docstring)" + with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 100 MeV") + app.visualize() # default: OGL + drawVolume + trajectories + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/01_reference_physics_lists.py b/examples/geant4-examples/01_reference_physics_lists.py new file mode 100644 index 00000000..930f2982 --- /dev/null +++ b/examples/geant4-examples/01_reference_physics_lists.py @@ -0,0 +1,46 @@ +"""01 - Reference physics-list selection. + +Shows listing Geant4 reference lists, running with FTFP_BERT vs custom, +and handling an unknown list name. +Run: python 01_reference_physics_lists.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import geant4_python_application as g4 + + lists = g4.Application.available_physics_lists() + print(f"Available physics lists: {len(lists)}") + print(f"Sample: {lists[:6]}") + assert "custom" in lists + assert "FTFP_BERT" in lists + + # Reference list + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=11) as app: + events = app.run(2) + print(f"FTFP_BERT: ran {len(events)} events") + + # Default custom list (EmOption4 + FTFP_BERT hadronic) + with g4.Application(gdml=g4.basic_gdml, physics="custom", seed=11) as app: + events = app.run(2) + print(f"custom: ran {len(events)} events") + + # Unknown name raises + try: + with g4.Application(gdml=g4.basic_gdml, physics="NOT_A_LIST") as app: + app.run(1) + except Exception as exc: + print(f"Unknown list correctly raised: {type(exc).__name__}: {exc}") + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=11) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 2 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/02_gps_sources.py b/examples/geant4-examples/02_gps_sources.py new file mode 100644 index 00000000..17020635 --- /dev/null +++ b/examples/geant4-examples/02_gps_sources.py @@ -0,0 +1,49 @@ +"""02 - GPS and configurable particle sources. + +Shows particle gun vs General Particle Source (GPS), plus advanced GPS +commands and awkward-array primaries. +Run: python 02_gps_sources.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import awkward as ak + import numpy as np + + import geant4_python_application as g4 + + with g4.Application(gdml=g4.basic_gdml, seed=12) as app: + # Particle gun + app.generator.use_gun() + print(f"generator type: {app.generator.type}") + app.command("/gun/particle gamma") + app.command("/gun/energy 2 MeV") + events = app.run(2) + print(f"gun primaries (keV): {ak.flatten(events.primaries.energy)}") + + # GPS with Python helpers + app.generator.use_gps().particle("gamma").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + print(f"generator type: {app.generator.type}") + events = app.run(2) + energies = ak.to_numpy(ak.flatten(events.primaries.energy)) + print(f"GPS primaries (keV, expect ~2000): {energies}") + assert np.allclose(energies, 2000) + + # Advanced GPS distribution via raw commands + app.generator.commands(["/gps/pos/type Point", "/gps/pos/centre 0 0 0 cm"]) + events = app.run(1) + print(f"advanced GPS: ran {len(events)} event(s)") + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, seed=12) as app: + app.generator.use_gps().particle("gamma").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/03_scoring_meshes_hits.py b/examples/geant4-examples/03_scoring_meshes_hits.py new file mode 100644 index 00000000..af76a60d --- /dev/null +++ b/examples/geant4-examples/03_scoring_meshes_hits.py @@ -0,0 +1,46 @@ +"""03 - Scoring meshes and hit collections. + +Shows offline scoring over recorded steps: per-event energy deposit, +Cartesian energy mesh (numpy histogramdd), and hit filtering. +Run: python 03_scoring_meshes_hits.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import awkward as ak + + import geant4_python_application as g4 + + with g4.Application(gdml=g4.basic_gdml, seed=13) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 10 MeV") + events = app.run(3) + + print(f"event fields: {events.fields}") + + edep = g4.Scoring.energy_deposit(events) + print(f"deposited energy per event (keV): {ak.to_numpy(edep)}") + + mesh, edges = g4.Scoring.energy_mesh(events, bins=(4, 5, 6)) + print(f"mesh shape: {mesh.shape}, total: {mesh.sum():.2f} keV") + + hits = events.hits() + n_hits = len(ak.flatten(hits.energy, axis=None)) + print(f"total positive-energy steps (hits): {n_hits}") + + # Volume-restricted scoring (physical volume name 'box' in basic_gdml) + edep_box = g4.Scoring.energy_deposit(events, volume="box") + print(f"energy in 'box' per event (keV): {ak.to_numpy(edep_box)}") + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, seed=13) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 10 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/04_magnetic_electric_fields.py b/examples/geant4-examples/04_magnetic_electric_fields.py new file mode 100644 index 00000000..eab0e4e7 --- /dev/null +++ b/examples/geant4-examples/04_magnetic_electric_fields.py @@ -0,0 +1,42 @@ +"""04 - Uniform magnetic and electric fields. + +Shows setting a uniform global B field (tesla) and E field (kV/cm). +E uses G4EqMagElectricField + ClassicalRK4; B-only uses G4GlobalMagFieldMessenger. +Run: python 04_magnetic_electric_fields.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import geant4_python_application as g4 + + with g4.Application(gdml=g4.basic_gdml, seed=14) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.0) # tesla + app.detector.electric_field = (0.0, 0.0, 0.5) # kV/cm + print(f"B = {app.detector.magnetic_field} T") + print(f"E = {app.detector.electric_field} kV/cm") + app.command("/gun/particle e-") + app.command("/gun/energy 5 MeV") + events = app.run(2) + print(f"ran {len(events)} events with combined B+E field") + + # B-only (original path) + with g4.Application(gdml=g4.basic_gdml, seed=14) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.5) + print(f"B-only = {app.detector.magnetic_field} T") + events = app.run(1) + print(f"ran {len(events)} event with B-only field") + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, seed=14) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.0) + app.detector.electric_field = (0.0, 0.0, 0.5) + app.command("/gun/particle e-") + app.command("/gun/energy 5 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/05_optical_scintillation.py b/examples/geant4-examples/05_optical_scintillation.py new file mode 100644 index 00000000..a4edd276 --- /dev/null +++ b/examples/geant4-examples/05_optical_scintillation.py @@ -0,0 +1,40 @@ +"""05 - Optical materials, surfaces and scintillation. + +Uses GDML-defined RINDEX/ABSLENGTH/SCINTILLATION + skinsurface (see +Geant4 OpNovice) with G4OpticalPhysics. Verifies opticalphoton production. +Run: python 05_optical_scintillation.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import awkward as ak + + import geant4_python_application as g4 + from geant4_python_application import optical as gopt + + print(f"tuning commands: {gopt.scintillation_commands()}") + + with g4.Application(gdml=gopt.optical_water_gdml, optical=True, seed=15) as app: + app.commands(gopt.scintillation_commands(stack_photons=True, max_cerenkov=100)) + app.command("/gun/particle e-") + app.command("/gun/energy 10 MeV") + app.command("/gun/position 0 0 0 mm") + app.command("/gun/direction 1 0 0") + events = app.run(2) + print(f"ran {len(events)} events with optical physics") + parts = sorted(set(ak.to_list(ak.flatten(events.track.particle, axis=None)))) + print(f"track particles: {parts}") + assert "opticalphoton" in parts, "expected scintillation/Cerenkov photons" + + if "--vis" in sys.argv: + with g4.Application(gdml=gopt.optical_water_gdml, optical=True, seed=15) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 10 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/06_callbacks.py b/examples/geant4-examples/06_callbacks.py new file mode 100644 index 00000000..43e7ff2d --- /dev/null +++ b/examples/geant4-examples/06_callbacks.py @@ -0,0 +1,50 @@ +"""06 - Python run/event/track/step callbacks. + +Shows Application.run_with_callbacks() - an offline callback layer over +the awkward event model (on_run / on_event / on_track / on_step). +Run: python 06_callbacks.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import geant4_python_application as g4 + + counts = {"run": 0, "event": 0, "track": 0, "step": 0} + + def on_run(events) -> None: + counts["run"] += 1 + print(f"on_run: {len(events)} events") + + def on_event(event) -> None: + counts["event"] += 1 + + def on_track(track, event) -> None: + counts["track"] += 1 + + def on_step(step, track, event) -> None: + counts["step"] += 1 + + with g4.Application(gdml=g4.basic_gdml, seed=16) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + events = app.run_with_callbacks( + 3, on_run=on_run, on_event=on_event, on_track=on_track, on_step=on_step + ) + print(f"ran {len(events)} events") + + print(f"callback counts: {counts}") + assert counts["run"] == 1 and counts["event"] == 3 + assert counts["track"] > 0 and counts["step"] > 0 + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, seed=16) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/07_custom_physics.py b/examples/geant4-examples/07_custom_physics.py new file mode 100644 index 00000000..23321e23 --- /dev/null +++ b/examples/geant4-examples/07_custom_physics.py @@ -0,0 +1,40 @@ +"""07 - Custom physics processes. + +Shows registering extra G4VPhysicsConstructors by name on top of a +reference or custom list, before initialize/run. +Run: python 07_custom_physics.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import geant4_python_application as g4 + + print(f"extra physics: {g4.Application.available_extra_physics()}") + + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=17) as app: + app.add_physics("G4EmExtraPhysics") + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + events = app.run(1) + print(f"ran {len(events)} event with G4EmExtraPhysics added") + + # Unknown constructor raises a clear error + try: + with g4.Application(gdml=g4.basic_gdml, seed=17) as app: + app.add_physics("NOT_A_PHYSICS") + except Exception as exc: + print(f"unknown constructor correctly raised: {exc}") + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=17) as app: + app.add_physics("G4EmExtraPhysics") + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/08_dna_medical.py b/examples/geant4-examples/08_dna_medical.py new file mode 100644 index 00000000..d0fd4967 --- /dev/null +++ b/examples/geant4-examples/08_dna_medical.py @@ -0,0 +1,35 @@ +"""08 - DNA chemistry and medical extensions. + +Shows adding Geant4-DNA physics (e.g. option2) for low-energy electron +transport in water. Chemistry constructors are listed but only the +physical stage is run here. +Run: python 08_dna_medical.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import geant4_python_application as g4 + + dna = [x for x in g4.Application.available_extra_physics() if "DNA" in x] + print(f"DNA constructors: {dna}") + + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=19) as app: + app.add_physics("G4EmDNAPhysics_option2") + app.command("/gun/particle e-") + app.command("/gun/energy 100 keV") + events = app.run(1) + print(f"ran {len(events)} event with G4EmDNAPhysics_option2") + + if "--vis" in sys.argv: + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=19) as app: + app.add_physics("G4EmDNAPhysics_option2") + app.command("/gun/particle e-") + app.command("/gun/energy 100 keV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/09_cad_vtk_root_generators.py b/examples/geant4-examples/09_cad_vtk_root_generators.py new file mode 100644 index 00000000..5ae8770a --- /dev/null +++ b/examples/geant4-examples/09_cad_vtk_root_generators.py @@ -0,0 +1,73 @@ +"""09 - CAD, VTK, ROOT and external generators. + +Shows: CAD mesh -> GDML (tessellated), energy mesh -> VTK, events -> +parquet/ROOT, HepMC-ASCII -> primaries. Writes to /tmp. +Run: python 09_cad_vtk_root_generators.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + from pathlib import Path + + import geant4_python_application as g4 + from geant4_python_application import cad + from geant4_python_application import io as gio + + # CAD: cube mesh -> GDML -> run 1 event + vertices, faces = cad.cube_mesh(100.0) + gdml = cad.mesh_to_gdml(vertices, faces) + assert "tessellated" in gdml + print(f"CAD cube: {len(vertices)} vertices, {len(faces)} triangles") + with g4.Application(gdml=gdml, seed=20) as app: + print(f"CAD GDML ran {len(app.run(1))} event") + + # ASCII STL -> GDML (parse-only check) + stl = ( + "solid c\nfacet normal 0 0 1\nouter loop\n" + "vertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\n" + "endloop\nendfacet\nendsolid c\n" + ) + assert "tessellated" in cad.stl_ascii_to_gdml(stl) + print("STL parse OK") + + # Physics run for I/O demos + with g4.Application(gdml=g4.basic_gdml, seed=21) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 5 MeV") + events = app.run(2) + + # VTK + mesh, edges = g4.Scoring.energy_mesh(events, bins=(3, 3, 3)) + vtk_path = gio.mesh_to_vtk(mesh, edges, Path("/tmp/g4_example_mesh.vtk")) + print(f"VTK written: {vtk_path}") + + # Parquet round-trip (always available via pyarrow) + pq = gio.events_to_parquet(events, Path("/tmp/g4_example_events.parquet")) + assert len(gio.events_from_parquet(pq)) == len(events) + print(f"parquet round-trip OK: {pq}") + + # ROOT via uproot (pip install uproot if missing) + try: + rp = gio.events_to_root(events, Path("/tmp/g4_example_events.root")) + back = gio.events_from_root(rp) + assert len(back["id"]) == len(events) + print(f"ROOT round-trip OK: {rp}") + except ImportError as exc: + print(f"ROOT skipped: {exc}") + + # External generator: minimal HepMC2-ASCII -> awkward primaries + hepmc = "E 0 1 1\nP 1 22 0 0 1 1 0 1\nP 2 11 0.1 0 0.5 0.6 0.0005 1\n" + primaries = gio.hepmc_to_primaries(hepmc) + print(f"HepMC primaries: {list(primaries.particle)}") + + if "--vis" in sys.argv: + with g4.Application(gdml=gdml, seed=20) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 5 MeV") + app.visualize() # Qt viewer; close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/10_mpi_distributed.py b/examples/geant4-examples/10_mpi_distributed.py new file mode 100644 index 00000000..3db25a6a --- /dev/null +++ b/examples/geant4-examples/10_mpi_distributed.py @@ -0,0 +1,49 @@ +"""10 - MPI and distributed execution. + +Shows rank/size detection (mpi4py -> env vars -> serial fallback) and +even event splitting. Each rank runs its share; gather with +awkward.concatenate or ROOT/parquet. +Run serial: python 10_mpi_distributed.py +Run MPI: mpiexec -n 2 python 10_mpi_distributed.py +""" +from __future__ import annotations + + +def main() -> None: + import sys + + import geant4_python_application as g4 + from geant4_python_application import distributed as dist + + rank, size = dist.get_rank_size() + print(f"rank {rank}/{size}") + + n_total = 4 + start, count = dist.split_count(n_total, rank, size) + print(f"this rank handles events [{start}:{start + count}] of {n_total}") + + # Each rank runs only its share (here 2 events max per rank for speed) + n_local = min(count, 2) if count else 0 + if n_local: + with g4.Application(gdml=g4.basic_gdml, seed=100 + rank) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + events = app.run(n_local) + print(f"rank {rank} ran {len(events)} event(s), ids: {list(events.id)}") + else: + print(f"rank {rank} has no work") + + # Pure splitting logic checks (run on every rank) + assert dist.split_count(10, 0, 2) == (0, 5) + assert dist.split_count(10, 1, 2) == (5, 5) + print("split logic OK") + + if "--vis" in sys.argv and rank == 0: + with g4.Application(gdml=g4.basic_gdml, seed=100) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + app.visualize() # Qt viewer (rank 0 only); close window to return + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/11_native_scoring_mesh.py b/examples/geant4-examples/11_native_scoring_mesh.py new file mode 100644 index 00000000..476d84dd --- /dev/null +++ b/examples/geant4-examples/11_native_scoring_mesh.py @@ -0,0 +1,23 @@ +"""11 - Native Geant4 scoring mesh through the Python command interface.""" + +from __future__ import annotations + +from pathlib import Path + +import geant4_python_application as g4 + + +def main() -> None: + output = Path("native_energy_mesh.csv") + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=22) as app: + mesh = g4.NativeScoringMesh(app, "waterMesh", "energyDeposit") + mesh.box((250, 250, 250), (10, 10, 10), unit="mm") + app.commands(["/gun/particle e-", "/gun/energy 10 MeV"]) + app.run(10) + mesh.dump(output) + assert output.exists() and output.stat().st_size > 0 + print(f"native Geant4 scoring mesh written to {output.resolve()}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md b/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md new file mode 100644 index 00000000..50c6ebaa --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md @@ -0,0 +1,40 @@ +# Geant4 example compatibility audit + +- Official CMake targets: 262 +- Official GDML files: 144 +- Standalone GDML candidates: 143 +- Python ports passing: 27 +- Python ports failing/timing out: 0 + +| Python port | Status | Seconds | +|---|---:|---:| +| `advanced/GoldFoilExperiment/gold_foil.py` | passed | 3.794 | +| `advanced/NanoparticleTherapy/brain_tumor.py` | passed | 4.751 | +| `advanced/NanoparticleTherapy/nano_therapy.py` | passed | 4.411 | +| `basic/B1/b1.py` | passed | 1.739 | +| `basic/B2/b2.py` | passed | 1.933 | +| `basic/B3/b3.py` | passed | 1.779 | +| `basic/B4/b4.py` | passed | 2.447 | +| `basic/B5/b5.py` | passed | 2.266 | +| `extended/analysis/AnaEx01/ana_ex01.py` | passed | 2.004 | +| `extended/analysis/AnaEx02/ana_ex02.py` | passed | 1.198 | +| `extended/analysis/AnaEx03/ana_ex03.py` | passed | 1.104 | +| `extended/electromagnetic/TestEm1/test_em1.py` | passed | 1.353 | +| `extended/eventgenerator/HepMC/hepmc.py` | passed | 1.063 | +| `extended/eventgenerator/exgps/exgps.py` | passed | 1.899 | +| `extended/eventgenerator/particleGun/particle_gun.py` | passed | 1.755 | +| `extended/field/field01/field01.py` | passed | 1.364 | +| `extended/field/field02/field02.py` | passed | 0.966 | +| `extended/hadronic/Hadr00/hadr00.py` | passed | 1.949 | +| `extended/medical/GammaTherapy/gamma_therapy.py` | passed | 1.333 | +| `extended/medical/electronScattering/electron_scattering.py` | passed | 1.794 | +| `extended/optical/LXe/lxe.py` | passed | 1.556 | +| `extended/optical/OpNovice/op_novice.py` | passed | 1.399 | +| `extended/optical/wls/wls.py` | passed | 1.367 | +| `extended/parameterisations/Par01/par01.py` | passed | 1.592 | +| `extended/physicslists/factory/factory.py` | passed | 1.134 | +| `extended/radioactivedecay/rdecay01/rdecay01.py` | passed | 1.167 | +| `extended/runAndEvent/RE01/re01.py` | passed | 1.376 | +| `run_gdml.py` | utility | | + +Only entries marked `passed` are validated as runnable Python ports. diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md b/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md new file mode 100644 index 00000000..5cab6a65 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md @@ -0,0 +1,78 @@ +# Compatibility with the official Geant4 examples + +This directory contains Python adaptations, not mechanical translations of +the C++ sources. They exercise the same detector concept using functionality +currently exposed by `geant4_python_application`. + +## Runnable adaptations + +| Official family | Python entry point | Coverage | +|---|---|---| +| basic/B1 | `basic/B1/b1.py` | Geometry, beam, volume energy scoring | +| basic/B2 | `basic/B2/b2.py` | Target, tracker chambers, field, step-derived hits | +| basic/B3 | `basic/B3/b3.py` | PET geometry and crystal/patient energy scoring | +| basic/B4 | `basic/B4/b4.py` | Sampling calorimeter and layer energy scoring | +| basic/B5 | `basic/B5/b5.py` | Spectrometer geometry, field, detector energy scoring | +| GoldFoilExperiment | `advanced/GoldFoilExperiment/gold_foil.py` | Rutherford-scattering workflow and CSV output | +| NanoparticleTherapy | `advanced/NanoparticleTherapy/*.py` | Geometric/physics adaptations and dose summaries | +| extended/eventgenerator/exgps | `extended/eventgenerator/exgps/exgps.py` | Native GPS and primary-spectrum output | +| extended/eventgenerator/particleGun | `extended/eventgenerator/particleGun/particle_gun.py` | Python-sampled source modes and transport | +| extended/eventgenerator/HepMC | `extended/eventgenerator/HepMC/hepmc.py` | Minimal HepMC2 parser adapter; no native HepMC SDK | +| extended/field/field01 | `extended/field/field01/field01.py` | Uniform magnetic-field tracking study | +| extended/field/field02 | `extended/field/field02/field02.py` | Native uniform electric field and absorber scoring | +| extended/optical/OpNovice | `extended/optical/OpNovice/op_novice.py` | Native optical transport and photon counting | +| extended/optical/LXe | `extended/optical/LXe/lxe.py` | LXe scintillation/transport; post-run photon analysis replaces PMT hit callbacks | +| extended/optical/wls | `extended/optical/wls/wls.py` | Native WLS absorption/re-emission in a GDML fiber | +| extended/analysis/AnaEx01 | `extended/analysis/AnaEx01/ana_ex01.py` | Calorimeter energy analysis and CSV ntuple | +| extended/analysis/AnaEx02 | `extended/analysis/AnaEx02/ana_ex02.py` | Nested event persistence through Parquet | +| extended/analysis/AnaEx03 | `extended/analysis/AnaEx03/ana_ex03.py` | ROOT summary adapter through uproot | +| extended/electromagnetic/TestEm1 | `extended/electromagnetic/TestEm1/test_em1.py` | Electromagnetic process and energy-deposit survey | +| extended/hadronic/Hadr00 | `extended/hadronic/Hadr00/hadr00.py` | Hadronic target and secondary-particle survey | +| extended/medical/GammaTherapy | `extended/medical/GammaTherapy/gamma_therapy.py` | Simplified therapy geometry with native energy mesh | +| extended/medical/electronScattering | `extended/medical/electronScattering/electron_scattering.py` | Simplified foil-scattering benchmark | +| extended/parameterisations/Par01 | `extended/parameterisations/Par01/par01.py` | Static parameterisation expanded to GDML placements | +| extended/physicslists/factory | `extended/physicslists/factory/factory.py` | Runtime selection from native Geant4 reference lists | +| extended/radioactivedecay/rdecay01 | `extended/radioactivedecay/rdecay01/rdecay01.py` | Native radioactive decay and product survey | +| extended/runAndEvent/RE01 | `extended/runAndEvent/RE01/re01.py` | Multiple primary vertices preserved per event | + +Each entry point opens Qt by default and accepts `--batch` for non-interactive +execution. + +## Standalone GDML geometries + +The official Desktop tree currently contains 144 `.gdml` files. A GDML file +that includes a complete `` and world volume can be tried directly: + +```bash +python run_gdml.py /absolute/path/to/detector.gdml --batch -n 10 +``` + +Many official GDML files are fragments included by a C++ detector or another +GDML file. Those fragments are not standalone examples and must not be passed +to the runner by themselves. + +## Supported concepts, but not faithful class-for-class ports + +- Reference physics lists and selected additional physics constructors +- Particle gun and General Particle Source commands +- Uniform global magnetic and electric fields +- Optical materials/surfaces expressed in GDML and optical physics +- Step-derived scoring, hits, and post-run Python callbacks +- Tessellated STL geometry, VTK/ROOT/Parquet export, minimal HepMC input +- Rank-aware event splitting + +## Still requiring additional native bindings + +- Arbitrary Python subclasses of Geant4 detector, action, process, field, + sensitive-detector, and hit-collection C++ interfaces +- Parameterised geometry callbacks and custom equation/stepper classes +- Live callbacks during transport (current Python callbacks are post-run) +- Arbitrary `G4MultiFunctionalDetector` primitives beyond the command-based + native energy-deposit mesh now exposed by `NativeScoringMesh` +- Full Geant4 MPI lifecycle and result merging +- DNA chemistry scheduling and chemistry-time actions +- Examples that depend on ROOT classes, VTK classes, CAD kernels, DICOM data, + VecGeom, Pythia, or other external native SDKs + +An example in the final category can only be called compatible after its +required native interface is bound and its expected physics result is tested. diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md b/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md new file mode 100644 index 00000000..bceb3e64 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md @@ -0,0 +1,61 @@ +# Geant4 Python examples + +Runnable Python ports of the Geant4 11.4.2 examples that fit the current +`geant4_python_application` interface. Each entry point opens the Geant4 Qt +viewer by default; pass `--batch` to run without a window. + +## Included ports + +- `basic/B1/b1.py`: medical phantom with tissue and bone scoring volumes. +- `basic/B2/b2.py`: fixed target, six tracker chambers, hits, and transverse field. +- `basic/B3/b3.py`: schematic PET ring with crystal and patient energy scoring. +- `basic/B4/b4.py`: lead/liquid-argon sampling calorimeter. +- `basic/B5/b5.py`: double-arm spectrometer adaptation with field and detector scoring. +- `advanced/GoldFoilExperiment/gold_foil.py`: Rutherford gold-foil scattering. +- `extended/eventgenerator/exgps/exgps.py`: GPS source distributions and primary analysis. +- `extended/eventgenerator/particleGun/particle_gun.py`: sampled particle-gun modes. +- `extended/eventgenerator/HepMC/hepmc.py`: minimal HepMC2 ASCII primary adapter. +- `extended/field/field02/field02.py`: constant electric-field transport. +- `extended/optical/OpNovice/op_novice.py`: optical generation and transport. +- `extended/optical/LXe/lxe.py`: liquid-xenon scintillation and optical transport. +- `extended/optical/wls/wls.py`: wavelength-shifting fiber transport. +- `extended/analysis/AnaEx01/ana_ex01.py`: sampling-calorimeter analysis output. +- `extended/analysis/AnaEx02/ana_ex02.py`: nested Parquet event output. +- `extended/analysis/AnaEx03/ana_ex03.py`: ROOT summary output via uproot. +- `extended/field/field01/field01.py`: magnetic-field tracking study. +- `extended/radioactivedecay/rdecay01/rdecay01.py`: radioactive-decay track survey. +- `extended/electromagnetic/TestEm1/test_em1.py`: EM process survey. +- `extended/medical/GammaTherapy/gamma_therapy.py`: photon-therapy phantom and native dose mesh. +- `extended/medical/electronScattering/electron_scattering.py`: foil-scattering benchmark adaptation. +- `extended/hadronic/Hadr00/hadr00.py`: hadronic target and secondary survey. +- `extended/parameterisations/Par01/par01.py`: expanded parameterised detector layout. +- `extended/physicslists/factory/factory.py`: native reference-physics-list factory selection. +- `extended/runAndEvent/RE01/re01.py`: multiple primary vertices in each event. +- `run_gdml.py`: a generic Qt runner for standalone GDML detector geometries. + +## Running an example + +```bash +conda activate geant4-python +cd ~/Desktop/Geant4-11.4.2-examples-python/basic/B1 +python b1.py +``` + +Use the Qt command panel to execute `/run/beamOn 100`. For a non-interactive +run, add `--batch -n 1000`. + +Validate every claimed Python port against an original Geant4 tree with: + +```bash +python tools/audit_examples.py ~/Desktop/Geant4-11.4.2-examples \ + examples/geant4-examples/Geant4-11.4.2-examples-python +``` + +## Not yet portable + +The original Geant4 example tree is retained separately in +`~/Desktop/Geant4-11.4.2-examples`. Examples requiring live custom C++ user +actions, arbitrary fields/processes, native MPI, DNA chemistry scheduling, or +external native SDKs still need additional bindings before they can become +faithful Python ports. The compatibility report distinguishes faithful native +features from Python/GDML adaptations. diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py new file mode 100644 index 00000000..c0e9ac07 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py @@ -0,0 +1,141 @@ +"""Rutherford gold foil experiment, ported from the C++ example to geant4-python-application. + +5.5 MeV alphas fired at a 500 nm gold foil in vacuum; records the scattering +angle and exit energy of each primary alpha as it leaves the foil. +""" + +from __future__ import annotations + +import argparse +import csv + +import awkward as ak +import numpy as np + +import geant4_python_application as g4 +from geant4_python_application.application import Message + +# World and foil match DetectorConstruction.cc. G4Box takes half-lengths and +# GDML takes full lengths, so every value here is twice the C++ one. +GOLD_FOIL_GDML = R""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +FOIL = "GoldFoilPhysical" + + +def scattering(events: ak.Array) -> tuple[np.ndarray, np.ndarray]: + """Angle (deg) and exit energy (MeV) of each primary alpha leaving the foil. + + Mirrors SteppingAction.cc: primary track only, on the step whose pre-volume + is the foil and whose post-volume is not. + """ + tracks = events.track + steps = tracks.step[(tracks.id == 1) & (tracks.particle == "alpha")] + exiting = (steps.volume == FOIL) & (steps.volume_post != FOIL) + + # momentum is a unit vector, so the polar angle from the +z beam axis is acos(z) + momentum_z = ak.flatten(steps.momentum[exiting].pz, axis=None) + energy_kev = ak.flatten(steps.track_kinetic_energy[exiting], axis=None) + + angle = np.degrees(np.arccos(np.clip(ak.to_numpy(momentum_z), -1.0, 1.0))) + return angle, ak.to_numpy(energy_kev) / 1000.0 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=1000) + parser.add_argument("-o", "--output", default="gold_foil_python_results.csv") + parser.add_argument("--seed", type=int, default=137) + parser.add_argument( + "--batch", + action="store_true", + help="run the requested events and write a CSV instead of opening the Qt viewer", + ) + args = parser.parse_args() + + with g4.Application(gdml=GOLD_FOIL_GDML, seed=args.seed) as app: + app.set_event_fields( + { + "id", + "track_id", + "track_particle", + "step_volume", + "step_volume_post", + "step_momentum", + "step_track_kinetic_energy", + } + ) + # /gun/ commands: energy in MeV, position in cm (PrimaryGeneratorAction.cpp) + app._send_and_recv(Message("generator", "set_particle", ("alpha",), {})) + app._send_and_recv(Message("generator", "set_energy", (5.5,), {})) + app._send_and_recv(Message("generator", "set_position", ([0.0, 0.0, -5.0],), {})) + app._send_and_recv(Message("generator", "set_direction", ([0.0, 0.0, 1.0],), {})) + + if not args.batch: + if not app.visualization_available(): + raise RuntimeError( + "This installation was built without Qt visualization support. " + "Reinstall geant4-python-application with " + "GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON." + ) + print("Opening the Qt viewer. In its command panel, run: /run/beamOn 100") + app.visualize( + [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ] + ) + return + + events = app.run(args.events) + + angle, energy = scattering(events) + + with open(args.output, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["event_id", "scattering_angle_deg", "exit_energy_MeV"]) + writer.writerows(zip(range(len(angle)), angle, energy)) + + print(f"events simulated : {len(events)}") + print(f"alphas exiting : {len(angle)}") + print(f"angle mean/max : {angle.mean():.4f} / {angle.max():.4f} deg") + print(f"energy mean : {energy.mean():.4f} MeV (from 5.5 MeV)") + print(f"scattered >1 deg : {(angle > 1).sum()} >5 deg: {(angle > 5).sum()}") + print(f"wrote {args.output}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py new file mode 100644 index 00000000..2a2d8f86 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py @@ -0,0 +1,137 @@ +"""Brain tumour reduction via targeted Au-nanoparticle therapy (assignment). + +Geant4 cannot shrink a tumour - it scores DOSE. Reduction is inferred: + dose -> cell survival (Linear-Quadratic) -> volume reduction estimate. + +Geometry (nested spheres, no overlaps): + World (G4_AIR) -> Skull (G4_BONE_COMPACT_ICRU, r=85mm) + -> Brain (G4_BRAIN_ICRP, r=78mm) + -> Tumor (G4_A-150_TISSUE, r=12mm at x=20mm,z=10mm) + -> NanoCluster (G4_Au or tissue, r=4mm, Au = targeted nanoparticles) + +Beam: 100 keV gammas aimed at tumour. Runs with/without Au (--batch), +reports DER, scaled clinical dose (2 Gy fraction), survival S=exp(-aD-bD^2), +and inferred reduction = 1-S. +""" + +from __future__ import annotations + +import argparse +import math + +import awkward as ak +import geant4_python_application as g4 + +DENSITY = {"Skull": 1.85, "Brain": 1.03, "Tumor": 1.127, + "ClusterAu": 19.32, "ClusterTissue": 1.127} +KEV_TO_J = 1.602176634e-16 +TX, TZ = 20.0, 10.0 # tumour centre (mm) +ALPHA, BETA = 0.3, 0.03 # /Gy, glioma-like LQ params + + +def geometry(with_gold: bool = True) -> str: + cluster_mat = "G4_Au" if with_gold else "G4_A-150_TISSUE" + return f''' + + + + + + + + + + + + + + + + + + + + + +''' + + +GUN = ["/gun/particle gamma", "/gun/energy 100 keV", + f"/gun/position {TX / 10} 0 -12 cm", "/gun/direction 0 0 1"] + + +def run_case(n: int, with_gold: bool) -> dict[str, float]: + with g4.Application(gdml=geometry(with_gold), seed=137) as app: + app.set_event_fields({"id", "step_energy", "step_volume"}) + app.commands(GUN) + events = app.run(n) + e, v = events.track.step.energy, events.track.step.volume + out = {r: float(ak.sum(e[v == r])) for r in ("Skull", "Brain", "Tumor", "NanoCluster")} + out["TumorTotal"] = out["Tumor"] + out["NanoCluster"] + return out + + +def dose_gy(kev: float, mass_g: float) -> float: + return kev * KEV_TO_J / (mass_g * 1e-3) + + +def survival(dose: float) -> float: + return math.exp(-ALPHA * dose - BETA * dose * dose) + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=1000) + p.add_argument("--batch", action="store_true") + args = p.parse_args() + + if not args.batch: + with g4.Application(gdml=geometry(True), seed=137) as app: + app.commands(GUN) + app.visualize([ + "/vis/open OGL", "/vis/drawVolume", + "/vis/viewer/set/background black", + "/vis/viewer/set/style surface", + "/vis/geometry/set/colour World 1 1 1 0", + "/vis/geometry/set/colour SkullLV 0.9 0.8 0.5 0.12", + "/vis/geometry/set/colour BrainLV 1 0.5 0.7 0.18", + "/vis/geometry/set/colour TumorLV 0 1 0 0.5", + "/vis/geometry/set/colour NanoClusterLV 1 0 0 1", + "/vis/modeling/trajectories/create/drawByParticleID", + "/vis/modeling/trajectories/drawByParticleID-0/set gamma yellow", + "/vis/modeling/trajectories/drawByParticleID-0/set e- red", + "/vis/modeling/trajectories/drawByParticleID-0/set e+ blue", + "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ]) + return + + v_cl = 4 / 3 * math.pi * 0.4**3 + v_tu = 4 / 3 * math.pi * 1.2**3 - v_cl + v_br = 4 / 3 * math.pi * 7.8**3 - 4 / 3 * math.pi * 1.2**3 + v_sk = 4 / 3 * math.pi * 8.5**3 - 4 / 3 * math.pi * 7.8**3 + m = {"Brain": v_br * DENSITY["Brain"], "Tumor": v_tu * DENSITY["Tumor"], + "Skull": v_sk * DENSITY["Skull"]} + + no, yes = run_case(args.events, False), run_case(args.events, True) + der = yes["TumorTotal"] / no["TumorTotal"] if no["TumorTotal"] else float("nan") + + # Scale: give no-Au tumour a clinical 2 Gy fraction; same fluence with Au gives 2*DER. + s_no, s_au = survival(2.0), survival(2.0 * der) + print(f"events: {args.events}, beam 100 keV -> tumour at ({TX},0,{TZ}) mm") + print(f"[no-Au ] TumorTotal {no['TumorTotal']:.1f} keV | Brain {no['Brain']:.1f} keV | Skull {no['Skull']:.1f} keV") + print(f"[with-Au] TumorTotal {yes['TumorTotal']:.1f} keV | Brain {yes['Brain']:.1f} keV | Skull {yes['Skull']:.1f} keV") + print(f"DER (tumour Au/no-Au): {der:.2f}") + print(f"At 2 Gy to tumour without Au: survival {s_no:.3f} (reduction {(1 - s_no) * 100:.1f}%)") + print(f"Same fluence with Au -> {2 * der:.1f} Gy: survival {s_au:.3e} (reduction {(1 - s_au) * 100:.2f}%)") + print("Report: Geant4 proves DOSE boost (photoelectric in Au Z=79); " + "reduction % comes from LQ model, not from cells dying in code. " + "Solid AuSphere exaggerates (real DER 1.1-2); skull/brain doses show sparing.") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py new file mode 100644 index 00000000..1dc10e79 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py @@ -0,0 +1,137 @@ +"""Targeted cancer therapy with nanoparticles (assignment). + +Photon beam irradiates a water phantom containing a tissue tumour. +The tumour contains a gold cluster representing targeted gold +nanoparticles (Au, Z=79). kV photons undergo enhanced photoelectric +absorption in gold -> photo/Auger electrons -> local dose enhancement. + +Runs twice (--batch): with Au and without Au (cluster = tissue), +then reports deposited energy, estimated dose, and Dose Enhancement +Ratio: DER = Dose_tumour(Au) / Dose_tumour(no Au). + +Geometry (nested, no overlaps): + World (G4_AIR, box) -> Phantom (G4_WATER, box 90 mm) + -> Tumour (G4_A-150_TISSUE, sphere r=15 mm) + -> NanoCluster (G4_Au or tissue, sphere r=5 mm) +""" + +from __future__ import annotations + +import argparse +import math + +import awkward as ak +import geant4_python_application as g4 + +# Densities (g/cm3) for dose = energy / mass estimates. +DENSITY = {"Phantom": 1.0, "Tumor": 1.127, "NanoCluster_Au": 19.32, "NanoCluster_tissue": 1.127} +KEV_TO_JOULE = 1.602176634e-16 + + +def geometry(with_gold: bool = True) -> str: + cluster_mat = "G4_Au" if with_gold else "G4_A-150_TISSUE" + return f''' + + + + + + + + + + + + + + + + +''' + + +def run_case(events_n: int, with_gold: bool) -> dict[str, float]: + """Run one configuration, return deposited energy (keV) per region.""" + with g4.Application(gdml=geometry(with_gold), seed=137) as app: + app.set_event_fields({"id", "step_energy", "step_volume"}) + app.commands([ + "/gun/particle gamma", "/gun/energy 100 keV", + "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1", + ]) + events = app.run(events_n) + + energy = events.track.step.energy + volume = events.track.step.volume + out = {} + for region in ("Phantom", "Tumor", "NanoCluster"): + out[region] = float(ak.sum(energy[volume == region])) + out["TumorTotal"] = out["Tumor"] + out["NanoCluster"] + return out + + +def dose_gy(energy_kev: float, mass_g: float) -> float: + return energy_kev * KEV_TO_JOULE / (mass_g * 1e-3) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=1000) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + + if not args.batch: + with g4.Application(gdml=geometry(with_gold=True), seed=137) as app: + app.commands([ + "/gun/particle gamma", "/gun/energy 100 keV", + "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1", + ]) + app.visualize([ + "/vis/open OGL", "/vis/drawVolume", + "/vis/viewer/set/background black", + "/vis/viewer/set/style surface", + # colour-code by logical volume: phantom=blue, tumour=green, Au=red + "/vis/geometry/set/colour World 1 1 1 0", + "/vis/geometry/set/colour PhantomLV 0 0.3 1 0.15", + "/vis/geometry/set/colour TumorLV 0 1 0 0.35", + "/vis/geometry/set/colour NanoClusterLV 1 0 0 1", + # colour-code tracks: gamma=yellow, e-=red, rest=white + "/vis/modeling/trajectories/create/drawByParticleID", + "/vis/modeling/trajectories/drawByParticleID-0/set gamma yellow", + "/vis/modeling/trajectories/drawByParticleID-0/set e- red", + "/vis/modeling/trajectories/drawByParticleID-0/set e+ blue", + "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ]) + return + + # Volumes/masses: cluster r=5mm, tumour r=15mm, phantom 90mm box. + v_cluster = 4 / 3 * math.pi * 0.5**3 # cm3 + v_tumor = 4 / 3 * math.pi * 1.5**3 - v_cluster + v_phantom = 9.0**3 - 4 / 3 * math.pi * 1.5**3 + masses = {"Phantom": v_phantom * DENSITY["Phantom"], + "Tumor": v_tumor * DENSITY["Tumor"]} + + no_au = run_case(args.events, with_gold=False) + with_au = run_case(args.events, with_gold=True) + masses_au = dict(masses, NanoCluster=v_cluster * DENSITY["NanoCluster_Au"]) + masses_no = dict(masses, NanoCluster=v_cluster * DENSITY["NanoCluster_tissue"]) + + print(f"events per case: {args.events}, beam: 100 keV gamma") + for label, res, m in (("no-Au ", no_au, masses_no), ("with-Au", with_au, masses_au)): + print(f"[{label}] Phantom(healthy): {res['Phantom']:.1f} keV " + f"({dose_gy(res['Phantom'], m['Phantom']):.3e} Gy) | " + f"Tumor rim: {res['Tumor']:.1f} keV | " + f"Cluster: {res['NanoCluster']:.1f} keV | " + f"TumorTotal: {res['TumorTotal']:.1f} keV") + + der = with_au["TumorTotal"] / no_au["TumorTotal"] if no_au["TumorTotal"] else float("nan") + print(f"Dose Enhancement Ratio (TumorTotal Au / no-Au): {der:.3f}") + print("Assignment: explain why DER>1 (photoelectric ~ Z^3/E^3 in Au), " + "why 100 keV shows it but MV beam would show less, and what the " + "Phantom dose says about healthy-tissue sparing.") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py new file mode 100644 index 00000000..4ffa6f02 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py @@ -0,0 +1,62 @@ +"""Python port of Geant4 basic/B1: tissue-and-bone phantom.""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +GDML = R''' + + + + + + + + + + + + + + + + + + + + +''' + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=100) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + + with g4.Application(gdml=GDML, seed=137) as app: + app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) + app.commands([ + "/gun/particle e-", "/gun/energy 10 MeV", + "/gun/position 0 0 -20 cm", "/gun/direction 0 0 1", + ]) + if not args.batch: + app.visualize([ + "/vis/open OGL", "/vis/drawVolume", "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ]) + return + events = app.run(args.events) + + for volume in ("Tissue", "Bone"): + deposited = ak.sum(events.track.step.energy[events.track.step.volume == volume]) + print(f"{volume} deposited energy: {float(deposited):.3f} keV") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py new file mode 100644 index 00000000..59335d22 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py @@ -0,0 +1,61 @@ +"""Python adaptation of Geant4 basic/B2: fixed target and tracker chambers.""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def geometry() -> str: + chambers = [] + for index in range(6): + z = -50 + index * 35 + radius = 20 + index * 5 + chambers.append( + f'' + ) + volumes = "".join( + f'' + f'' for i in range(6) + ) + placements = "".join( + f'' + f'' + for i in range(6) + ) + return f''' + + + +{''.join(chambers)} + +{volumes} + +{placements}''' + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=100) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + with g4.Application(gdml=geometry(), physics="FTFP_BERT", seed=137) as app: + app.set_event_fields({"id", "track_id", "step_energy", "step_volume", "step_position"}) + app.detector.magnetic_field = (0.2, 0.0, 0.0) + app.commands(["/gun/particle proton", "/gun/energy 3 GeV", "/gun/position 0 0 -20 cm", "/gun/direction 0 0 1"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + steps = ak.flatten(events.track.step, axis=-1) + tracker_hits = steps[ + (steps.energy > 0) & ak.str.starts_with(steps.volume, "Chamber") + ] + print(f"events: {len(events)}, tracker hits: {len(ak.flatten(tracker_hits.energy, axis=None))}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py new file mode 100644 index 00000000..deedc2b9 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py @@ -0,0 +1,55 @@ +"""Python adaptation of Geant4 basic/B3: schematic PET scanner.""" + +from __future__ import annotations + +import argparse +import math + +import awkward as ak +import geant4_python_application as g4 + + +def geometry() -> str: + placements = [] + for ring in range(3): + z = (ring - 1) * 25 + for crystal in range(24): + angle = crystal * 15 + radians = math.radians(angle) + x, y = 100 * math.cos(radians), 100 * math.sin(radians) + placements.append( + f'' + f'' + f'' + ) + return f''' + + + + + + +{''.join(placements)} +''' + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=100) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + with g4.Application(gdml=geometry(), physics="FTFP_BERT", seed=137) as app: + app.add_physics("G4RadioactiveDecayPhysics") + app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) + app.commands(["/gun/particle gamma", "/gun/energy 511 keV", "/gun/position 0 0 0 mm", "/gun/direction 1 0 0"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + crystal = ak.sum(events.track.step.energy[ak.str.starts_with(events.track.step.volume, "Crystal")]) + patient = ak.sum(events.track.step.energy[events.track.step.volume == "Patient"]) + print(f"events: {len(events)}, crystal energy: {float(crystal):.3f} keV, patient energy: {float(patient):.3f} keV") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py new file mode 100644 index 00000000..4150daab --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py @@ -0,0 +1,63 @@ +"""Python port of Geant4 basic/B4: lead/liquid-argon sampling calorimeter.""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def geometry() -> str: + layers = [] + for index in range(10): + z = -67.5 + index * 15 + layers.append( + f'' + f'' + f'' + f'' + ) + return f''' + + + + + + + + {''.join(layers)} +''' + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=100) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + + with g4.Application(gdml=geometry(), seed=137) as app: + app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) + app.commands([ + "/gun/particle e-", "/gun/energy 1 GeV", + "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1", + ]) + if not args.batch: + app.visualize([ + "/vis/open OGL", "/vis/drawVolume", "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ]) + return + events = app.run(args.events) + + energy = events.track.step.energy + volume = events.track.step.volume + absorber = ak.sum(energy[ak.str.starts_with(volume, "Absorber")]) + gap = ak.sum(energy[ak.str.starts_with(volume, "Gap")]) + print(f"absorber deposited energy: {float(absorber):.3f} keV") + print(f"gap deposited energy: {float(gap):.3f} keV") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py new file mode 100644 index 00000000..52338777 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py @@ -0,0 +1,54 @@ +"""Python adaptation of Geant4 basic/B5: double-arm spectrometer.""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def geometry() -> str: + placements = [] + specs = [ + ("Hodoscope1", "HodoLV", -180), ("Chamber1", "ChamberLV", -130), + ("FieldRegion", "FieldLV", 0), ("Hodoscope2", "HodoLV", 130), + ("Chamber2", "ChamberLV", 170), ("ECal", "ECalLV", 220), + ("HCal", "HCalLV", 290), + ] + for name, logical, z in specs: + placements.append(f'') + return f''' + + + + + + + + + +{''.join(placements)} +''' + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=100) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + with g4.Application(gdml=geometry(), physics="FTFP_BERT", seed=137) as app: + app.detector.magnetic_field = (0.0, 1.0, 0.0) + app.set_event_fields({"id", "track_id", "step_energy", "step_time", "step_volume", "step_position"}) + app.commands(["/gun/particle proton", "/gun/energy 1 GeV", "/gun/position 0 0 -35 cm", "/gun/direction 0 0 1"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + for volume in ("Hodoscope", "Chamber", "ECal", "HCal"): + mask = ak.str.starts_with(events.track.step.volume, volume) + print(f"{volume} energy: {float(ak.sum(events.track.step.energy[mask])):.3f} keV") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py new file mode 100644 index 00000000..059d4b68 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py @@ -0,0 +1,45 @@ +"""Python adaptation of extended/analysis/AnaEx01 sampling-calorimeter analysis.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import awkward as ak +import geant4_python_application as g4 +import numpy as np + +from importlib.util import module_from_spec, spec_from_file_location + + +def load_b4_geometry(): + path = Path(__file__).parents[3] / "basic" / "B4" / "b4.py" + spec = spec_from_file_location("g4_b4_geometry", path) + module = module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + return module.geometry() + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--batch", action="store_true") + p.add_argument("-o", "--output", type=Path, default=Path("ana_ex01.csv")) + args = p.parse_args() + with g4.Application(gdml=load_b4_geometry(), physics="FTFP_BERT", seed=137) as app: + app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) + app.commands(["/gun/particle e-", "/gun/energy 1 GeV", "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + step = events.track.step + absorber = ak.to_numpy(ak.sum(ak.flatten(step.energy[ak.str.starts_with(step.volume, "Absorber")], axis=-1), axis=1)) + gap = ak.to_numpy(ak.sum(ak.flatten(step.energy[ak.str.starts_with(step.volume, "Gap")], axis=-1), axis=1)) + np.savetxt(args.output, np.column_stack((np.arange(len(events)), absorber, gap)), delimiter=",", header="event,EAbs_keV,EGap_keV", comments="") + print(f"events: {len(events)}, wrote {args.output}; mean EAbs={absorber.mean():.3f} keV, EGap={gap.mean():.3f} keV") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py new file mode 100644 index 00000000..1b8e59e1 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py @@ -0,0 +1,16 @@ +"""Python adaptation of AnaEx02 with nested Parquet event output.""" +from __future__ import annotations +import argparse +from pathlib import Path +import geant4_python_application as g4 + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); p.add_argument("-o","--output",type=Path,default=Path("ana_ex02.parquet")); a=p.parse_args() + with g4.Application(gdml=g4.basic_gdml,physics="FTFP_BERT",seed=138) as app: + app.commands(["/gun/particle e-","/gun/energy 100 MeV","/gun/position 0 0 -40 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + g4.io.events_to_parquet(events,a.output) + assert len(g4.io.events_from_parquet(a.output))==len(events) + print(f"events: {len(events)}, wrote {a.output}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py new file mode 100644 index 00000000..eee5b842 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py @@ -0,0 +1,19 @@ +"""Python adaptation of AnaEx03 with ROOT summary output via uproot.""" +from __future__ import annotations +import argparse +from pathlib import Path +import geant4_python_application as g4 + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); p.add_argument("-o","--output",type=Path,default=Path("ana_ex03.root")); a=p.parse_args() + with g4.Application(gdml=g4.basic_gdml,physics="FTFP_BERT",seed=139) as app: + app.commands(["/gun/particle gamma","/gun/energy 10 MeV","/gun/position 0 0 -40 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + try: + g4.io.events_to_root(events,a.output) + assert len(g4.io.events_from_root(a.output)["id"])==len(events) + print(f"events: {len(events)}, wrote {a.output}") + except ImportError as exc: + print(f"events: {len(events)}, ROOT output skipped: {exc}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py new file mode 100644 index 00000000..0b239d45 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py @@ -0,0 +1,19 @@ +"""Python adaptation of extended/electromagnetic/TestEm1 process survey.""" +from __future__ import annotations +import argparse +from collections import Counter +import awkward as ak +import geant4_python_application as g4 + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--material",default="G4_Al"); p.add_argument("--batch",action="store_true"); a=p.parse_args() + gdml=f'''''' + with g4.Application(gdml=gdml,seed=137) as app: + app.set_event_fields({"id","track_id","step_energy","step_process","step_position","step_track_kinetic_energy"}) + app.commands(["/gun/particle e-","/gun/energy 10 MeV","/gun/position 0 0 -40 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + processes=Counter(ak.to_list(ak.flatten(events.track.step.process,axis=None))) + print(f"events: {len(events)}, material: {a.material}, deposited: {float(ak.sum(events.track.step.energy)):.3f} keV") + print(f"process calls: {dict(processes)}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py new file mode 100644 index 00000000..e71665ee --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py @@ -0,0 +1,39 @@ +"""Python adapter for extended/eventgenerator/HepMC. + +Reads a small HepMC2 ASCII subset through the package's Python adapter and +transports final-state particles. It does not embed a native HepMC library. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import geant4_python_application as g4 + +SAMPLE = """E 0 1 1 +P 1 22 0 0 1 1 0 1 +P 2 11 0.1 0 0.5 0.6 0.000511 1 +""" + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", nargs="?", type=Path) + parser.add_argument("-n", "--events", type=int, default=1, help="accepted for the common audit interface") + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + text = args.input.read_text() if args.input else SAMPLE + primaries = g4.io.hepmc_to_primaries(text) + + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=137) as app: + if not args.batch: + app.visualize() + return + events = app.run(primaries) + print(f"parsed and transported {len(events)} HepMC particles") + assert len(events) == len(primaries) + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py new file mode 100644 index 00000000..e228d9ec --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py @@ -0,0 +1,37 @@ +"""Python adaptation of extended/eventgenerator/exgps.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import awkward as ak +import geant4_python_application as g4 +import numpy as np + +GDML = '''''' + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=1000) + p.add_argument("--batch", action="store_true") + p.add_argument("-o", "--output", type=Path, default=Path("exgps_primaries.npz")) + args = p.parse_args() + with g4.Application(gdml=GDML, seed=137) as app: + app.generator.use_gps().particle("gamma") + app.generator.commands([ + "/gps/pos/type Volume", "/gps/pos/shape Sphere", "/gps/pos/radius 4 cm", + "/gps/ang/type iso", "/gps/ene/type Lin", "/gps/ene/min 100 keV", "/gps/ene/max 2 MeV", + ]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + energy = ak.to_numpy(ak.flatten(events.primaries.energy)) + np.savez(args.output, energy_keV=energy) + print(f"events: {len(events)}, energy range: {energy.min():.2f}-{energy.max():.2f} keV; wrote {args.output}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py new file mode 100644 index 00000000..6474a204 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py @@ -0,0 +1,66 @@ +"""Python adaptation of extended/eventgenerator/particleGun sampling modes.""" + +from __future__ import annotations + +import argparse +import math + +import awkward as ak +import geant4_python_application as g4 +import numpy as np + + +def _particle(position, direction, energy=1000.0): + return {"particle": "geantino", "energy": energy, "position": dict(zip("xyz", position)), "direction": dict(zip("xyz", direction))} + + +def primaries(count: int, mode: int, seed: int) -> ak.Array: + rng = np.random.default_rng(seed) + records = [] + for _ in range(count): + phi = rng.uniform(0, 2 * math.pi) + if mode == 1: + position = (50 * math.cos(phi), 50 * math.sin(phi), rng.uniform(-50, 50)) + radial = (math.cos(phi), math.sin(phi), 0.0) + records.append({"primaries": [ + _particle(position, radial), + _particle(tuple(-x for x in position), tuple(-x for x in radial)), + _particle((0.0, 0.0, 0.0), (0.0, 0.0, 1.0)), + ]}) + continue + if mode == 2: + energy = float(rng.choice([100, 250, 500, 1000], p=[0.1, 0.3, 0.4, 0.2])) + position = (0.0, 0.0, 0.0) + direction = (0.0, 0.0, 1.0) + elif mode == 4: + radius = np.cbrt(rng.uniform(20**3, 80**3)) + costheta = rng.uniform(-1, 1) + sintheta = math.sqrt(1 - costheta**2) + position = (radius * sintheta * math.cos(phi), radius * sintheta * math.sin(phi), radius * costheta) + direction = tuple(-value / radius for value in position) + energy = 1000.0 + else: + alpha = rng.uniform(0, math.radians(15 if mode == 3 else 30)) + direction = (math.sin(alpha) * math.cos(phi), math.sin(alpha) * math.sin(phi), math.cos(alpha)) + position, energy = (0.0, 0.0, -100.0), 1000.0 + records.append(_particle(position, direction, energy)) + return ak.Array(records) + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=1000) + p.add_argument("--mode", type=int, choices=(0, 1, 2, 3, 4), default=0) + p.add_argument("--batch", action="store_true") + args = p.parse_args() + source = primaries(args.events, args.mode, 137) + with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + if not args.batch: + app.visualize() + return + events = app.run(source) + print(f"mode {args.mode}: generated and transported {len(events)} primaries") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py new file mode 100644 index 00000000..bc7d221a --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py @@ -0,0 +1,17 @@ +"""Python adaptation of extended/field/field01 magnetic tracking study.""" +from __future__ import annotations +import argparse +import awkward as ak +import geant4_python_application as g4 + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--field",type=float,default=1.0); p.add_argument("--batch",action="store_true"); a=p.parse_args() + with g4.Application(gdml=g4.basic_gdml,seed=137) as app: + app.detector.magnetic_field=(0.0,a.field,0.0) + app.set_event_fields({"id","track_id","step_position","step_volume","step_track_kinetic_energy"}) + app.commands(["/gun/particle e-","/gun/energy 100 MeV","/gun/position 0 0 -20 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + nsteps=len(ak.flatten(events.track.step.position.x,axis=None)) + print(f"events: {len(events)}, B=(0,{a.field},0) T, recorded steps: {nsteps}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py new file mode 100644 index 00000000..ec614067 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py @@ -0,0 +1,33 @@ +"""Python adaptation of extended/field/field02 constant-electric-field test.""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + +GDML = '''''' + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--field", type=float, default=0.5, help="electric field in kV/cm") + p.add_argument("--batch", action="store_true") + args = p.parse_args() + with g4.Application(gdml=GDML, physics="FTFP_BERT", seed=137) as app: + app.detector.electric_field = (args.field, 0.0, 0.0) + app.set_event_fields({"id", "track_id", "step_energy", "step_volume", "step_position"}) + app.commands(["/gun/particle e-", "/gun/energy 10 MeV", "/gun/position 0 0 -20 cm", "/gun/direction 0 0 1"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + steps = events.track.step + edep = float(ak.sum(steps.energy[steps.volume == "Absorber"])) + print(f"events: {len(events)}, E=({args.field},0,0) kV/cm, absorber energy: {edep:.3f} keV") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py new file mode 100644 index 00000000..227ab146 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py @@ -0,0 +1,18 @@ +"""Python adaptation of extended/hadronic/Hadr00 target interactions.""" +from __future__ import annotations +import argparse +from collections import Counter +import awkward as ak +import geant4_python_application as g4 + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--physics",default="FTFP_BERT"); p.add_argument("--batch",action="store_true"); a=p.parse_args() + gdml='''''' + with g4.Application(gdml=gdml,physics=a.physics,seed=137) as app: + app.commands(["/gun/particle proton","/gun/energy 15 GeV","/gun/position 0 0 -9 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + secondaries=events.track.particle[events.track.parent_id>0] + counts=Counter(ak.to_list(ak.flatten(secondaries,axis=None))) + print(f"events: {len(events)}, physics: {a.physics}, secondaries: {dict(counts)}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py new file mode 100644 index 00000000..0dd16e33 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py @@ -0,0 +1,19 @@ +"""Python adaptation of extended/medical/GammaTherapy dose scoring.""" +from __future__ import annotations +import argparse +from pathlib import Path +import awkward as ak +import geant4_python_application as g4 + +GDML='''''' + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); p.add_argument("-o","--output",type=Path,default=Path("gamma_therapy_mesh.csv")); a=p.parse_args() + with g4.Application(gdml=GDML,physics="FTFP_BERT",seed=137) as app: + mesh=g4.NativeScoringMesh(app,"phantomMesh","doseEnergy").box((150,150,200),(10,10,20),center=(0,0,0)) + app.commands(["/gun/particle gamma","/gun/energy 6 MeV","/gun/position 0 0 -45 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events); mesh.dump(a.output) + dose=float(ak.sum(events.track.step.energy[events.track.step.volume=="Phantom"])) + print(f"events: {len(events)}, phantom deposited energy: {dose:.3f} keV; mesh: {a.output}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py new file mode 100644 index 00000000..83e0db82 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py @@ -0,0 +1,19 @@ +"""Python adaptation of the NRCC electron-scattering benchmark geometry.""" +from __future__ import annotations +import argparse +import awkward as ak +import geant4_python_application as g4 + +GDML='''''' + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=1000); p.add_argument("--batch",action="store_true"); a=p.parse_args() + with g4.Application(gdml=GDML,seed=137) as app: + app.set_event_fields({"id","track_id","step_volume_post","step_momentum","step_position"}) + app.commands(["/gun/particle e-","/gun/energy 13 MeV","/gun/position 0 0 -30 cm","/gun/direction 0 0 1"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + step=events.track.step; crossing=step[step.volume_post=="ScoringPlane"] + n=len(ak.flatten(crossing.position.x,axis=None)) + print(f"events: {len(events)}, scoring-plane crossings: {n}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py new file mode 100644 index 00000000..5a5156d4 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py @@ -0,0 +1,44 @@ +"""Python optical-transport adaptation of extended/optical/LXe. + +This covers liquid-xenon scintillation and optical boundary transport. The +official example's custom PMT hit collection is represented by track analysis +after the run because Python sensitive-detector callbacks are not yet bound. +""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=10) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + + with g4.Application(gdml=g4.optical.lxe_scintillator_gdml, optical=True, seed=137) as app: + app.commands(g4.optical.scintillation_commands(max_cerenkov=100)) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 1 MeV", + "/gun/position 0 0 0 mm", + "/gun/direction 1 0 0", + ] + ) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + + particles = ak.flatten(events.track.particle, axis=None) + photons = int(ak.sum(particles == "opticalphoton")) + print(f"events: {len(events)}, liquid-xenon optical photons tracked: {photons}") + assert photons > 0 + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py new file mode 100644 index 00000000..07b54017 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py @@ -0,0 +1,30 @@ +"""Python adaptation of extended/optical/OpNovice.""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=10) + p.add_argument("--batch", action="store_true") + args = p.parse_args() + with g4.Application(gdml=g4.optical.optical_water_gdml, optical=True, seed=137) as app: + app.commands(g4.optical.scintillation_commands(max_cerenkov=100)) + app.commands(["/gun/particle e-", "/gun/energy 10 MeV", "/gun/position 0 0 0 mm", "/gun/direction 1 0 0"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + particles = ak.flatten(events.track.particle, axis=None) + optical = int(ak.sum(particles == "opticalphoton")) + print(f"events: {len(events)}, optical photons tracked: {optical}") + assert optical > 0 + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py new file mode 100644 index 00000000..b8293dc7 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py @@ -0,0 +1,44 @@ +"""Python optical-transport adaptation of extended/optical/wls. + +The detector is a polystyrene-like WLS core inside lower-index cladding. A +violet optical photon is absorbed and can be re-emitted at lower energy by the +native Geant4 wavelength-shifting process. +""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=10) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + + with g4.Application(gdml=g4.optical.wls_fiber_gdml, optical=True, seed=521) as app: + app.commands( + [ + "/gun/particle opticalphoton", + "/gun/energy 3.5 eV", + "/gun/position 0 0 -190 mm", + "/gun/direction 0 0 1", + "/gun/polarization 1 0 0", + ] + ) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + + particles = ak.flatten(events.track.particle, axis=None) + photons = int(ak.sum(particles == "opticalphoton")) + print(f"events: {len(events)}, WLS optical-photon tracks: {photons}") + assert photons >= args.events + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py new file mode 100644 index 00000000..a562728b --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py @@ -0,0 +1,33 @@ +"""Python/GDML adaptation of extended/parameterisations/Par01. + +The repeated detector cells are expanded into fixed GDML placements before +initialization. This covers static parameterised layouts without exposing a +live G4VPVParameterisation callback. +""" + +from __future__ import annotations + +import argparse + +import geant4_python_application as g4 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=10) + parser.add_argument("--cells", type=int, default=12) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + gdml = g4.geometry.linear_array_gdml(args.cells, spacing_mm=15, box_size_mm=5) + + with g4.Application(gdml=gdml, seed=137) as app: + app.commands(["/gun/particle geantino", "/gun/position -120 0 0 mm", "/gun/direction 1 0 0"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + print(f"events: {len(events)}, expanded detector cells: {args.cells}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py new file mode 100644 index 00000000..d70905c3 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py @@ -0,0 +1,30 @@ +"""Python adaptation of extended/physicslists/factory.""" + +from __future__ import annotations + +import argparse + +import geant4_python_application as g4 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=10) + parser.add_argument("--physics", default="FTFP_BERT") + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + available = g4.Application.available_physics_lists() + if args.physics not in available: + raise SystemExit(f"unknown physics list {args.physics!r}; choose from: {', '.join(available)}") + + with g4.Application(gdml=g4.basic_gdml, physics=args.physics, seed=137) as app: + app.commands(["/gun/particle proton", "/gun/energy 1 GeV", "/gun/position 0 0 -400 mm", "/gun/direction 0 0 1"]) + if not args.batch: + app.visualize() + return + events = app.run(args.events) + print(f"physics list: {args.physics}; events: {len(events)}; factory lists: {len(available)}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py new file mode 100644 index 00000000..7e1983de --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py @@ -0,0 +1,18 @@ +"""Python adaptation of extended/radioactivedecay/rdecay01.""" +from __future__ import annotations +import argparse +from collections import Counter +import awkward as ak +import geant4_python_application as g4 + +def main(): + p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); a=p.parse_args() + with g4.Application(gdml=g4.basic_gdml,physics="FTFP_BERT",seed=137) as app: + app.add_physics("G4RadioactiveDecayPhysics") + app.initialize() # ion definitions must exist before /gun/ion + app.commands(["/gun/particle ion","/gun/ion 10 24 0 0","/gun/energy 0 keV","/gun/position 0 0 0 cm"]) + if not a.batch: app.visualize(); return + events=app.run(a.events) + particles=Counter(ak.to_list(ak.flatten(events.track.particle,axis=None))) + print(f"events: {len(events)}, tracked particles: {dict(particles)}") +if __name__=="__main__": main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py new file mode 100644 index 00000000..33cb7a25 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py @@ -0,0 +1,45 @@ +"""Multi-primary Python adaptation of extended/runAndEvent/RE01. + +Each Geant4 event contains two independent primary vertices. This exercises +the package's native nested-primary event support and preserves both vertices +in the returned Awkward event record. +""" + +from __future__ import annotations + +import argparse + +import awkward as ak +import geant4_python_application as g4 + + +def primary(particle: str, x: float) -> dict: + return { + "particle": particle, + "energy": 1000.0, + "position": {"x": x, "y": 0.0, "z": -300.0}, + "direction": {"x": 0.0, "y": 0.0, "z": 1.0}, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-n", "--events", type=int, default=10) + parser.add_argument("--batch", action="store_true") + args = parser.parse_args() + source = ak.Array( + [{"primaries": [primary("gamma", -10.0), primary("e-", 10.0)]} for _ in range(args.events)] + ) + + with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + if not args.batch: + app.visualize() + return + events = app.run(source) + counts = ak.to_list(ak.num(events.primaries)) + print(f"events: {len(events)}, primary vertices: {sum(counts)}") + assert counts == [2] * args.events + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py new file mode 100644 index 00000000..d37df0d8 --- /dev/null +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py @@ -0,0 +1,40 @@ +"""Run a GDML detector with geant4-python-application and the Qt viewer.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import geant4_python_application as g4 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("gdml", type=Path, help="path to a GDML geometry file") + parser.add_argument("-n", "--events", type=int, default=100) + parser.add_argument("--batch", action="store_true") + parser.add_argument("--command", action="append", default=[], help="Geant4 command") + args = parser.parse_args() + + geometry = args.gdml.read_text() + commands = args.command or [ + "/gun/particle geantino", + "/gun/energy 1 GeV", + "/gun/direction 0 0 1", + ] + with g4.Application(gdml=geometry, seed=137) as app: + app.commands(commands) + if args.batch: + print(f"events simulated: {len(app.run(args.events))}") + else: + app.visualize([ + "/vis/open OGL", "/vis/drawVolume", + "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ]) + + +if __name__ == "__main__": + main() diff --git a/src/geant4_application/include/geant4_application/Application.h b/src/geant4_application/include/geant4_application/Application.h index 17b8bb14..371cdf21 100644 --- a/src/geant4_application/include/geant4_application/Application.h +++ b/src/geant4_application/include/geant4_application/Application.h @@ -44,8 +44,12 @@ class Application { void SetRandomSeed(long seed); void SetupManager(unsigned short nThreads); + static bool MultithreadingAvailable(); void SetupDetector(const std::string& gdml); - void SetupPhysics(); + void SetupPhysics(const std::string& physicsListName = "custom", bool optical = false); + static std::vector GetAvailablePhysicsLists(); + void AddExtraPhysics(const std::string& constructorName); + static std::vector GetAvailableExtraPhysics(); void SetupAction(); void Initialize(); diff --git a/src/geant4_application/include/geant4_application/DetectorConstruction.h b/src/geant4_application/include/geant4_application/DetectorConstruction.h index 3a2d79ea..0de18e02 100644 --- a/src/geant4_application/include/geant4_application/DetectorConstruction.h +++ b/src/geant4_application/include/geant4_application/DetectorConstruction.h @@ -9,6 +9,9 @@ #include #include #include +#include + +class G4GlobalMagFieldMessenger; namespace geant4_app { @@ -25,6 +28,12 @@ class DetectorConstruction : public G4VUserDetectorConstruction { void SetSensitiveVolumes(const std::set& sensitiveVolumes); std::set GetSensitiveVolumes() const { return sensitiveVolumes; } + void SetMagneticField(const std::array& fieldTesla); + std::array GetMagneticField() const { return magneticFieldTesla; } + + void SetElectricField(const std::array& fieldKVperCM); + std::array GetElectricField() const { return electricFieldKVperCM; } + std::string GetGDML() const { return gdml; } void SetGDML(const std::string& gdml); @@ -44,6 +53,10 @@ class DetectorConstruction : public G4VUserDetectorConstruction { std::set sensitiveVolumes; bool sensitiveDetectorConstructed = false; + std::array magneticFieldTesla = {0., 0., 0.}; + std::array electricFieldKVperCM = {0., 0., 0.}; + bool fieldConstructed = false; + static G4ThreadLocal G4GlobalMagFieldMessenger* magneticFieldMessenger; }; }// namespace geant4_app diff --git a/src/geant4_application/include/geant4_application/PhysicsList.h b/src/geant4_application/include/geant4_application/PhysicsList.h index c8fab885..d9008510 100644 --- a/src/geant4_application/include/geant4_application/PhysicsList.h +++ b/src/geant4_application/include/geant4_application/PhysicsList.h @@ -17,6 +17,6 @@ namespace geant4_app { class PhysicsList : public G4VModularPhysicsList { public: - PhysicsList(); + explicit PhysicsList(bool optical = false); }; }// namespace geant4_app diff --git a/src/geant4_application/include/geant4_application/PrimaryGeneratorAction.h b/src/geant4_application/include/geant4_application/PrimaryGeneratorAction.h index 7f8c1ce4..1b37ec3e 100644 --- a/src/geant4_application/include/geant4_application/PrimaryGeneratorAction.h +++ b/src/geant4_application/include/geant4_application/PrimaryGeneratorAction.h @@ -30,6 +30,7 @@ class PrimaryGeneratorAction : public G4VUserPrimaryGeneratorAction { inline static void SetAwkwardPrimaryPositions(const std::vector>& positions) { awkwardPrimaryPositions = positions; } inline static void SetAwkwardPrimaryDirections(const std::vector>& directions) { awkwardPrimaryDirections = directions; } inline static void SetAwkwardPrimaryParticles(const std::vector& particles) { awkwardPrimaryParticles = particles; } + inline static void SetAwkwardPrimaryEventOffsets(const std::vector& offsets) { awkwardPrimaryEventOffsets = offsets; } static void ClearAwkwardPrimaries(); private: @@ -41,6 +42,7 @@ class PrimaryGeneratorAction : public G4VUserPrimaryGeneratorAction { static std::vector> awkwardPrimaryPositions; static std::vector> awkwardPrimaryDirections; static std::vector awkwardPrimaryParticles; + static std::vector awkwardPrimaryEventOffsets; }; }// namespace geant4_app diff --git a/src/geant4_application/src/ActionInitialization.cpp b/src/geant4_application/src/ActionInitialization.cpp index 00ab70f3..6b3d50e7 100644 --- a/src/geant4_application/src/ActionInitialization.cpp +++ b/src/geant4_application/src/ActionInitialization.cpp @@ -8,6 +8,8 @@ #include "geant4_application/SteppingVerbose.h" #include "geant4_application/TrackingAction.h" +#include +#include #include using namespace std; @@ -27,7 +29,8 @@ void ActionInitialization::Build() const { SetUserAction(new StackingAction); SetUserAction(new TrackingAction); - // G4EventManager::GetEventManager()->SetNumberOfAdditionalWaitingStacks(1); // optical stack + // Required for optical photons (scintillation/Cerenkov use an extra stack). + G4RunManager::GetRunManager()->SetNumberOfAdditionalWaitingStacks(1); } G4VSteppingVerbose* ActionInitialization::InitializeSteppingVerbose() const { diff --git a/src/geant4_application/src/Application.cpp b/src/geant4_application/src/Application.cpp index 9e97a013..ae383717 100644 --- a/src/geant4_application/src/Application.cpp +++ b/src/geant4_application/src/Application.cpp @@ -8,6 +8,26 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -48,7 +68,7 @@ void Application::SetupDetector(const string& gdml) { runManager->SetUserInitialization(new DetectorConstruction(gdml)); } -void Application::SetupPhysics() { +void Application::SetupPhysics(const string& physicsListName, bool optical) { if (G4RunManager::GetRunManager() == nullptr) { throw runtime_error("The run manager needs to be set up before the physics list"); } @@ -58,11 +78,90 @@ void Application::SetupPhysics() { } delete runManager->GetUserPhysicsList(); - auto physics = new PhysicsList(); + G4VUserPhysicsList* physics = nullptr; + if (physicsListName.empty() || physicsListName == "custom") { + physics = new PhysicsList(optical); + } else { + G4PhysListFactory factory; + if (!factory.IsReferencePhysList(physicsListName)) { + throw runtime_error("Unknown Geant4 reference physics list: " + physicsListName); + } + physics = factory.GetReferencePhysList(physicsListName); + if (optical) { + auto modular = dynamic_cast(physics); + if (modular == nullptr) { + delete physics; + throw runtime_error("Optical physics requires a modular physics list"); + } + modular->RegisterPhysics(new G4OpticalPhysics()); + } + } physics->SetVerboseLevel(0); runManager->SetUserInitialization(physics); } +vector Application::GetAvailablePhysicsLists() { + G4PhysListFactory factory; + const auto available = factory.AvailablePhysLists(); + vector names = {"custom"}; + names.reserve(available.size() + 1); + for (const auto& name: available) { + names.emplace_back(name.c_str()); + } + return names; +} + +static G4VPhysicsConstructor* CreateExtraPhysics(const string& name) { + if (name == "G4OpticalPhysics") return new G4OpticalPhysics(); + if (name == "G4EmExtraPhysics") return new G4EmExtraPhysics(); + if (name == "G4RadioactiveDecayPhysics") return new G4RadioactiveDecayPhysics(); + if (name == "G4DecayPhysics") return new G4DecayPhysics(); + if (name == "G4EmDNAPhysics") return new G4EmDNAPhysics(); + if (name == "G4EmDNAPhysics_option1") return new G4EmDNAPhysics_option1(); + if (name == "G4EmDNAPhysics_option2") return new G4EmDNAPhysics_option2(); + if (name == "G4EmDNAPhysics_option3") return new G4EmDNAPhysics_option3(); + if (name == "G4EmDNAPhysics_option4") return new G4EmDNAPhysics_option4(); + if (name == "G4EmDNAPhysics_option5") return new G4EmDNAPhysics_option5(); + if (name == "G4EmDNAPhysics_option6") return new G4EmDNAPhysics_option6(); + if (name == "G4EmDNAPhysics_option7") return new G4EmDNAPhysics_option7(); + if (name == "G4EmDNAPhysics_option8") return new G4EmDNAPhysics_option8(); + if (name == "G4EmDNAChemistry") return new G4EmDNAChemistry(); + if (name == "G4EmDNAChemistry_option1") return new G4EmDNAChemistry_option1(); + if (name == "G4EmDNAChemistry_option2") return new G4EmDNAChemistry_option2(); + if (name == "G4EmDNAChemistry_option3") return new G4EmDNAChemistry_option3(); + return nullptr; +} + +vector Application::GetAvailableExtraPhysics() { + return {"G4OpticalPhysics", "G4EmExtraPhysics", "G4RadioactiveDecayPhysics", "G4DecayPhysics", + "G4EmDNAPhysics", "G4EmDNAPhysics_option1", "G4EmDNAPhysics_option2", "G4EmDNAPhysics_option3", + "G4EmDNAPhysics_option4", "G4EmDNAPhysics_option5", "G4EmDNAPhysics_option6", + "G4EmDNAPhysics_option7", "G4EmDNAPhysics_option8", + "G4EmDNAChemistry", "G4EmDNAChemistry_option1", "G4EmDNAChemistry_option2", "G4EmDNAChemistry_option3"}; +} + +void Application::AddExtraPhysics(const string& constructorName) { + if (G4RunManager::GetRunManager() == nullptr) { + throw runtime_error("The run manager needs to be set up before adding physics"); + } + if (runManager->GetUserPhysicsList() == nullptr) { + throw runtime_error("Setup physics before adding extra physics constructors"); + } + if (IsInitialized()) { + throw runtime_error("Extra physics cannot be added after initialization"); + } + auto* modular = dynamic_cast(const_cast(runManager->GetUserPhysicsList())); + if (modular == nullptr) { + throw runtime_error("Extra physics requires a modular physics list"); + } + G4VPhysicsConstructor* ctor = CreateExtraPhysics(constructorName); + if (ctor == nullptr) { + throw runtime_error("Unknown extra physics constructor: " + constructorName + + ". See available_extra_physics()"); + } + modular->RegisterPhysics(ctor); +} + void Application::SetupAction() { if (G4RunManager::GetRunManager() == nullptr) { throw runtime_error("The run manager needs to be set up before the action initialization"); @@ -88,13 +187,32 @@ void Application::SetupManager(unsigned short nThreads) { delete G4VSteppingVerbose::GetInstance(); SteppingVerbose::SetInstance(new SteppingVerbose); +#ifndef G4MULTITHREADED + if (nThreads > 0) { + throw runtime_error( + "This Geant4 installation was built without multithreading support; " + "use n_threads=0"); + } +#endif + const auto runManagerType = nThreads > 0 ? G4RunManagerType::MTOnly : G4RunManagerType::SerialOnly; runManager = unique_ptr(G4RunManagerFactory::CreateRunManager(runManagerType)); + // Instantiate the command-based scoring manager so /score/* UI commands + // are available to Python before initialization. + G4ScoringManager::GetScoringManager(); if (nThreads > 0) { runManager->SetNumberOfThreads((G4int) nThreads); } } +bool Application::MultithreadingAvailable() { +#ifdef G4MULTITHREADED + return true; +#else + return false; +#endif +} + void Application::Initialize() { if (runManager != nullptr && runManager->GetUserDetectorConstruction() != nullptr && runManager->GetUserPhysicsList() != nullptr && runManager->GetUserActionInitialization() == nullptr) { @@ -145,30 +263,42 @@ py::list Application::Run(const py::object& primaries) { const auto fields = py::cast(primaries.attr("fields")); const auto nEvents = py::cast(len_func(primaries)); - if (fields.contains("energy")) { - std::vector energies = py::cast>(primaries.attr("energy")); + py::object primaryRecords = primaries; + if (fields.contains("primaries")) { + primaryRecords = primaries.attr("primaries"); + auto countsObject = ak.attr("to_list")(ak.attr("num")(primaryRecords, py::arg("axis") = 1)); + auto counts = py::cast>(countsObject); + vector offsets = {0}; + for (const auto count: counts) offsets.push_back(offsets.back() + count); + PrimaryGeneratorAction::SetAwkwardPrimaryEventOffsets(offsets); + primaryRecords = ak.attr("flatten")(primaryRecords, py::arg("axis") = 1); + } + const auto primaryFields = py::cast(primaryRecords.attr("fields")); + + if (primaryFields.contains("energy")) { + std::vector energies = py::cast>(primaryRecords.attr("energy")); PrimaryGeneratorAction::SetAwkwardPrimaryEnergies(energies); } - if (fields.contains("particle")) { - std::vector particles = py::cast>(primaries.attr("particle")); + if (primaryFields.contains("particle")) { + std::vector particles = py::cast>(primaryRecords.attr("particle")); PrimaryGeneratorAction::SetAwkwardPrimaryParticles(particles); } - if (fields.contains("position")) { - std::vector positionX = py::cast>(primaries.attr("position")["x"]); - std::vector positionY = py::cast>(primaries.attr("position")["y"]); - std::vector positionZ = py::cast>(primaries.attr("position")["z"]); + if (primaryFields.contains("position")) { + std::vector positionX = py::cast>(primaryRecords.attr("position")["x"]); + std::vector positionY = py::cast>(primaryRecords.attr("position")["y"]); + std::vector positionZ = py::cast>(primaryRecords.attr("position")["z"]); std::vector> positions; - for (size_t i = 0; i < nEvents; i++) { + for (size_t i = 0; i < positionX.size(); i++) { positions.push_back({positionX[i], positionY[i], positionZ[i]}); } PrimaryGeneratorAction::SetAwkwardPrimaryPositions(positions); } - if (fields.contains("direction")) { - std::vector directionX = py::cast>(primaries.attr("direction")["x"]); - std::vector directionY = py::cast>(primaries.attr("direction")["y"]); - std::vector directionZ = py::cast>(primaries.attr("direction")["z"]); + if (primaryFields.contains("direction")) { + std::vector directionX = py::cast>(primaryRecords.attr("direction")["x"]); + std::vector directionY = py::cast>(primaryRecords.attr("direction")["y"]); + std::vector directionZ = py::cast>(primaryRecords.attr("direction")["z"]); std::vector> directions; - for (size_t i = 0; i < nEvents; i++) { + for (size_t i = 0; i < directionX.size(); i++) { directions.push_back({directionX[i], directionY[i], directionZ[i]}); } PrimaryGeneratorAction::SetAwkwardPrimaryDirections(directions); @@ -179,6 +309,7 @@ py::list Application::Run(const py::object& primaries) { } } + bool Application::IsSetup() const { return runManager != nullptr && runManager->GetUserDetectorConstruction() != nullptr && runManager->GetUserPhysicsList() != nullptr && runManager->GetUserActionInitialization() != nullptr; diff --git a/src/geant4_application/src/DataModel.cpp b/src/geant4_application/src/DataModel.cpp index 993e0365..56562d12 100644 --- a/src/geant4_application/src/DataModel.cpp +++ b/src/geant4_application/src/DataModel.cpp @@ -42,7 +42,7 @@ void InsertEvent(const G4Event* event, Builders& builders) { } if (builders.fields.find("primaries") != builders.fields.end()) { builders.primaries.begin_list(); - const auto primaryVertex = event->GetPrimaryVertex(); + for (auto primaryVertex = event->GetPrimaryVertex(); primaryVertex != nullptr; primaryVertex = primaryVertex->GetNext()) { for (int i = 0; i < primaryVertex->GetNumberOfParticle(); i++) { const auto primary = primaryVertex->GetPrimary(i); const auto particle = primary->GetParticleDefinition(); @@ -55,6 +55,7 @@ void InsertEvent(const G4Event* event, Builders& builders) { builders.primaries.content().content<2>().append(position / units::length); builders.primaries.content().content<3>().append(direction); } + } builders.primaries.end_list(); } } diff --git a/src/geant4_application/src/DetectorConstruction.cpp b/src/geant4_application/src/DetectorConstruction.cpp index 8dce9f7b..b7b1ca92 100644 --- a/src/geant4_application/src/DetectorConstruction.cpp +++ b/src/geant4_application/src/DetectorConstruction.cpp @@ -6,6 +6,16 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -14,6 +24,29 @@ using namespace std; using namespace geant4_app; namespace fs = std::filesystem; +namespace { +// Uniform B (tesla) + E field for G4EqMagElectricField. +// Field value layout is [Bx,By,Bz,Ex,Ey,Ez] (see G4UniformElectricField). +class UniformEMField : public G4ElectroMagneticField { +public: + UniformEMField(const G4ThreeVector& b, const G4ThreeVector& e) : bField(b), eField(e) {} + void GetFieldValue(const G4double*, G4double* field) const override { + field[0] = bField.x(); + field[1] = bField.y(); + field[2] = bField.z(); + field[3] = eField.x(); + field[4] = eField.y(); + field[5] = eField.z(); + } + G4bool DoesFieldChangeEnergy() const override { return true; } +private: + G4ThreeVector bField; + G4ThreeVector eField; +}; +} // namespace + +G4ThreadLocal G4GlobalMagFieldMessenger* DetectorConstruction::magneticFieldMessenger = nullptr; + DetectorConstruction::DetectorConstruction(const string& gdml) : G4VUserDetectorConstruction() { SetGDML(gdml); // TODO: store this string as compressed data? @@ -35,6 +68,20 @@ void DetectorConstruction::SetSensitiveVolumes(const set& volumes) { sensitiveVolumes = volumes; } +void DetectorConstruction::SetMagneticField(const array& fieldTesla) { + if (fieldConstructed) { + throw runtime_error("Magnetic field cannot be changed after initialization"); + } + magneticFieldTesla = fieldTesla; +} + +void DetectorConstruction::SetElectricField(const array& fieldKVperCM) { + if (fieldConstructed) { + throw runtime_error("Electric field cannot be changed after initialization"); + } + electricFieldKVperCM = fieldKVperCM; +} + G4VPhysicalVolume* DetectorConstruction::Construct() { G4GDMLParser parser; @@ -89,6 +136,42 @@ void DetectorConstruction::ConstructSDandField() { // TODO: Regions } sensitiveDetectorConstructed = true; + + const bool hasElectric = (electricFieldKVperCM[0] != 0. || electricFieldKVperCM[1] != 0. || electricFieldKVperCM[2] != 0.); + if (!hasElectric) { + const G4ThreeVector field( + magneticFieldTesla[0] * tesla, + magneticFieldTesla[1] * tesla, + magneticFieldTesla[2] * tesla); + magneticFieldMessenger = new G4GlobalMagFieldMessenger(field); + magneticFieldMessenger->SetVerboseLevel(0); + G4AutoDelete::Register(magneticFieldMessenger); + } else { + // Combined uniform B (tesla) + E (kV/cm) via custom EM field. + // Mirrors extended/field/field02 (G4EqMagElectricField + ClassicalRK4). + auto* emField = new UniformEMField( + G4ThreeVector( + magneticFieldTesla[0] * tesla, + magneticFieldTesla[1] * tesla, + magneticFieldTesla[2] * tesla), + G4ThreeVector( + electricFieldKVperCM[0] * kilovolt / cm, + electricFieldKVperCM[1] * kilovolt / cm, + electricFieldKVperCM[2] * kilovolt / cm)); + auto* equation = new G4EqMagElectricField(emField); + auto* stepper = new G4ClassicalRK4(equation, 8); + auto* driver = new G4MagInt_Driver(0.01 * mm, stepper, stepper->GetNumberOfVariables()); + auto* fieldManager = G4TransportationManager::GetTransportationManager()->GetFieldManager(); + fieldManager->SetDetectorField(emField); + fieldManager->SetFieldChangesEnergy(true); + fieldManager->SetChordFinder(new G4ChordFinder(driver)); + G4AutoDelete::Register(emField); + G4AutoDelete::Register(equation); + G4AutoDelete::Register(stepper); + // ChordFinder owns driver; do not double-register driver. + magneticFieldMessenger = nullptr; + } + fieldConstructed = true; } set DetectorConstruction::GetMaterialNames() { diff --git a/src/geant4_application/src/PhysicsList.cpp b/src/geant4_application/src/PhysicsList.cpp index 90b4ab11..d94b2cd1 100644 --- a/src/geant4_application/src/PhysicsList.cpp +++ b/src/geant4_application/src/PhysicsList.cpp @@ -9,13 +9,14 @@ #include #include #include +#include #include #include #include using namespace geant4_app; -PhysicsList::PhysicsList() : G4VModularPhysicsList() { +PhysicsList::PhysicsList(bool optical) : G4VModularPhysicsList() { SetVerboseLevel(1); // Decay physics @@ -41,4 +42,8 @@ PhysicsList::PhysicsList() : G4VModularPhysicsList() { // Neutron tracking cut RegisterPhysics(new G4NeutronTrackingCut()); + + if (optical) { + RegisterPhysics(new G4OpticalPhysics()); + } } diff --git a/src/geant4_application/src/PrimaryGeneratorAction.cpp b/src/geant4_application/src/PrimaryGeneratorAction.cpp index a56b7725..e3958967 100644 --- a/src/geant4_application/src/PrimaryGeneratorAction.cpp +++ b/src/geant4_application/src/PrimaryGeneratorAction.cpp @@ -16,6 +16,27 @@ using namespace geant4_app; PrimaryGeneratorAction::PrimaryGeneratorAction() : G4VUserPrimaryGeneratorAction() {} void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event) { + if (!awkwardPrimaryEventOffsets.empty()) { + const auto eventId = static_cast(event->GetEventID()); + for (size_t i = awkwardPrimaryEventOffsets[eventId]; i < awkwardPrimaryEventOffsets[eventId + 1]; ++i) { + if (!awkwardPrimaryEnergies.empty()) gun.SetParticleEnergy(awkwardPrimaryEnergies[i] * keV); + if (!awkwardPrimaryPositions.empty()) { + const auto& p = awkwardPrimaryPositions[i]; + gun.SetParticlePosition(G4ThreeVector(p[0] * cm, p[1] * cm, p[2] * cm)); + } + if (!awkwardPrimaryDirections.empty()) { + const auto& d = awkwardPrimaryDirections[i]; + gun.SetParticleMomentumDirection(G4ThreeVector(d[0], d[1], d[2])); + } + if (!awkwardPrimaryParticles.empty()) { + auto* particle = G4ParticleTable::GetParticleTable()->FindParticle(awkwardPrimaryParticles[i]); + if (particle == nullptr) throw runtime_error("Primary particle not found: " + awkwardPrimaryParticles[i]); + gun.SetParticleDefinition(particle); + } + gun.GeneratePrimaryVertex(event); + } + return; + } if (!awkwardPrimaryEnergies.empty()) { const double energy = awkwardPrimaryEnergies[event->GetEventID()]; gun.SetParticleEnergy(energy * keV); @@ -47,9 +68,9 @@ void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event) { } void PrimaryGeneratorAction::SetGeneratorType(const string& type) { - const auto allowedTypes = set({"gun", "gps", "python"}); + const auto allowedTypes = set({"gun", "gps"}); if (allowedTypes.find(type) == allowedTypes.end()) { - throw runtime_error("PrimaryGeneratorAction::SetGeneratorType - type must be 'gun', 'gps', or 'python'"); + throw runtime_error("PrimaryGeneratorAction::SetGeneratorType - type must be 'gun' or 'gps'"); } generatorType = type; } @@ -83,9 +104,11 @@ void PrimaryGeneratorAction::ClearAwkwardPrimaries() { awkwardPrimaryPositions.clear(); awkwardPrimaryDirections.clear(); awkwardPrimaryParticles.clear(); + awkwardPrimaryEventOffsets.clear(); } vector PrimaryGeneratorAction::awkwardPrimaryEnergies = {}; vector> PrimaryGeneratorAction::awkwardPrimaryPositions = {}; vector> PrimaryGeneratorAction::awkwardPrimaryDirections = {}; vector PrimaryGeneratorAction::awkwardPrimaryParticles = {}; +vector PrimaryGeneratorAction::awkwardPrimaryEventOffsets = {}; diff --git a/src/geant4_application/src/SteppingVerbose.cpp b/src/geant4_application/src/SteppingVerbose.cpp index 11dc7692..d1ff8ba1 100644 --- a/src/geant4_application/src/SteppingVerbose.cpp +++ b/src/geant4_application/src/SteppingVerbose.cpp @@ -12,8 +12,11 @@ SteppingVerbose::SteppingVerbose() : G4SteppingVerbose() {} void SteppingVerbose::TrackingStarted() { CopyState(); + // Optical stacking (scintillation/Cerenkov) can invoke TrackingStarted + // with CurrentStepNumber != 0 for resumed tracks; skip duplicate init + // record instead of aborting the run. if (fStep->GetTrack()->GetCurrentStepNumber() != 0) { - throw runtime_error("SteppingVerbose::TrackingStarted: fStep->GetTrack()->GetCurrentStepNumber() != 0"); + return; } data::InsertStep(fStep, RunAction::GetBuilder()); } diff --git a/src/geant4_python_application/__init__.py b/src/geant4_python_application/__init__.py index a6d16ad7..29df4537 100644 --- a/src/geant4_python_application/__init__.py +++ b/src/geant4_python_application/__init__.py @@ -12,8 +12,15 @@ application_data_directory as data_directory, ) from geant4_python_application.files.datasets import install_datasets +from geant4_python_application.generator import Generator +from geant4_python_application.scoring import NativeScoringMesh, Scoring from geant4_python_application.files.directories import application_directory from geant4_python_application.gdml import basic_gdml +from geant4_python_application import optical as optical +from geant4_python_application import cad as cad +from geant4_python_application import io as io +from geant4_python_application import distributed as distributed +from geant4_python_application import geometry as geometry __version__ = version __version__tuple__ = version_tuple @@ -21,14 +28,22 @@ __all__ = [ "Application", "Detector", + "Generator", + "NativeScoringMesh", + "Scoring", "__version__", "__version__tuple__", "application_directory", "awkward_version", "basic_gdml", + "cad", "data_directory", + "distributed", "geant4_version", + "geometry", "install_datasets", + "io", + "optical", "pybind11_version", "version", ] diff --git a/src/geant4_python_application/cad.py b/src/geant4_python_application/cad.py new file mode 100644 index 00000000..74987ab4 --- /dev/null +++ b/src/geant4_python_application/cad.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +"""CAD -> GDML helpers via tessellated solids (no external CAD kernel). + +Provides: +- mesh_to_gdml(vertices, faces): build GDML with a solid. +- stl_ascii_to_gdml(stl_text): parse ASCII STL -> GDML (triangles only). +Both produce a self-contained GDML string with world box + placed volume, +suitable for Application(gdml=...). +""" + + +def mesh_to_gdml( + vertices: list[tuple[float, float, float]], + faces: list[tuple[int, int, int]], + *, + name: str = "cad_solid", + material: str = "G4_WATER", + world_size_mm: float = 1000.0, + unit: str = "mm", +) -> str: + if len(vertices) == 0 or len(faces) == 0: + msg = "vertices and faces must be non-empty" + raise ValueError(msg) + pos_lines = [] + for i, (x, y, z) in enumerate(vertices): + pos_lines.append( + f' ' + ) + tri_lines = [] + for a, b, c in faces: + if min(a, b, c) < 0 or max(a, b, c) >= len(vertices): + msg = f"face index out of range: {(a, b, c)}" + raise ValueError(msg) + tri_lines.append( + f' ' + ) + positions = "\n".join(pos_lines) + triangles = "\n".join(tri_lines) + return f""" + + +{positions} + + + + + + + + +{triangles} + + + + + + + + + + + + + + + + + + +""" + + +def cube_mesh(size_mm: float = 100.0) -> tuple[list, list]: + """Return (vertices, faces) for an axis-aligned cube centred at origin.""" + h = size_mm / 2.0 + v = [ + (-h, -h, -h), (h, -h, -h), (h, h, -h), (-h, h, -h), + (-h, -h, h), (h, -h, h), (h, h, h), (-h, h, h), + ] + f = [ + (0, 1, 2), (0, 2, 3), # -z + (4, 6, 5), (4, 7, 6), # +z + (0, 4, 5), (0, 5, 1), # -y + (3, 2, 6), (3, 6, 7), # +y + (0, 3, 7), (0, 7, 4), # -x + (1, 5, 6), (1, 6, 2), # +x + ] + return v, f + + +def stl_ascii_to_gdml(stl_text: str, **kwargs) -> str: + """Parse minimal ASCII STL (facet normal/vertex lines) into GDML.""" + verts: list[tuple[float, float, float]] = [] + index: dict[tuple[float, float, float], int] = {} + faces: list[tuple[int, int, int]] = [] + cur: list[int] = [] + for line in stl_text.splitlines(): + s = line.strip().split() + if len(s) >= 4 and s[0] == "vertex": + p = (float(s[1]), float(s[2]), float(s[3])) + if p not in index: + index[p] = len(verts) + verts.append(p) + cur.append(index[p]) + if len(cur) == 3: + faces.append((cur[0], cur[1], cur[2])) + cur = [] + if not faces: + msg = "no facets parsed from STL text" + raise ValueError(msg) + return mesh_to_gdml(verts, faces, **kwargs) diff --git a/src/geant4_python_application/detector.py b/src/geant4_python_application/detector.py index fec0c1f0..21e50b99 100644 --- a/src/geant4_python_application/detector.py +++ b/src/geant4_python_application/detector.py @@ -47,6 +47,36 @@ def sensitive_volumes(self, volumes): Message("detector", "set_sensitive_volumes", (volumes,), {}) ) + @property + def magnetic_field(self) -> tuple[float, float, float]: + """Uniform global magnetic field vector in tesla.""" + return tuple( + self._application._send_and_recv( + Message("detector", "get_magnetic_field", (), {}) + ) + ) + + @magnetic_field.setter + def magnetic_field(self, field_tesla: tuple[float, float, float]): + self._application._send_and_recv( + Message("detector", "set_magnetic_field", (field_tesla,), {}) + ) + + @property + def electric_field(self) -> tuple[float, float, float]: + """Uniform global electric field vector in kV/cm.""" + return tuple( + self._application._send_and_recv( + Message("detector", "get_electric_field", (), {}) + ) + ) + + @electric_field.setter + def electric_field(self, field_kv_per_cm: tuple[float, float, float]): + self._application._send_and_recv( + Message("detector", "set_electric_field", (field_kv_per_cm,), {}) + ) + def material_from_volume(self, volume: str) -> str: return self._application._send_and_recv( Message("detector", "get_material_from_volume", (volume,), {}) diff --git a/src/geant4_python_application/distributed.py b/src/geant4_python_application/distributed.py new file mode 100644 index 00000000..240a1886 --- /dev/null +++ b/src/geant4_python_application/distributed.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +"""MPI / distributed helpers with graceful fallback (no hard mpi4py dep).""" + +import os + + +def get_rank_size() -> tuple[int, int]: + """Return (rank, size) from mpi4py if available, else env vars, else (0,1).""" + try: + from mpi4py import MPI # type: ignore + + comm = MPI.COMM_WORLD + return int(comm.Get_rank()), int(comm.Get_size()) + except Exception: + pass + for rank_var, size_var in [ + ("OMPI_COMM_WORLD_RANK", "OMPI_COMM_WORLD_SIZE"), + ("PMI_RANK", "PMI_SIZE"), + ("MPI_RANK", "MPI_SIZE"), + ]: + if rank_var in os.environ and size_var in os.environ: + try: + return int(os.environ[rank_var]), int(os.environ[size_var]) + except ValueError: + continue + return 0, 1 + + +def split_count(n_total: int, rank: int, size: int) -> tuple[int, int]: + """Return (start, count) for rank under even split of n_total events.""" + if n_total < 0 or rank < 0 or size <= 0 or rank >= size: + msg = f"invalid split args: n={n_total} rank={rank} size={size}" + raise ValueError(msg) + base, rem = divmod(n_total, size) + start = rank * base + min(rank, rem) + count = base + (1 if rank < rem else 0) + return start, count + + +def local_count(n_total: int) -> tuple[int, int, int]: + """Return (rank, size, count) for this process.""" + rank, size = get_rank_size() + _, count = split_count(n_total, rank, size) + return rank, size, count diff --git a/src/geant4_python_application/events.py b/src/geant4_python_application/events.py index bbb46fb4..1d1b2d49 100644 --- a/src/geant4_python_application/events.py +++ b/src/geant4_python_application/events.py @@ -23,8 +23,9 @@ def hits(self, volume: str | None = None) -> ak.Array: hits = hits[hits.energy > 0] if volume is not None: hits = hits[hits.volume == volume] - # only keep x,y,z, time, and energy - hits = hits[["position", "time", "energy"]] + # Keep available hit fields; event-field selection may omit time or position. + fields = [field for field in ("position", "time", "energy") if field in hits.fields] + hits = hits[fields] return ak.Array(hits, with_name="Geant4Hits") diff --git a/src/geant4_python_application/generator.py b/src/geant4_python_application/generator.py new file mode 100644 index 00000000..4b69a268 --- /dev/null +++ b/src/geant4_python_application/generator.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from geant4_python_application.application import Message + +if TYPE_CHECKING: + from geant4_python_application.application import Application + + +class Generator: + """Configure Geant4's particle gun or General Particle Source (GPS).""" + + def __init__(self, application: Application): + self._application = application + + @property + def type(self) -> str: + return self._application._send_and_recv(Message("generator", "get_type", (), {})) + + @type.setter + def type(self, generator_type: str): + self._application._send_and_recv( + Message("generator", "set_type", (generator_type,), {}) + ) + + def use_gun(self) -> Generator: + self.type = "gun" + return self + + def use_gps(self) -> Generator: + self.type = "gps" + return self + + def particle(self, name: str) -> Generator: + command = "/gps/particle" if self.type == "gps" else "/gun/particle" + self._application.command(f"{command} {name}") + return self + + def energy(self, value: float, unit: str = "MeV") -> Generator: + command = "/gps/ene/mono" if self.type == "gps" else "/gun/energy" + self._application.command(f"{command} {value} {unit}") + return self + + def position( + self, x: float, y: float, z: float, unit: str = "cm" + ) -> Generator: + if self.type == "gps": + self._application.command("/gps/pos/type Point") + self._application.command(f"/gps/pos/centre {x} {y} {z} {unit}") + else: + self._application.command(f"/gun/position {x} {y} {z} {unit}") + return self + + def direction(self, x: float, y: float, z: float) -> Generator: + command = "/gps/direction" if self.type == "gps" else "/gun/direction" + self._application.command(f"{command} {x} {y} {z}") + return self + + def commands(self, commands: list[str]) -> Generator: + """Apply advanced source commands such as GPS distributions.""" + self._application.commands(commands) + return self diff --git a/src/geant4_python_application/geometry.py b/src/geant4_python_application/geometry.py new file mode 100644 index 00000000..5fd659a0 --- /dev/null +++ b/src/geant4_python_application/geometry.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +"""Pure-Python GDML geometry generators. + +These helpers expand regular layouts into ordinary GDML placements. They are +useful where an official example uses a live ``G4VPVParameterisation`` callback: +the geometry is equivalent at initialization time, but there is no callback +during tracking. +""" + + +def linear_array_gdml( + count: int, + *, + spacing_mm: float = 20.0, + box_size_mm: float = 10.0, + material: str = "G4_Si", + world_material: str = "G4_AIR", +) -> str: + """Return GDML containing ``count`` equally spaced boxes along x.""" + if count < 1: + raise ValueError("count must be at least one") + if spacing_mm <= 0 or box_size_mm <= 0: + raise ValueError("spacing_mm and box_size_mm must be positive") + extent = (count - 1) * spacing_mm + box_size_mm + world_x = max(100.0, extent + 4.0 * spacing_mm) + placements = [] + x0 = -0.5 * (count - 1) * spacing_mm + for index in range(count): + x = x0 + index * spacing_mm + placements.append( + f''' + + + ''' + ) + return f''' + + + + + + + + + +{chr(10).join(placements)} + + + + +''' diff --git a/src/geant4_python_application/io.py b/src/geant4_python_application/io.py new file mode 100644 index 00000000..cdb66ec3 --- /dev/null +++ b/src/geant4_python_application/io.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +"""VTK / ROOT / external-generator helpers (pure Python, optional deps). + +- mesh_to_vtk: write Cartesian energy mesh to legacy VTK STRUCTURED_POINTS. +- events_to_parquet / events_from_parquet: always available (pyarrow). +- events_to_root / events_from_root: via uproot if installed. +- hepmc_to_primaries: minimal HepMC2-ASCII -> awkward primaries. +""" + +from pathlib import Path + +import awkward as ak +import numpy as np + + +def mesh_to_vtk(mesh: np.ndarray, edges: tuple, filename: str | Path) -> Path: + """Write (nx,ny,nz) energy mesh to legacy VTK STRUCTURED_POINTS file.""" + mesh = np.asarray(mesh, dtype=float) + if mesh.ndim != 3: + msg = f"mesh must be 3D, got shape {mesh.shape}" + raise ValueError(msg) + nx, ny, nz = mesh.shape + if len(edges) != 3: + msg = "edges must be (x_edges, y_edges, z_edges)" + raise ValueError(msg) + origins, spacings = [], [] + dims = [] + for n, e in zip((nx, ny, nz), edges): + e = np.asarray(e, dtype=float) + if len(e) != n + 1: + msg = f"edges length {len(e)} inconsistent with mesh dim {n}" + raise ValueError(msg) + origins.append(float(e[0])) + spacings.append(float(e[1] - e[0]) if n > 0 else 1.0) + dims.append(n + 1 if False else n) # points == cells for cell data + out = Path(filename) + # VTK Fortran ordering: x varies fastest + flat = mesh.ravel(order="F") + with out.open("w") as f: + f.write("# vtk DataFile Version 3.0\n") + f.write("geant4 energy mesh\n") + f.write("ASCII\n") + f.write("DATASET STRUCTURED_POINTS\n") + f.write(f"DIMENSIONS {nx} {ny} {nz}\n") + f.write(f"ORIGIN {origins[0]} {origins[1]} {origins[2]}\n") + f.write(f"SPACING {spacings[0]} {spacings[1]} {spacings[2]}\n") + f.write(f"POINT_DATA {nx * ny * nz}\n") + f.write("SCALARS energy_keV float 1\n") + f.write("LOOKUP_TABLE default\n") + for v in flat: + f.write(f"{v}\n") + return out + + +def events_to_parquet(events: ak.Array, filename: str | Path) -> Path: + out = Path(filename) + ak.to_parquet(events, out) + return out + + +def events_from_parquet(filename: str | Path) -> ak.Array: + return ak.from_parquet(Path(filename)) + + +def events_to_root(events: ak.Array, filename: str | Path, treename: str = "events") -> Path: + """Write flat per-event summary + hits to ROOT via uproot (requires uproot).""" + try: + import uproot + except ImportError as e: + msg = "uproot is required for ROOT I/O: pip install uproot" + raise ImportError(msg) from e + out = Path(filename) + # Flatten what ROOT can store: per-event id + deposited energy + nhits. + # Full nested step records stay in parquet; ROOT holds analysis summary. + import geant4_python_application as g4 + + edep = np.asarray(g4.Scoring.energy_deposit(events), dtype=np.float64) + ids = np.asarray(events.id, dtype=np.int64) if "id" in events.fields else np.arange(len(events)) + with uproot.recreate(out) as f: + f[treename] = {"id": ids, "edep_keV": edep} + return out + + +def events_from_root(filename: str | Path, treename: str = "events") -> dict: + try: + import uproot + except ImportError as e: + msg = "uproot is required for ROOT I/O: pip install uproot" + raise ImportError(msg) from e + with uproot.open(f"{filename}:{treename}") as t: + return t.arrays(library="np") + + +_PDG_TO_G4 = { + 11: "e-", -11: "e+", 22: "gamma", 13: "mu-", -13: "mu+", + 2112: "neutron", 2212: "proton", 211: "pi+", -211: "pi-", + 111: "pi0", 321: "kaon+", -321: "kaon-", 12: "nu_e", + -12: "anti_nu_e", 14: "nu_mu", -14: "anti_nu_mu", +} + + +def hepmc_to_primaries(hepmc_text: str) -> ak.Array: + """Parse minimal HepMC2 ASCII (E/P lines) into awkward primaries. + + Expects lines like: P ... + Position defaults to origin; direction = p/|p|; energy = kinetic approx. + Unknown PDG codes fall back to geantino. + """ + records = [] + for line in hepmc_text.splitlines(): + s = line.strip().split() + if not s or s[0] != "P": + continue + if len(s) < 7: + continue + pdg = int(s[2]) + px, py, pz, e = (float(s[3]), float(s[4]), float(s[5]), float(s[6])) + p = float(np.sqrt(px * px + py * py + pz * pz)) + if p == 0: + continue + particle = _PDG_TO_G4.get(pdg, "geantino") + # awkward primary energy convention in this package: keV numbers? + # Application primaries use raw doubles passed to gun in keV? Keep MeV->keV. + records.append( + { + "particle": particle, + "energy": e * 1e6, # GeV -> keV (gun awkward path uses keV) + "direction": {"x": px / p, "y": py / p, "z": pz / p}, + "position": {"x": 0.0, "y": 0.0, "z": 0.0}, + } + ) + if not records: + msg = "no P lines parsed from HepMC text" + raise ValueError(msg) + return ak.Array(records) diff --git a/src/geant4_python_application/optical.py b/src/geant4_python_application/optical.py new file mode 100644 index 00000000..15287350 --- /dev/null +++ b/src/geant4_python_application/optical.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +"""Minimal optical-materials/surfaces helpers (GDML-based). + +Geant4 optical properties (RINDEX, scintillation, WLS, ...) and optical +surfaces (skin/border) are defined in GDML via , and +// tags. This module provides a +compact, fast-simulating water-scintillator GDML example plus UI-command +helpers for tuning optical processes at run time. +""" + +optical_water_gdml = R""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +# Compact, self-contained optical geometries used by the Python adaptations of +# Geant4's LXe and WLS examples. They intentionally model the transport +# concepts rather than the examples' custom PMT/sensitive-detector classes. +lxe_scintillator_gdml = R""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +wls_fiber_gdml = R""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +#: Common optical-process UI commands for Geant4 11.2 (apply via app.commands(...)). +#: Verified against /process/optical/scintillation and /process/optical/cerenkov. +optical_tuning_commands = [ + "/process/optical/scintillation/setStackPhotons true", + "/process/optical/cerenkov/setMaxPhotons 100", + "/process/optical/verbose 1", +] + + +def scintillation_commands(stack_photons: bool = True, max_cerenkov: int = 100) -> list[str]: + """Return UI commands tuning scintillation/Cerenkov at run time (Geant4 11.2).""" + return [ + f"/process/optical/scintillation/setStackPhotons {'true' if stack_photons else 'false'}", + f"/process/optical/cerenkov/setMaxPhotons {max_cerenkov}", + ] diff --git a/src/geant4_python_application/scoring.py b/src/geant4_python_application/scoring.py new file mode 100644 index 00000000..eddd8d90 --- /dev/null +++ b/src/geant4_python_application/scoring.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +import awkward as ak +import numpy as np + +if TYPE_CHECKING: + from geant4_python_application.application import Application + + +class NativeScoringMesh: + """Configure a Geant4 command-based Cartesian scoring mesh. + + This uses Geant4's native ``G4ScoringManager`` commands. It is distinct + from :class:`Scoring`, which bins recorded steps in Python after a run. + """ + + def __init__( + self, + application: Application, + name: str = "mesh", + quantity: str = "eDep", + ): + self.application = application + self.name = name + self.quantity = quantity + self._closed = False + + def box( + self, + half_size: tuple[float, float, float], + bins: tuple[int, int, int], + *, + unit: str = "mm", + center: tuple[float, float, float] = (0.0, 0.0, 0.0), + ) -> NativeScoringMesh: + """Create a box mesh; ``half_size`` follows Geant4 mesh semantics.""" + if self._closed: + raise RuntimeError("scoring mesh is already closed") + sx, sy, sz = half_size + nx, ny, nz = bins + cx, cy, cz = center + self.application.commands( + [ + f"/score/create/boxMesh {self.name}", + f"/score/mesh/boxSize {sx} {sy} {sz} {unit}", + f"/score/mesh/nBin {nx} {ny} {nz}", + f"/score/mesh/translate/xyz {cx} {cy} {cz} {unit}", + f"/score/quantity/energyDeposit {self.quantity}", + "/score/close", + ] + ) + self._closed = True + return self + + def dump(self, path: str | Path, *, option: str = "") -> Path: + """Dump the native mesh quantity after a run and return its path.""" + if not self._closed: + raise RuntimeError("configure and close the scoring mesh before dumping") + output = Path(path).expanduser().resolve() + output.parent.mkdir(parents=True, exist_ok=True) + if " " in str(output): + raise ValueError("Geant4 scoring output paths cannot contain spaces") + suffix = f" {option}" if option else "" + self.application.command( + f"/score/dumpQuantityToFile {self.name} {self.quantity} {output}{suffix}" + ) + return output + + +class Scoring: + """Scoring operations over Geant4 event, track, and step records.""" + + @staticmethod + def energy_deposit(events: ak.Array, volume: str | None = None) -> ak.Array: + """Return deposited energy in keV for each event.""" + steps = events.track.step + energy = steps.energy + if volume is not None: + energy = energy[steps.volume == volume] + return ak.sum(ak.flatten(energy, axis=-1), axis=1) + + @staticmethod + def hits(events: ak.Array, volume: str | None = None) -> ak.Array: + """Return positive-energy steps, optionally restricted to a volume.""" + return events.hits(volume) + + @staticmethod + def energy_mesh( + events: ak.Array, + bins: int | tuple[int, int, int] = 10, + mesh_range: tuple[tuple[float, float], tuple[float, float], tuple[float, float]] + | None = None, + volume: str | None = None, + ) -> tuple[np.ndarray, tuple[np.ndarray, np.ndarray, np.ndarray]]: + """Bin step energy into a Cartesian 3D mesh. + + Positions are in millimetres and mesh values are deposited energy in keV. + """ + hits = Scoring.hits(events, volume) + weights = ak.to_numpy(ak.flatten(hits.energy, axis=None)) + if len(weights) == 0: + shape = (bins, bins, bins) if isinstance(bins, int) else bins + return np.zeros(shape), (np.array([]), np.array([]), np.array([])) + + points = np.column_stack( + [ + ak.to_numpy(ak.flatten(hits.position.x, axis=None)), + ak.to_numpy(ak.flatten(hits.position.y, axis=None)), + ak.to_numpy(ak.flatten(hits.position.z, axis=None)), + ] + ) + mesh, edges = np.histogramdd( + points, bins=bins, range=mesh_range, weights=weights + ) + return mesh, edges diff --git a/src/python/module.cpp b/src/python/module.cpp index 80043512..aa6441b2 100644 --- a/src/python/module.cpp +++ b/src/python/module.cpp @@ -19,8 +19,13 @@ PYBIND11_MODULE(_geant4_application, m) { py::class_(m, "Application") .def(py::init<>()) .def("setup_manager", &Application::SetupManager, py::arg("n_threads")) + .def_static("multithreading_available", &Application::MultithreadingAvailable) .def("setup_detector", &Application::SetupDetector, py::arg("gdml")) - .def("setup_physics", &Application::SetupPhysics) + .def("setup_physics", &Application::SetupPhysics, + py::arg("physics_list") = "custom", py::arg("optical") = false) + .def_static("available_physics_lists", &Application::GetAvailablePhysicsLists) + .def("add_physics", &Application::AddExtraPhysics, py::arg("constructor_name")) + .def_static("available_extra_physics", &Application::GetAvailableExtraPhysics) .def("setup_action", &Application::SetupAction) .def("initialize", &Application::Initialize) .def("run", &Application::Run, py::arg("n_events")) @@ -44,6 +49,9 @@ PYBIND11_MODULE(_geant4_application, m) { [](const py::object&, const string& type) { return PrimaryGeneratorAction::SetGeneratorType(type); }) + .def_static("get_type", &PrimaryGeneratorAction::GetGeneratorType) + .def_static("set_type", &PrimaryGeneratorAction::SetGeneratorType, + py::arg("type")) .def_static("set_energy", &PrimaryGeneratorAction::SetEnergy, py::arg("energy")) .def_static("set_position", &PrimaryGeneratorAction::SetPosition, py::arg("position")) .def_static("set_direction", &PrimaryGeneratorAction::SetDirection, py::arg("direction")) @@ -56,6 +64,12 @@ PYBIND11_MODULE(_geant4_application, m) { .def_static("get_physical_volumes", &DetectorConstruction::GetPhysicalVolumeNames) .def("get_sensitive_volumes", &DetectorConstruction::GetSensitiveVolumes) .def("set_sensitive_volumes", &DetectorConstruction::SetSensitiveVolumes, py::arg("volumes")) + .def("get_magnetic_field", &DetectorConstruction::GetMagneticField) + .def("set_magnetic_field", &DetectorConstruction::SetMagneticField, + py::arg("field_tesla")) + .def("get_electric_field", &DetectorConstruction::GetElectricField) + .def("set_electric_field", &DetectorConstruction::SetElectricField, + py::arg("field_kV_per_cm")) .def_static("get_physical_volumes_from_logical_volume", &DetectorConstruction::GetPhysicalVolumesFromLogicalVolume, py::arg("logical_volume")) .def_static("get_logical_volume_from_physical_volume", &DetectorConstruction::GetLogicalVolumeFromPhysicalVolume, py::arg("physical_volume")) .def_static("get_material_from_volume", &DetectorConstruction::GetMaterialFromVolume, py::arg("volume")) diff --git a/tests/test_analysis.py b/tests/test_analysis.py index c05e7eeb..352ac736 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -8,6 +8,7 @@ import requests from geant4_python_application import Application +from geant4_python_application import Scoring complexGdml = requests.get( "https://raw.githubusercontent.com/rest-for-physics/restG4/dc3a8f42cea4978206a13325261fa85ec1b26261/examples/13.IAXO/geometry/setup.gdml" @@ -80,6 +81,8 @@ def test_sensitive(): ) @pytest.mark.parametrize("n_threads", [0, 8]) def test_photoelectric(n_threads): + if n_threads > 0 and not Application.multithreading_available(): + pytest.skip("linked Geant4 runtime was built without multithreading") with Application(gdml=complexGdml, seed=1234, n_threads=n_threads) as app: sensitive_volume = "gasVolume" @@ -152,4 +155,21 @@ def test_muons(): sensitive_energy = events.energy_in_volume(volume) assert np.isclose(np.average(sensitive_energy), 24.821580016670598, rtol=1e-1) - assert np.isclose(np.std(sensitive_energy), 16.09166011697945, rtol=1e-1) + assert np.isclose(np.std(sensitive_energy), 16.09166011697945, rtol=1e-1) + + +def test_scoring_helpers(): + with Application(gdml=complexGdml, seed=1234) as app: + app.set_event_fields( + {"id", "track_id", "step_energy", "step_volume", "step_position"} + ) + app.command("/gun/particle gamma") + app.command("/gun/energy 10 keV") + app.command("/gun/position 0 0 20 cm") + app.command("/gun/direction 0 0 -1") + events = app.run(10) + + assert len(Scoring.energy_deposit(events)) == 10 + mesh, edges = Scoring.energy_mesh(events, bins=(4, 5, 6)) + assert mesh.shape == (4, 5, 6) + assert len(edges) == 3 diff --git a/tests/test_application.py b/tests/test_application.py index 8a734633..5a8e20c8 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -37,6 +37,20 @@ def test_run(): assert len(events) == 100 +def test_reference_physics_list(): + assert "custom" in Application.available_physics_lists() + assert "FTFP_BERT" in Application.available_physics_lists() + with Application(gdml=basic_gdml, physics="FTFP_BERT") as app: + assert len(app.run(1)) == 1 + + +def test_optical_physics_and_magnetic_field(): + with Application(gdml=basic_gdml, optical=True) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.5) + assert app.detector.magnetic_field == (0.0, 0.0, 1.5) + assert len(app.run(1)) == 1 + + def test_seed_single_thread(): with Application(gdml=basic_gdml, seed=1100) as app: app.command("/gun/particle e-") diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py new file mode 100644 index 00000000..739b8b42 --- /dev/null +++ b/tests/test_capabilities.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import awkward as ak +import numpy as np + +import geant4_python_application as g4 +from geant4_python_application import cad +from geant4_python_application import distributed as dist +from geant4_python_application import io as gio +from geant4_python_application import optical as opt + + +def test_electric_field(): + with g4.Application(gdml=g4.basic_gdml, seed=101) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.0) + app.detector.electric_field = (0.0, 0.0, 0.5) + assert tuple(app.detector.magnetic_field) == (0.0, 0.0, 1.0) + assert tuple(app.detector.electric_field) == (0.0, 0.0, 0.5) + app.command("/gun/particle e-") + app.command("/gun/energy 5 MeV") + assert len(app.run(2)) == 2 + + +def test_optical_scintillation(): + with g4.Application(gdml=opt.optical_water_gdml, optical=True, seed=102) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 10 MeV") + app.command("/gun/position 0 0 0 mm") + app.command("/gun/direction 1 0 0") + events = app.run(2) + assert len(events) == 2 + parts = set(ak.to_list(ak.flatten(events.track.particle, axis=None))) + assert "opticalphoton" in parts + assert len(opt.scintillation_commands()) == 2 + + +def test_lxe_and_wls_optical_materials(): + for gdml, particle, energy in ( + (opt.lxe_scintillator_gdml, "e-", "1 MeV"), + (opt.wls_fiber_gdml, "opticalphoton", "3.5 eV"), + ): + with g4.Application(gdml=gdml, optical=True, seed=110) as app: + app.command(f"/gun/particle {particle}") + app.command(f"/gun/energy {energy}") + app.command("/gun/position 0 0 0 mm") + app.command("/gun/direction 0 0 1") + events = app.run(1) + particles = ak.flatten(events.track.particle, axis=None) + assert bool(ak.any(particles == "opticalphoton")) + + +def test_expanded_parameterised_geometry(): + gdml = g4.geometry.linear_array_gdml(5, spacing_mm=15, box_size_mm=5) + assert gdml.count(' 0 and counts["step"] > 0 + + +def test_extra_physics_and_dna(): + assert "G4EmExtraPhysics" in g4.Application.available_extra_physics() + assert any("DNA" in x for x in g4.Application.available_extra_physics()) + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=104) as app: + app.add_physics("G4EmExtraPhysics") + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + assert len(app.run(1)) == 1 + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=105) as app: + app.add_physics("G4EmDNAPhysics_option2") + app.command("/gun/particle e-") + app.command("/gun/energy 100 keV") + assert len(app.run(1)) == 1 + + +def test_cad_vtk_parquet_hepmc(tmp_path): + v, f = cad.cube_mesh(100.0) + gdml = cad.mesh_to_gdml(v, f) + assert "tessellated" in gdml + with g4.Application(gdml=gdml, seed=106) as app: + assert len(app.run(1)) == 1 + with g4.Application(gdml=g4.basic_gdml, seed=107) as app: + app.command("/gun/particle e-") + app.command("/gun/energy 5 MeV") + events = app.run(2) + mesh, edges = g4.Scoring.energy_mesh(events, bins=(3, 3, 3)) + vtk_path = gio.mesh_to_vtk(mesh, edges, tmp_path / "mesh.vtk") + assert "STRUCTURED_POINTS" in vtk_path.read_text()[:500] + pq = gio.events_to_parquet(events, tmp_path / "events.parquet") + assert len(gio.events_from_parquet(pq)) == len(events) + hepmc = "E 0 1 1\nP 1 22 0 0 1 1 0 1\nP 2 11 0.1 0 0.5 0.6 0.0005 1\n" + prim = gio.hepmc_to_primaries(hepmc) + assert len(prim) == 2 + + +def test_root_io(tmp_path): + uproot = __import__("importlib").util.find_spec("uproot") + if uproot is None: + return + with g4.Application(gdml=g4.basic_gdml, seed=108) as app: + app.command("/gun/particle gamma") + app.command("/gun/energy 1 MeV") + events = app.run(2) + rp = gio.events_to_root(events, tmp_path / "events.root") + assert rp.exists() + assert len(gio.events_from_root(rp)["id"]) == len(events) + + +def test_distributed_split(): + assert dist.get_rank_size() == (0, 1) + assert dist.split_count(10, 0, 2) == (0, 5) + assert dist.split_count(10, 1, 2) == (5, 5) + s0, c0 = dist.split_count(3, 0, 2) + _, c1 = dist.split_count(3, 1, 2) + assert c0 + c1 == 3 + assert s0 == 0 + + +def test_native_scoring_mesh(tmp_path): + with g4.Application(gdml=g4.basic_gdml, seed=109) as app: + mesh = g4.NativeScoringMesh(app, "testMesh", "edep") + mesh.box((250, 250, 250), (2, 2, 2)) + app.command("/gun/particle e-") + app.command("/gun/energy 1 MeV") + app.run(1) + output = mesh.dump(tmp_path / "mesh.csv") + assert output.exists() + assert output.stat().st_size > 0 diff --git a/tests/test_module.py b/tests/test_module.py index 3e9e2817..e0a88d1f 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -15,3 +15,4 @@ def test_imports(): assert geant4_python_application assert geant4_python_application.Application assert geant4_python_application.Detector + assert geant4_python_application.Generator diff --git a/tests/test_run.py b/tests/test_run.py index d92169fb..a501f6f3 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -36,3 +36,29 @@ def test_awkward_primaries(n_threads): event_primary_energy = ak.flatten(events.primaries.energy) assert np.allclose(event_primary_energy, primaries.energy) + + +def test_general_particle_source(): + with Application(gdml=basic_gdml) as app: + app.generator.use_gps().particle("geantino").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + events = app.run(2) + assert len(events) == 2 + assert np.allclose(ak.flatten(events.primaries.energy), 2000) + + +def test_multiple_primary_vertices_per_event(): + particle = lambda name, x: { + "particle": name, + "energy": 1000.0, + "direction": {"x": 0.0, "y": 0.0, "z": 1.0}, + "position": {"x": x, "y": 0.0, "z": 0.0}, + } + source = ak.Array([ + {"primaries": [particle("gamma", -1.0), particle("e-", 1.0)]}, + {"primaries": [particle("gamma", 0.0)]}, + ]) + with Application(gdml=basic_gdml) as app: + events = app.run(source) + assert ak.to_list(ak.num(events.primaries)) == [2, 1] + assert ak.to_list(events.primaries.particle) == [["gamma", "e-"], ["gamma"]] diff --git a/tools/audit_examples.py b/tools/audit_examples.py new file mode 100755 index 00000000..6e5ce1ac --- /dev/null +++ b/tools/audit_examples.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Inventory official Geant4 examples and execute every claimed Python port.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import tempfile +import time +import xml.etree.ElementTree as ET +from pathlib import Path + + +def official_inventory(root: Path) -> dict: + targets = sorted(p.parent.relative_to(root).as_posix() for p in root.rglob("CMakeLists.txt")) + gdml = sorted(root.rglob("*.gdml")) + standalone: list[str] = [] + fragments: list[str] = [] + invalid: list[str] = [] + for path in gdml: + relative = path.relative_to(root).as_posix() + try: + tree = ET.parse(path) + document = tree.getroot() + except (ET.ParseError, OSError): + invalid.append(relative) + continue + setup = document.find("setup") + if setup is not None and setup.find("world") is not None: + standalone.append(relative) + else: + fragments.append(relative) + return { + "root": str(root.resolve()), + "cmake_targets": targets, + "gdml": { + "total": len(gdml), + "standalone_candidates": standalone, + "fragments": fragments, + "invalid_or_external_entity": invalid, + }, + } + + +def python_ports(root: Path, events: int, timeout: int) -> list[dict]: + results: list[dict] = [] + for path in sorted(root.rglob("*.py")): + if path.name == "run_gdml.py": + results.append({"path": path.relative_to(root).as_posix(), "status": "utility"}) + continue + text = path.read_text(errors="replace") + if "--batch" not in text: + results.append( + { + "path": path.relative_to(root).as_posix(), + "status": "not-batch-testable", + } + ) + continue + command = [sys.executable, str(path.resolve()), "--batch", "-n", str(events)] + started = time.monotonic() + with tempfile.TemporaryDirectory(prefix="g4-port-audit-") as workdir: + try: + completed = subprocess.run( + command, + cwd=workdir, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + status = "passed" if completed.returncode == 0 else "failed" + result = { + "path": path.relative_to(root).as_posix(), + "status": status, + "returncode": completed.returncode, + "seconds": round(time.monotonic() - started, 3), + } + if completed.returncode: + result["stderr_tail"] = completed.stderr[-2000:] + results.append(result) + except subprocess.TimeoutExpired as error: + results.append( + { + "path": path.relative_to(root).as_posix(), + "status": "timeout", + "seconds": round(time.monotonic() - started, 3), + "stderr_tail": (error.stderr or "")[-2000:], + } + ) + return results + + +def markdown(report: dict) -> str: + inventory = report["official"] + results = report["python_ports"] + passed = sum(item["status"] == "passed" for item in results) + failed = sum(item["status"] in {"failed", "timeout"} for item in results) + lines = [ + "# Geant4 example compatibility audit", + "", + f"- Official CMake targets: {len(inventory['cmake_targets'])}", + f"- Official GDML files: {inventory['gdml']['total']}", + f"- Standalone GDML candidates: {len(inventory['gdml']['standalone_candidates'])}", + f"- Python ports passing: {passed}", + f"- Python ports failing/timing out: {failed}", + "", + "| Python port | Status | Seconds |", + "|---|---:|---:|", + ] + for item in results: + lines.append(f"| `{item['path']}` | {item['status']} | {item.get('seconds', '')} |") + lines.append("") + lines.append("Only entries marked `passed` are validated as runnable Python ports.") + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("official_root", type=Path) + parser.add_argument("python_root", type=Path) + parser.add_argument("--events", type=int, default=1) + parser.add_argument("--timeout", type=int, default=120) + parser.add_argument("--json", type=Path) + parser.add_argument("--markdown", type=Path) + args = parser.parse_args() + + report = { + "official": official_inventory(args.official_root), + "python_ports": python_ports(args.python_root, args.events, args.timeout), + } + if args.json: + args.json.parent.mkdir(parents=True, exist_ok=True) + args.json.write_text(json.dumps(report, indent=2) + "\n") + rendered = markdown(report) + if args.markdown: + args.markdown.parent.mkdir(parents=True, exist_ok=True) + args.markdown.write_text(rendered) + print(rendered, end="") + failed = any(item["status"] in {"failed", "timeout"} for item in report["python_ports"]) + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 940337034155a59b5625a57793b7cdd72a39ad11 Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sat, 10 Oct 2026 16:01:37 +0530 Subject: [PATCH 3/8] Add a Qt viewer that loads only when visualize() is called Application.visualize() opens Geant4's interactive Qt viewer. It lives in a separate _geant4_vis module that the Geant4 child process imports only on demand, so importing the package and running simulations never loads Qt, and headless machines, containers and clusters need neither Qt nor OpenGL. The physics module links only the Geant4 libraries that never reach Qt; both modules share one Geant4 through its shared libraries, so the viewer build requires shared Geant4. Geant4's Qt3D driver is left out because the pip-installable Qt has no Qt3D. Qt plugin paths are set only inside the child process, so they cannot interfere with other Qt applications. CMake changes beyond the viewer: - Request Geant4's "static" component as optional, so builds use static libraries when available and shared ones otherwise. This replaces the Release-only switch, which passed "gdml static" as a single component name. - Go back to FetchContent_Populate with add_subdirectory(EXCLUDE_FROM_ALL): with CMake 3.25 as the minimum, FetchContent_MakeAvailable cannot exclude the dependencies' targets and install rules (that needs 3.28). CMP0169 is set only when CMake knows it (3.30+). --- CMakeLists.txt | 119 ++++++++++--- examples/qt_viewer.py | 20 +++ src/geant4_python_application/application.py | 173 ++++++++++++++++++- src/vis/module.cpp | 52 ++++++ 4 files changed, 334 insertions(+), 30 deletions(-) create mode 100644 examples/qt_viewer.py create mode 100644 src/vis/module.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5410ab8a..7e8c97c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,12 @@ cmake_minimum_required(VERSION 3.25...3.30) +# CMP0169 only exists from CMake 3.30; older versions already behave as OLD. +if (POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) +endif () set(CMAKE_CXX_STANDARD 17) - set(AWKWARD_VERSION "v2.6.9") set(PYBIND11_VERSION "v2.13.6") -set(Geant4_VERSION "11.2.2") if (NOT DEFINED SKBUILD_PROJECT_NAME) # IDE @@ -14,6 +16,9 @@ endif () project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) set(PYTHON_MODULE_NAME _geant4_application) +set(VIS_MODULE_NAME _geant4_vis) +option(GEANT4_PYTHON_APPLICATION_VISUALIZATION + "Build the interactive Geant4 Qt viewer as a separate, lazily imported module" OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -23,18 +28,26 @@ find_package( COMPONENTS Interpreter Development.Module REQUIRED) -# define geant4 components in a conditional way, "gdml static" if release build, -# "gdml" otherwise -set(GEANT4_COMPONENTS "gdml") -if (CMAKE_BUILD_TYPE STREQUAL "Release") - set(GEANT4_COMPONENTS "gdml static") +if (GEANT4_PYTHON_APPLICATION_VISUALIZATION) + # The viewer module and the physics module must share one Geant4 (its + # managers are singletons), which only works with shared libraries. + find_package( + Geant4 11.2.2 CONFIG REQUIRED + COMPONENTS gdml ui_all vis_all) + if (NOT Geant4_shared_FOUND) + message(FATAL_ERROR + "GEANT4_PYTHON_APPLICATION_VISUALIZATION requires Geant4 built with shared libraries") + endif () +else () + # "static" is optional: Geant4Config fails if it is required but only shared + # libraries are installed, while as an optional component it still selects + # the static libraries whenever they exist. + find_package( + Geant4 11.2.2 CONFIG REQUIRED + COMPONENTS gdml + OPTIONAL_COMPONENTS static) endif () -find_package( - Geant4 ${Geant4_VERSION} CONFIG - COMPONENTS ${GEANT4_COMPONENTS} - REQUIRED) - message(STATUS "Using Geant4 ${Geant4_VERSION} from ${Geant4_DIR}") message(STATUS "Using Python ${Python_VERSION} from ${Python_EXECUTABLE}") @@ -46,7 +59,9 @@ FetchContent_Declare( ) FetchContent_GetProperties(awkward-headers) if (NOT awkward-headers_POPULATED) - FetchContent_MakeAvailable(awkward-headers) + FetchContent_Populate(awkward-headers) + add_subdirectory(${awkward-headers_SOURCE_DIR} + ${awkward-headers_BINARY_DIR} EXCLUDE_FROM_ALL) endif () FetchContent_Declare( @@ -56,15 +71,30 @@ FetchContent_Declare( DOWNLOAD_EXTRACT_TIMESTAMP 1) FetchContent_GetProperties(pybind11) if (NOT pybind11_POPULATED) - FetchContent_MakeAvailable(pybind11) + FetchContent_Populate(pybind11) + add_subdirectory(${pybind11_SOURCE_DIR} ${pybind11_BINARY_DIR} + EXCLUDE_FROM_ALL) endif () +# Geant4 libraries that link Qt directly, or reach it through each other (the +# visualization drivers, UI sessions and vis manager). The physics module must +# not link any of them, so that importing the package never loads Qt. +set(GEANT4_QT_LIBRARIES + G4OpenGL G4ToolsSG G4interfaces G4visQt3D G4vis_management G4FR G4GMocren + G4RayTracer G4Tree G4VRML G4visHepRep G4OpenInventor G4visVtk) +set(GEANT4_PHYSICS_LIBRARIES ${Geant4_LIBRARIES}) +foreach (lib IN LISTS GEANT4_QT_LIBRARIES) + list(REMOVE_ITEM GEANT4_PHYSICS_LIBRARIES Geant4::${lib} Geant4::${lib}-static) +endforeach () + file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/geant4_application/src/*.cpp") file(GLOB_RECURSE PYTHON_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/python/*.cpp") +get_filename_component(GEANT4_PREFIX "${Geant4_DIR}/../../.." ABSOLUTE) + pybind11_add_module(${PYTHON_MODULE_NAME} MODULE ${SOURCES} ${PYTHON_SOURCES}) target_compile_definitions( @@ -81,23 +111,60 @@ target_include_directories( ${Geant4_INCLUDE_DIRS}) target_link_libraries( - ${PYTHON_MODULE_NAME} PRIVATE ${Geant4_LIBRARIES} awkward::layout-builder - pybind11::headers) + ${PYTHON_MODULE_NAME} PRIVATE ${GEANT4_PHYSICS_LIBRARIES} + awkward::layout-builder pybind11::headers) + +set(EXTENSION_MODULES ${PYTHON_MODULE_NAME}) + +if (GEANT4_PYTHON_APPLICATION_VISUALIZATION) + pybind11_add_module(${VIS_MODULE_NAME} MODULE + "${PROJECT_SOURCE_DIR}/src/vis/module.cpp") + # Leave out Geant4's Qt3D driver: the PyQt5-Qt5 wheel that provides Qt to + # published wheels ships no Qt3D modules. Not linking Geant4::G4visQt3D also + # drops its G4VIS_USE_QT3D define, so G4VisExecutive never references it. + # The OpenGL Qt viewer (/vis/open OGL) is unaffected. + set(GEANT4_VIS_LIBRARIES ${Geant4_LIBRARIES}) + list(REMOVE_ITEM GEANT4_VIS_LIBRARIES Geant4::G4visQt3D) + target_include_directories(${VIS_MODULE_NAME} PRIVATE ${Geant4_INCLUDE_DIRS}) + target_link_libraries(${VIS_MODULE_NAME} PRIVATE ${GEANT4_VIS_LIBRARIES} + pybind11::headers) + # Homebrew may expose Qt 6 from its global include directory while this + # Geant4 installation was built with Qt 5. Put Qt 5's forwarding headers + # first so framework headers do not accidentally include their Qt 6 peers. + if (APPLE AND DEFINED Qt5Core_DIR) + get_filename_component(QT5_PREFIX "${Qt5Core_DIR}/../../.." ABSOLUTE) + target_include_directories(${VIS_MODULE_NAME} SYSTEM BEFORE PRIVATE + "${QT5_PREFIX}/include") + endif () + list(APPEND EXTENSION_MODULES ${VIS_MODULE_NAME}) +endif () -pybind11_extension(${PYTHON_MODULE_NAME}) -if (NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo) - # Strip unnecessary sections of the binary on Linux/macOS - pybind11_strip(${PYTHON_MODULE_NAME}) +# Wheel repair tools (delocate/auditwheel) replace these with the bundled +# library locations; source installs keep finding Geant4 where it was built. +# Qt frameworks linked by @rpath (as when building against PyQt5-Qt5's Qt) +# also need their directory listed for delocate to resolve them. +set(EXTENSION_RPATH "${GEANT4_PREFIX}/lib") +if (APPLE AND DEFINED QT5_PREFIX) + list(APPEND EXTENSION_RPATH "${QT5_PREFIX}/lib") endif () -set_target_properties( - ${PYTHON_MODULE_NAME} - PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${PROJECT_NAME} - CXX_VISIBILITY_PRESET "hidden" - CUDA_VISIBILITY_PRESET "hidden") +foreach (module IN LISTS EXTENSION_MODULES) + pybind11_extension(${module}) + if (NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo) + # Strip unnecessary sections of the binary on Linux/macOS + pybind11_strip(${module}) + endif () + set_target_properties( + ${module} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${PROJECT_NAME} + CXX_VISIBILITY_PRESET "hidden" + CUDA_VISIBILITY_PRESET "hidden" + BUILD_RPATH "${EXTENSION_RPATH}" + INSTALL_RPATH "${EXTENSION_RPATH}") +endforeach () install( - TARGETS ${PYTHON_MODULE_NAME} + TARGETS ${EXTENSION_MODULES} LIBRARY DESTINATION ${PROJECT_NAME} RUNTIME DESTINATION ${PROJECT_NAME} ARCHIVE DESTINATION ${PROJECT_NAME}) diff --git a/examples/qt_viewer.py b/examples/qt_viewer.py new file mode 100644 index 00000000..69cb6b59 --- /dev/null +++ b/examples/qt_viewer.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import geant4_python_application as g4 + + +def main(): + with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + app.commands( + [ + "/gun/particle e-", + "/gun/energy 100 MeV", + "/gun/direction 0 0 -1", + "/gun/position 0 0 40 cm", + ] + ) + app.visualize() + + +if __name__ == "__main__": + main() diff --git a/src/geant4_python_application/application.py b/src/geant4_python_application/application.py index c9f6c3b1..c02f4575 100644 --- a/src/geant4_python_application/application.py +++ b/src/geant4_python_application/application.py @@ -1,8 +1,11 @@ from __future__ import annotations +import importlib.util import multiprocessing +import os import threading from collections import namedtuple +from pathlib import Path import awkward as ak @@ -33,6 +36,45 @@ } +_package_dir = Path(__file__).parent +# Written into wheels whose Qt-linked libraries were repointed at the Qt shipped +# in the PyQt5-Qt5 package (see tools/relink_qt_wheel.py). +_PIP_QT_MARKER = _package_dir / "_qt_from_pyqt5_qt5" +_PIP_QT_DIR = _package_dir.parent / "PyQt5" / "Qt5" + + +def _visualization_problem() -> str | None: + """Why the Qt viewer can't be opened, or None if it can. Never loads Qt.""" + if importlib.util.find_spec("geant4_python_application._geant4_vis") is None: + return ( + "This installation was built without the Qt viewer. Reinstall from " + "source with -Ccmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON " + "using a Geant4 built with Qt and OpenGL." + ) + if _PIP_QT_MARKER.exists() and not (_PIP_QT_DIR / "lib").is_dir(): + return ( + "The Qt viewer needs Qt from the 'gui' extra: " + "pip install 'geant4_python_application[gui]'" + ) + return None + + +def _visualize(app, commands: list[str]) -> None: + """Runs in the Geant4 child process, which then owns the Qt event loop.""" + problem = _visualization_problem() + if problem is not None: + raise RuntimeError(problem) + # A platform plugin only loads into the exact Qt build it was compiled for, + # so point Qt at the plugins shipped alongside the Qt libraries in use. + if _PIP_QT_MARKER.exists(): + os.environ["QT_PLUGIN_PATH"] = str(_PIP_QT_DIR / "plugins") + from geant4_python_application import _geant4_vis + + if not app.is_initialized(): + app.initialize() + _geant4_vis.start_visualization(commands) + + def _start_application(pipe: multiprocessing.Pipe): app = Geant4Application() app.set_event_fields(_default_event_fields) @@ -44,6 +86,12 @@ def _start_application(pipe: multiprocessing.Pipe): break counter, message = message_with_counter + if message.target == "" and message.method == "visualize": + # Handled here so the Qt viewer module is only ever imported in + # this process, and only when a viewer is actually requested. + _visualize(app, *message.args, **message.kwargs) + pipe.send((counter, None)) + continue target = app target_list = message.target.split(".") target_list = [element for element in target_list if element] @@ -70,7 +118,12 @@ def _start_application(pipe: multiprocessing.Pipe): class Application: def __init__( - self, n_threads: int = 0, gdml: str = None, physics=None, seed: int = 0 + self, + n_threads: int = 0, + gdml: str = None, + physics: str = "custom", + optical: bool = False, + seed: int = 0, ): geant4_python_application.install_datasets(show_progress=True) @@ -83,8 +136,11 @@ def __init__( self._seed = seed self._detector = geant4_python_application.Detector(self) + self._generator = geant4_python_application.Generator(self) self._n_threads = n_threads self._gdml = gdml + self._physics = physics + self._optical = optical def start(self, setup: bool = True, initialize: bool = False) -> Application: if self._process.is_alive(): @@ -94,7 +150,7 @@ def start(self, setup: bool = True, initialize: bool = False) -> Application: if setup: self.seed = self._seed self.setup_manager(self._n_threads) - self.setup_physics() + self.setup_physics(self._physics, optical=self._optical) if self._gdml is not None: self.setup_detector(self._gdml) @@ -149,8 +205,30 @@ def setup_manager(self, n_threads: int = 0) -> Application: self._send_and_recv(Message("", "setup_manager", (n_threads,), {})) return self - def setup_physics(self) -> Application: - self._send_and_recv(Message("", "setup_physics", (), {})) + def setup_physics( + self, physics_list: str = "custom", *, optical: bool = False + ) -> Application: + self._send_and_recv( + Message("", "setup_physics", (physics_list, optical), {}) + ) + return self + + @staticmethod + def available_physics_lists() -> list[str]: + return list(Geant4Application.available_physics_lists()) + + @staticmethod + def available_extra_physics() -> list[str]: + """Extra G4VPhysicsConstructor names usable with add_physics (incl. DNA).""" + return list(Geant4Application.available_extra_physics()) + + def add_physics(self, constructor_name: str) -> Application: + """Register an extra physics constructor (e.g. DNA, optical, EM extra). + + Must be called after setup_physics and before initialize/run. + Example: app.add_physics("G4EmDNAPhysics_option2") + """ + self._send_and_recv(Message("", "add_physics", (constructor_name,), {})) return self def setup_detector(self, gdml: str) -> Application: @@ -253,6 +331,62 @@ def run(self, primaries: int | ak.Array): events = events[ak.argsort(events.id)] return events + def run_with_callbacks( + self, + primaries: int | ak.Array, + *, + on_run=None, + on_event=None, + on_track=None, + on_step=None, + ): + """Run then invoke Python callbacks over run/event/track/step records. + + Offline (post-run) layer over the awkward event model: + on_run(events), on_event(event) per event, on_track(track, event) + per track, on_step(step, track, event) per step. Returns events. + + Note: events.track is a struct-of-lists per event (see DataModel), + so tracks/steps are reconstructed via ak.zip per event/track. + """ + events = self.run(primaries) + if on_run is not None: + on_run(events) + if on_event is None and on_track is None and on_step is None: + return events + for ei in range(len(events)): + event = events[ei] + if on_event is not None: + on_event(event) + if on_track is None and on_step is None: + continue + if "track" not in event.fields: + continue + track_struct = event.track + if "id" not in track_struct.fields: + continue + n_tracks = len(track_struct.id) + track_only_fields = [f for f in track_struct.fields if f != "step"] + has_steps = "step" in track_struct.fields + for ti in range(n_tracks): + # Lightweight dict view (scalars/vectors); steps as awkward array. + track_view = {f: track_struct[f][ti] for f in track_only_fields} + steps_view = None + if has_steps: + step_struct = track_struct.step + steps_view = ak.zip( + {f: step_struct[f][ti] for f in step_struct.fields} + ) + track_view = dict(track_view) + track_view["step"] = steps_view + if on_track is not None: + on_track(track_view, event) + if on_step is None or steps_view is None: + continue + for step in steps_view: + on_step(step, track_view, event) + return events + @property def seed(self): return self._send_and_recv(Message("", "get_seed", (), {})) @@ -273,6 +407,37 @@ def commands(self, commands: list[str]) -> Application: def list_commands(self, directory="/") -> str: return self._send_and_recv(Message("", "list_commands", (directory,), {})) + @staticmethod + def visualization_available() -> bool: + """Whether the Qt viewer can be opened. Does not load Qt.""" + return _visualization_problem() is None + + @staticmethod + def multithreading_available() -> bool: + """Whether the linked Geant4 runtime supports multithreaded run managers.""" + return Geant4Application.multithreading_available() + + def visualize(self, commands: list[str] | None = None) -> Application: + """Open the interactive Geant4 Qt viewer. + + The call returns after the viewer is closed. Geant4 commands can be + entered in the Qt session's command panel while it is open. + """ + if commands is None: + commands = [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/autoRefresh true", + "/vis/scene/add/trajectories smooth", + "/vis/scene/endOfEventAction accumulate", + ] + self._send_and_recv(Message("", "visualize", (commands,), {})) + return self + @property def detector(self) -> geant4_python_application.Detector: return self._detector + + @property + def generator(self) -> geant4_python_application.Generator: + return self._generator diff --git a/src/vis/module.cpp b/src/vis/module.cpp new file mode 100644 index 00000000..11c01f56 --- /dev/null +++ b/src/vis/module.cpp @@ -0,0 +1,52 @@ +// The interactive Qt viewer, built as its own extension module so that only +// calling Application.visualize() loads Qt; importing the package never does. +// It drives the Geant4 instance set up by _geant4_application through Geant4's +// singleton managers, which both modules share via the shared Geant4 libraries. + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace py = pybind11; + +namespace { + +void StartVisualization(const std::vector& commands) { + if (G4RunManager::GetRunManager() == nullptr) { + throw std::runtime_error("Geant4 run manager is not set up; start the Application first"); + } + + int argc = 1; + char applicationName[] = "geant4-python-application"; + char* argv[] = {applicationName, nullptr}; + + auto visualization = std::make_unique(); + visualization->Initialize(); + + auto ui = std::make_unique(argc, argv, "qt"); + auto* uiManager = G4UImanager::GetUIpointer(); + for (const auto& command: commands) { + const int code = uiManager->ApplyCommand(command); + if (code != 0) { + throw std::runtime_error("Command '" + command + "' failed with code " + std::to_string(code)); + } + } + ui->SessionStart(); +} + +}// namespace + +PYBIND11_MODULE(_geant4_vis, m) { + m.doc() = "Interactive Geant4 Qt viewer for geant4_python_application"; + m.def("start_visualization", &StartVisualization, py::arg("commands"), + "Open the Qt viewer and block until its window is closed."); +} From bb716cfd704e7d202ef04e146f26bc244cd8c67d Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sat, 10 Oct 2026 16:02:36 +0530 Subject: [PATCH 4/8] Build macOS and Linux wheels with a pip-installable Qt viewer Wheels bundle Geant4, so installing them needs no separate Geant4 (C++) installation. One job per platform (macOS arm64, macOS x86_64, Linux x86_64) builds wheels for CPython 3.8-3.13; Windows wheels are dropped. Geant4 11.2.2 is built as shared libraries with Qt and bundled into each wheel, but Qt itself is not: the new 'gui' extra installs PyQt5-Qt5, and tools/relink_qt_wheel.py points the bundled Qt-linked libraries at it after delocate/auditwheel. On macOS the build links directly against PyQt5-Qt5's frameworks, with headers and tools from the official Qt 5.15.2 archive, so the build no longer depends on Homebrew's deprecated qt@5. Build fixes needed on the current runners: - macOS: find Qt by prefix (Geant4 looks it up as find_package(QT)), use the system zlib (the bundled one breaks the macOS 15.5 SDK), and patch a typo in Geant4 11.2.2's g4tools that newer Xcode clang rejects - Linux: build Geant4 with the global-dynamic TLS model, otherwise importing the extension fails with "cannot allocate memory in static TLS block" - relink tool: drop leftover absolute build rpaths, and unpack wheels without verifying RECORD, which delocate leaves stale after retagging WHEEL Each wheel runs the test suite plus tools/smoke_viewer.py, which opens the viewer (under Xvfb on Linux) and checks it stays up. Qt, Xerces and Geant4 builds are cached as soon as they finish. --- .github/workflows/wheels.yaml | 287 ++++++++++++++++------------ pyproject.toml | 5 + tools/build_macos_self_contained.sh | 35 ++++ tools/relink_qt_wheel.py | 122 ++++++++++++ tools/smoke_viewer.py | 53 +++++ 5 files changed, 381 insertions(+), 121 deletions(-) create mode 100755 tools/build_macos_self_contained.sh create mode 100644 tools/relink_qt_wheel.py create mode 100644 tools/smoke_viewer.py diff --git a/.github/workflows/wheels.yaml b/.github/workflows/wheels.yaml index a611ad68..03e4f609 100644 --- a/.github/workflows/wheels.yaml +++ b/.github/workflows/wheels.yaml @@ -14,7 +14,11 @@ env: GEANT4_VERSION: 11.2.2 XERCES_VERSION: 3.2.5 CPP_STANDARD: 17 - Geant4_DIR: ${{ github.workspace }}/geant4 + # Qt the viewer is built against. Headers and tools come from the official + # Qt 5.15.2 archive; at runtime the [gui] extra supplies Qt from PyQt5-Qt5. + QT_VERSION: 5.15.2 + PYQT5_QT5_VERSION: 5.15.19 + MACOSX_DEPLOYMENT_TARGET: "11.0" concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -27,6 +31,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 @@ -45,159 +51,170 @@ jobs: path: dist/*.tar.gz wheels: - name: - Build wheel for cp${{ matrix.python }} ${{ matrix.platform_id }} ${{ - matrix.manylinux_image }} + name: Wheels for ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - - os: ubuntu-latest - python: 38 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - - os: ubuntu-latest - python: 39 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - - os: ubuntu-latest - python: 310 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - - os: ubuntu-latest - python: 311 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - - os: ubuntu-latest - python: 312 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - - - os: macos-latest - python: 38 - platform_id: macosx_x86_64 - - os: macos-latest - python: 39 - platform_id: macosx_x86_64 - - os: macos-latest - python: 310 - platform_id: macosx_x86_64 - - os: macos-latest - python: 311 - platform_id: macosx_x86_64 - - os: macos-latest - python: 312 - platform_id: macosx_x86_64 - - - os: macos-latest - python: 38 - platform_id: macosx_arm64 - - os: macos-latest - python: 39 - platform_id: macosx_arm64 - - os: macos-latest - python: 310 - platform_id: macosx_arm64 - - os: macos-latest - python: 311 - platform_id: macosx_arm64 - - os: macos-latest - python: 312 - platform_id: macosx_arm64 - - - os: windows-latest - python: 38 - platform_id: win_amd64 - - os: windows-latest - python: 39 - platform_id: win_amd64 - - os: windows-latest - python: 310 - platform_id: win_amd64 - - os: windows-latest - python: 311 - platform_id: win_amd64 - - os: windows-latest - python: 312 - platform_id: win_amd64 + - name: Linux x86_64 + os: ubuntu-latest + arch: x86_64 + - name: macOS arm64 + os: macos-15 + arch: arm64 + pyqt5_qt5_platform: macosx_11_0_arm64 + - name: macOS x86_64 + os: macos-15-intel + arch: x86_64 + pyqt5_qt5_platform: macosx_10_13_x86_64 + env: + Geant4_DIR: ${{ github.workspace }}/geant4 + QT_PREFIX: ${{ github.workspace }}/qt/5.15.2/clang_64 steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 - name: Setup CMake + if: runner.os == 'macOS' uses: lukka/get-cmake@v3.30.3 - - name: Setup Windows environment variables - if: matrix.os == 'windows-latest' - shell: bash - run: | - echo "CMAKE_ARGS=-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded" >> $GITHUB_ENV + - name: Install Rosetta (runs the x86_64 Qt 5.15.2 build tools) + if: runner.os == 'macOS' && matrix.arch == 'arm64' + run: sudo softwareupdate --install-rosetta --agree-to-license || true - - name: Setup MacOS environment variables - if: matrix.os == 'macos-latest' + - name: Cache Qt + if: runner.os == 'macOS' + id: cache-qt + uses: actions/cache/restore@v4 + with: + path: ${{ github.workspace }}/qt + key: + qt-${{ env.QT_VERSION }}-pyqt5-qt5-${{ env.PYQT5_QT5_VERSION }}-${{ + matrix.pyqt5_qt5_platform }} + + - name: Install Qt (official headers and tools, PyQt5-Qt5 libraries) + if: runner.os == 'macOS' && steps.cache-qt.outputs.cache-hit != 'true' run: | - echo "CMAKE_ARGS=-DCMAKE_OSX_ARCHITECTURES=${{ matrix.platform_id == 'macosx_x86_64' && 'x86_64' || 'arm64' }}" >> $GITHUB_ENV + python -m pip install aqtinstall + aqt install-qt mac desktop ${{ env.QT_VERSION }} clang_64 --archives qtbase -O "${{ github.workspace }}/qt" + # Recent macOS SDKs dropped the AGL framework that Qt 5.15.2's CMake config still links. + grep -rl "AGL" "$QT_PREFIX/lib/cmake" | xargs sed -i '' -e 's#;[^;"]*/AGL\.framework##g' -e 's#"[^;"]*/AGL\.framework;#"#g' + # Link against the exact Qt binaries the [gui] extra installs at runtime; + # they carry @rpath install names, so wheels need no absolute Qt paths. + python -m pip download --no-deps --only-binary=:all: --platform ${{ matrix.pyqt5_qt5_platform }} "PyQt5-Qt5==${{ env.PYQT5_QT5_VERSION }}" -d pyqt5-qt5 + unzip -q pyqt5-qt5/*.whl 'PyQt5/Qt5/lib/*' -d pyqt5-qt5 + for framework in "$QT_PREFIX"/lib/*.framework; do + name=$(basename "$framework" .framework) + binary="pyqt5-qt5/PyQt5/Qt5/lib/$name.framework/Versions/5/$name" + if [ -f "$binary" ]; then cp "$binary" "$framework/Versions/5/$name"; fi + done + rm -rf pyqt5-qt5 + + # Saved right away (not at job end) so a later failure doesn't discard the work. + - name: Save Qt + if: runner.os == 'macOS' && steps.cache-qt.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/qt + key: ${{ steps.cache-qt.outputs.cache-primary-key }} - name: Cache Xerces-C Installation - if: matrix.os != 'ubuntu-latest' + if: runner.os == 'macOS' id: cache-xerces - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: ${{ github.workspace }}/xerces key: xerces-v${{ env.XERCES_VERSION }}-cpp${{ env.CPP_STANDARD }}-${{ - matrix.platform_id }} + matrix.arch }}-macos${{ env.MACOSX_DEPLOYMENT_TARGET }} - name: Build and Install Xerces-C - if: - steps.cache-xerces.outputs.cache-hit != 'true' && matrix.os != - 'ubuntu-latest' + if: runner.os == 'macOS' && steps.cache-xerces.outputs.cache-hit != 'true' run: | git clone https://github.com/apache/xerces-c.git xerces-source git -C xerces-source checkout tags/v${{ env.XERCES_VERSION }} - cmake -B xerces-build -S xerces-source ${{ env.CMAKE_ARGS }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/xerces -DCMAKE_CXX_STANDARD=${{ env.CPP_STANDARD }} -DBUILD_SHARED_LIBS=OFF ${{ matrix.os == 'windows-latest' && '-DCMAKE_CXX_FLAGS_RELEASE="/MT"' || '-G Ninja -DCMAKE_CXX_FLAGS=-fPIC -DCMAKE_C_FLAGS=-fPIC -Dnetwork-accessor=socket -Dtranscoder=iconv' }} - cmake --build xerces-build --parallel $(nproc) --config Release --target install + cmake -G Ninja -B xerces-build -S xerces-source -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/xerces -DCMAKE_CXX_STANDARD=${{ env.CPP_STANDARD }} -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ env.MACOSX_DEPLOYMENT_TARGET }} -DBUILD_SHARED_LIBS=OFF -DCMAKE_CXX_FLAGS=-fPIC -DCMAKE_C_FLAGS=-fPIC -Dnetwork-accessor=socket -Dtranscoder=iconv + cmake --build xerces-build --parallel --config Release --target install + + - name: Save Xerces-C Installation + if: runner.os == 'macOS' && steps.cache-xerces.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/xerces + key: ${{ steps.cache-xerces.outputs.cache-primary-key }} - name: Cache Geant4 Installation - if: matrix.os != 'ubuntu-latest' + if: runner.os == 'macOS' id: cache-geant4 - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: ${{ env.Geant4_DIR }} key: - geant4-v${{ env.GEANT4_VERSION }}-cpp${{ env.CPP_STANDARD }}-${{ - matrix.platform_id }} + geant4-v${{ env.GEANT4_VERSION }}-shared-qt${{ env.QT_VERSION }}-pyqt5-qt5-${{ + env.PYQT5_QT5_VERSION }}-cpp${{ env.CPP_STANDARD }}-${{ matrix.arch + }}-macos${{ env.MACOSX_DEPLOYMENT_TARGET }} - - name: Build and Install Geant4 (without datasets) - if: - steps.cache-geant4.outputs.cache-hit != 'true' && matrix.os != - 'ubuntu-latest' + - name: Build and Install Geant4 (shared, Qt viewer, without datasets) + if: runner.os == 'macOS' && steps.cache-geant4.outputs.cache-hit != 'true' run: | - git clone https://github.com/Geant4/geant4.git ${{ github.workspace }}/geant4-source --depth 1 --branch v${{ env.GEANT4_VERSION }} - cmake -B geant4-build -S geant4-source ${{ env.CMAKE_ARGS }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ env.Geant4_DIR }} -DXERCESC_ROOT_DIR=${{ github.workspace }}/xerces -DCMAKE_CXX_STANDARD=${{ env.CPP_STANDARD }} -DGEANT4_USE_GDML=ON -DGEANT4_INSTALL_EXAMPLES=OFF -DGEANT4_INSTALL_DATA=OFF -DGEANT4_BUILD_TLS_MODEL=global-dynamic -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=OFF ${{ matrix.os == 'windows-latest' && '-DCMAKE_CXX_FLAGS_RELEASE="/MT"' || '-G Ninja -DCMAKE_CXX_FLAGS=-fPIC -DCMAKE_C_FLAGS=-fPIC -DGEANT4_USE_SYSTEM_EXPAT=OFF' }} - cmake --build geant4-build --parallel $(nproc) --config Release --target install - - - name: Setup Python - uses: actions/setup-python@v5 + git clone https://github.com/Geant4/geant4.git geant4-source --depth 1 --branch v${{ env.GEANT4_VERSION }} + # Typo in Geant4 11.2.2's g4tools (fixed in later releases) that Xcode 16.3+ clang rejects. + sed -i '' 's/a_from\.m_barnch/a_from.m_branch/' geant4-source/source/externals/g4tools/include/tools/wroot/columns.icc + cmake -G Ninja -B geant4-build -S geant4-source -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ env.Geant4_DIR }} -DCMAKE_CXX_STANDARD=${{ env.CPP_STANDARD }} -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ env.MACOSX_DEPLOYMENT_TARGET }} -DXERCESC_ROOT_DIR=${{ github.workspace }}/xerces -DGEANT4_USE_GDML=ON -DGEANT4_USE_QT=ON -DGEANT4_USE_QT3D=OFF -DCMAKE_PREFIX_PATH=$QT_PREFIX -DGEANT4_INSTALL_EXAMPLES=OFF -DGEANT4_INSTALL_DATA=OFF -DGEANT4_USE_SYSTEM_EXPAT=OFF -DGEANT4_USE_SYSTEM_ZLIB=ON -DBUILD_SHARED_LIBS=ON -DBUILD_STATIC_LIBS=OFF + cmake --build geant4-build --parallel --config Release --target install + rm -rf geant4-source geant4-build + + - name: Save Geant4 Installation + if: runner.os == 'macOS' && steps.cache-geant4.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 with: - python-version: 3.12 + path: ${{ env.Geant4_DIR }} + key: ${{ steps.cache-geant4.outputs.cache-primary-key }} + + # Geant4's Qt-linked libraries reference @rpath/Qt*.framework but carry no + # rpath to it, so delocate couldn't resolve (and then --exclude) them. + # relink_qt_wheel.py later swaps this build path for the PyQt5-Qt5 one. + - name: Add the Qt rpath to Geant4's Qt-linked libraries + if: runner.os == 'macOS' + run: | + for lib in "$Geant4_DIR"/lib/libG4*.dylib; do + otool -L "$lib" | grep -q "@rpath/Qt" || continue + otool -l "$lib" | grep -q "path $QT_PREFIX/lib " && continue + install_name_tool -add_rpath "$QT_PREFIX/lib" "$lib" + codesign --force --sign - "$lib" + echo "added Qt rpath: $(basename "$lib")" + done - - name: Build wheel - uses: pypa/cibuildwheel@v2.21.2 + - name: Build wheels + uses: pypa/cibuildwheel@v2.22.0 env: - CIBW_TEST_COMMAND: - python -m pytest {project}/tests -vv --reruns 10 --reruns-delay 30 - --only-rerun "(?i)http|timeout|connection|socket|resolve" - CIBW_ENVIRONMENT: > - SKLEARN_SKIP_NETWORK_TESTS=1 SKLEARN_BUILD_PARALLEL=3 - CIBW_BUILD: cp${{ matrix.python }}-${{ matrix.platform_id }} - CIBW_ARCHS: all - CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_image }} - CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_image }} - CIBW_TEST_SKIP: "*-macosx_arm64" # test on arm64 is not supported yet + CIBW_BUILD: cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* + CIBW_SKIP: "*-musllinux_*" + CIBW_ARCHS: ${{ matrix.arch }} + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_CONFIG_SETTINGS: cmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON + CIBW_TEST_EXTRAS: test,gui + CIBW_TEST_SKIP: "" + + CIBW_ENVIRONMENT_MACOS: > + MACOSX_DEPLOYMENT_TARGET=${{ env.MACOSX_DEPLOYMENT_TARGET }} + Geant4_DIR=${{ env.Geant4_DIR }} + CMAKE_PREFIX_PATH=${{ env.QT_PREFIX }} + GEANT4_PYTHON_APPLICATION_DIR=${{ runner.temp }}/geant4-data + CIBW_ENVIRONMENT_LINUX: GEANT4_PYTHON_APPLICATION_DIR=/tmp/geant4-data + CIBW_BEFORE_ALL_LINUX: | + set -ex + dnf install -y qt5-qtbase-devel mesa-libGL-devel mesa-libGLU-devel python3 -m pip install cmake ninja git clone https://github.com/apache/xerces-c.git /tmp/xerces git -C /tmp/xerces checkout tags/v${{ env.XERCES_VERSION }} @@ -205,17 +222,45 @@ jobs: cmake --build /tmp/xerces/build --parallel $(nproc) --target install > /dev/null 2>&1 rm -rf /tmp/xerces git clone https://github.com/Geant4/geant4.git /tmp/geant4 --branch=v${{ env.GEANT4_VERSION }} --depth=1 - cmake -G Ninja -B /tmp/geant4/build -S /tmp/geant4 -DCMAKE_CXX_STANDARD=${{ env.CPP_STANDARD }} -DCMAKE_BUILD_TYPE=Release -DGEANT4_USE_GDML=ON -DGEANT4_INSTALL_EXAMPLES=OFF -DGEANT4_INSTALL_DATA=OFF -DGEANT4_BUILD_TLS_MODEL=global-dynamic -DCMAKE_CXX_FLAGS=-fPIC -DCMAKE_C_FLAGS=-fPIC -DGEANT4_USE_SYSTEM_EXPAT=OFF -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=OFF + # global-dynamic TLS: Geant4's thread-locals otherwise exhaust the static TLS + # block when Python dlopen()s the extension ("cannot allocate memory in static TLS block"). + cmake -G Ninja -B /tmp/geant4/build -S /tmp/geant4 -DCMAKE_CXX_STANDARD=${{ env.CPP_STANDARD }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_LIBDIR=lib -DGEANT4_BUILD_TLS_MODEL=global-dynamic -DGEANT4_USE_GDML=ON -DGEANT4_USE_QT=ON -DGEANT4_USE_QT3D=OFF -DGEANT4_INSTALL_EXAMPLES=OFF -DGEANT4_INSTALL_DATA=OFF -DGEANT4_USE_SYSTEM_EXPAT=OFF -DBUILD_SHARED_LIBS=ON -DBUILD_STATIC_LIBS=OFF cmake --build /tmp/geant4/build --parallel $(nproc) --target install > /dev/null 2>&1 rm -rf /tmp/geant4 - CIBW_ENVIRONMENT_MACOS: > - MACOSX_DEPLOYMENT_TARGET=11.0 ${{ matrix.platform_id == - 'macosx_arm64' && 'SKIP_PYBIND11_STUBGEN=1' || '' }} - - name: Upload wheel + # Bundle Geant4 into the wheel but leave Qt out; tools/relink_qt_wheel.py + # then points the Qt-linked libraries at the PyQt5-Qt5 package. + CIBW_REPAIR_WHEEL_COMMAND_MACOS: > + rm -rf /tmp/delocated && + delocate-wheel --require-archs {delocate_archs} --exclude .framework/Versions/5/Qt -w /tmp/delocated -v {wheel} && + python -m pip install wheel && + python ${{ github.workspace }}/tools/relink_qt_wheel.py /tmp/delocated/*.whl -w {dest_dir} + CIBW_REPAIR_WHEEL_COMMAND_LINUX: > + rm -rf /tmp/repaired && + auditwheel repair -w /tmp/repaired {wheel} + --exclude libQt5Core.so.5 --exclude libQt5Gui.so.5 --exclude libQt5Widgets.so.5 + --exclude libQt5OpenGL.so.5 --exclude libQt5PrintSupport.so.5 --exclude libQt5DBus.so.5 && + python -m pip install wheel && + python /project/tools/relink_qt_wheel.py /tmp/repaired/*.whl -w {dest_dir} + + # Runtime libraries PyQt5-Qt5 expects from the system, plus a virtual display. + CIBW_BEFORE_TEST_LINUX: > + dnf install -y xorg-x11-server-Xvfb mesa-dri-drivers mesa-libGL fontconfig freetype + libxkbcommon libxkbcommon-x11 xcb-util-wm xcb-util-image xcb-util-keysyms + xcb-util-renderutil dbus-libs + CIBW_TEST_COMMAND_MACOS: > + python -m pytest {project}/tests -vv --reruns 10 --reruns-delay 30 + --only-rerun "(?i)http|timeout|connection|socket|resolve" && + python {project}/tools/smoke_viewer.py + CIBW_TEST_COMMAND_LINUX: > + python -m pytest {project}/tests -vv --reruns 10 --reruns-delay 30 + --only-rerun "(?i)http|timeout|connection|socket|resolve" && + xvfb-run -a python {project}/tools/smoke_viewer.py + + - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + name: cibw-wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse/*.whl check-wheels-size: diff --git a/pyproject.toml b/pyproject.toml index 5258efd6..d7d5d398 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,11 @@ wheel.expand-macos-universal-tags = true write_to = "src/geant4_python_application/_version.py" [project.optional-dependencies] +# Qt for the interactive viewer in the published wheels. Only Application.visualize() +# loads it; source builds use the Qt that their Geant4 was built against instead. +gui = [ + "PyQt5-Qt5>=5.15.2,<5.16", +] dev = [ "boost_histogram", "hist", diff --git a/tools/build_macos_self_contained.sh b/tools/build_macos_self_contained.sh new file mode 100755 index 00000000..b2912080 --- /dev/null +++ b/tools/build_macos_self_contained.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu + +if [ "$(uname -s)" != "Darwin" ]; then + echo "This script builds the self-contained macOS wheel." >&2 + exit 2 +fi + +python_bin=${PYTHON:-python} +geant4_dir=${Geant4_DIR:-/usr/local/lib/cmake/Geant4} +output_dir=${1:-dist/self-contained} +raw_dir="$output_dir/raw" +repaired_dir="$output_dir/repaired" + +rm -rf "$raw_dir" "$repaired_dir" +mkdir -p "$raw_dir" "$repaired_dir" "$output_dir" +"$python_bin" -m pip install --quiet build delocate wheel +"$python_bin" -m build --wheel --outdir "$raw_dir" \ + -Ccmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON \ + -Ccmake.define.Geant4_DIR="$geant4_dir" + +wheel=$(find "$raw_dir" -maxdepth 1 -name '*.whl' -print | head -n 1) +if [ -z "$wheel" ]; then + echo "Wheel build did not produce an artifact." >&2 + exit 1 +fi + +# Bundle Geant4, Xerces-C, Expat and other dylibs, but leave Qt out: the wheel +# takes Qt from the PyQt5-Qt5 package (the [gui] extra) at runtime instead of +# the build machine's Qt, which relink_qt_wheel.py sets up. +"$python_bin" -m delocate.cmd.delocate_wheel \ + --exclude .framework/Versions/5/Qt -w "$repaired_dir" "$wheel" +"$python_bin" "$(dirname "$0")/relink_qt_wheel.py" "$repaired_dir"/*.whl -w "$output_dir" +rm -rf "$raw_dir" "$repaired_dir" +echo "Relocatable wheel written to $output_dir; install it with the [gui] extra for the viewer" diff --git a/tools/relink_qt_wheel.py b/tools/relink_qt_wheel.py new file mode 100644 index 00000000..0bf5e0f6 --- /dev/null +++ b/tools/relink_qt_wheel.py @@ -0,0 +1,122 @@ +"""Point a repaired wheel's Qt dependencies at the Qt shipped in PyQt5-Qt5. + +Run after delocate-wheel / auditwheel repair, which must be told to leave Qt +out of the wheel (``--exclude``). Every bundled binary that links Qt gets an +rpath to site-packages/PyQt5/Qt5/lib; on macOS any absolute Qt framework paths +(e.g. Homebrew's /opt/homebrew/opt/qt@5/...) are first rewritten to @rpath. +A marker file tells the package to use that Qt's plugins at runtime. + + python tools/relink_qt_wheel.py dist/repaired/pkg.whl -w dist/final +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +PACKAGE = "geant4_python_application" +MARKER = f"{PACKAGE}/_qt_from_pyqt5_qt5" +# Relative to site-packages, which is the root of an unpacked wheel. +QT_LIB_DIR = "PyQt5/Qt5/lib" + +# Absolute (e.g. Homebrew) or already @rpath-relative Qt framework references. +MACOS_QT_FRAMEWORK = re.compile(r"^\s+(\S*/(Qt\w+\.framework/Versions/5/Qt\w+))\s") +LINUX_QT_LIBRARY = re.compile(r"^libQt5\w+\.so\.5$") +BUNDLED_QT = re.compile(r"^(libQt5\w*|Qt[A-Z]\w*)([.-]|$)") + + +def run(*args: str) -> str: + result = subprocess.run(args, capture_output=True, text=True) + if result.returncode != 0: + sys.exit(f"Command failed ({result.returncode}): {' '.join(args)}\n{result.stderr}{result.stdout}") + return result.stdout + + +def unpack(wheel: Path, root: Path) -> None: + # Not 'wheel unpack': it rejects RECORD hashes that delocate leaves stale + # after retagging WHEEL. 'wheel pack' regenerates RECORD afterwards anyway. + with zipfile.ZipFile(wheel) as archive: + for info in archive.infolist(): + path = Path(archive.extract(info, root)) + mode = (info.external_attr >> 16) & 0o777 + if mode: + path.chmod(mode) + + +def binaries(root: Path): + for path in sorted(root.rglob("*")): + if path.is_file() and (path.suffix in {".so", ".dylib"} or ".so." in path.name): + yield path + + +def rpath_to_qt(root: Path, binary: Path, origin: str) -> str: + return f"{origin}/{os.path.relpath(root / QT_LIB_DIR, binary.parent)}" + + +def relink_macos(root: Path, binary: Path) -> bool: + load_commands = run("otool", "-L", str(binary)).splitlines()[1:] + references = [m.groups() for line in load_commands if (m := MACOS_QT_FRAMEWORK.match(line))] + if not references: + return False + for old, framework in references: + if old != f"@rpath/{framework}": + run("install_name_tool", "-change", old, f"@rpath/{framework}", str(binary)) + rpath = rpath_to_qt(root, binary, "@loader_path") + rpaths = re.findall(r"^\s+path (\S+) \(offset", run("otool", "-l", str(binary)), re.M) + # Absolute rpaths only point into the build machine (e.g. its Qt or Geant4). + for stale in (p for p in rpaths if p.startswith("/")): + run("install_name_tool", "-delete_rpath", stale, str(binary)) + if rpath not in rpaths: + run("install_name_tool", "-add_rpath", rpath, str(binary)) + # install_name_tool invalidates the ad-hoc signature, and arm64 macOS + # refuses to load unsigned code. + run("codesign", "--force", "--sign", "-", str(binary)) + return True + + +def relink_linux(root: Path, binary: Path) -> bool: + needed = run("patchelf", "--print-needed", str(binary)).split() + if not any(LINUX_QT_LIBRARY.match(name) for name in needed): + return False + entries = [e for e in run("patchelf", "--print-rpath", str(binary)).strip().split(":") if e] + rpath = rpath_to_qt(root, binary, "$ORIGIN") + if rpath not in entries: + run("patchelf", "--set-rpath", ":".join([*entries, rpath]), str(binary)) + return True + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("wheel", type=Path, help="wheel already repaired by delocate/auditwheel") + parser.add_argument("-w", "--wheel-dir", type=Path, required=True, help="output directory") + args = parser.parse_args() + + relink = relink_macos if sys.platform == "darwin" else relink_linux + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "wheel" + unpack(args.wheel, root) + + bundled = [str(p.relative_to(root)) for p in root.rglob("*") if BUNDLED_QT.match(p.name)] + if bundled: + sys.exit(f"Qt was bundled into the wheel; exclude it during repair: {bundled}") + + relinked = [b for b in binaries(root) if relink(root, b)] + if not relinked: + sys.exit("No bundled binary links Qt; was the wheel built with the viewer enabled?") + + (root / MARKER).write_text("PyQt5-Qt5\n") + args.wheel_dir.mkdir(parents=True, exist_ok=True) + run(sys.executable, "-m", "wheel", "pack", str(root), "-d", str(args.wheel_dir)) + + for binary in relinked: + print(f"Qt -> {QT_LIB_DIR}: {binary.relative_to(root)}") + + +if __name__ == "__main__": + main() diff --git a/tools/smoke_viewer.py b/tools/smoke_viewer.py new file mode 100644 index 00000000..2e195763 --- /dev/null +++ b/tools/smoke_viewer.py @@ -0,0 +1,53 @@ +"""CI check that an installed wheel's Qt viewer works with the Qt from the gui extra. + +Needs a display: run directly on macOS, under ``xvfb-run`` on Linux. +""" + +from __future__ import annotations + +import os +import signal +import subprocess +import sys +import textwrap + +import geant4_python_application as g4 + +VIEWER_SECONDS = 45 + +if not g4.Application.visualization_available(): + sys.exit("Qt viewer unavailable: viewer module missing or the [gui] extra not installed") + +# The viewer module must resolve every Qt library it links against. +subprocess.run( + [sys.executable, "-c", "from geant4_python_application import _geant4_vis"], + check=True, +) +print("viewer module loads") + +# Open the viewer; it blocks until its window is closed, so surviving the +# timeout means the window came up and stayed up. +viewer = subprocess.Popen( + [ + sys.executable, + "-c", + textwrap.dedent( + """ + import geant4_python_application as g4 + with g4.Application(gdml=g4.basic_gdml, seed=1) as app: + app.visualize() + """ + ), + ], + # Own process group, so the Geant4 child process is cleaned up too. + start_new_session=True, +) +try: + viewer.wait(timeout=VIEWER_SECONDS) +except subprocess.TimeoutExpired: + print(f"viewer still running after {VIEWER_SECONDS} s") +else: + sys.exit(f"viewer exited early with code {viewer.returncode}") +finally: + os.killpg(viewer.pid, signal.SIGKILL) + viewer.wait() From cc099170c1e0f5f7917f27733f8c2dd3fb0dd448 Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sat, 10 Oct 2026 16:02:36 +0530 Subject: [PATCH 5/8] Rewrite the installation guide around prebuilt wheels Lead with the one-command install from PyPI, which needs no separate Geant4 (C++) installation, with installing the GitHub release wheels as the route until the package is published there. Add a platform support table, explain the 'gui' extra and the system libraries Qt needs on minimal Linux installs, and keep building from source for other platforms and development. Also fix the README usage example, which called run(n_events=100) although run() takes the number of events positionally, and the data path example, which called a get_data_path() function that does not exist. --- INSTALLATION.md | 192 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 174 ++++++++++++++++++++++--------------------- 2 files changed, 284 insertions(+), 82 deletions(-) create mode 100644 INSTALLATION.md diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 00000000..db1063d0 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,192 @@ +# Installation + +Prebuilt wheels bundle a compiled Geant4 11.2.2, so on supported platforms you +**do not need to install Geant4 (C++) yourself**. Building from source still +requires a Geant4 installation. + +| Platform | Python | Prebuilt wheel | Qt viewer (`[gui]`) | +| --------------------- | ----------- | -------------- | ------------------- | +| macOS, Apple Silicon | 3.8 – 3.13 | yes | yes | +| macOS, Intel | 3.8 – 3.13 | yes | yes | +| Linux x86_64 | 3.8 – 3.13 | yes | yes (needs a desktop with OpenGL) | +| Linux aarch64, Windows | — | no | build from source | + +## Install a prebuilt wheel + +### From PyPI + +```bash +pip install "geant4-python-application[gui]" +``` + +Leave out `[gui]` for a physics-only install without Qt: + +```bash +pip install geant4-python-application +``` + +### From a GitHub release + +Until the package is on PyPI, install the wheels attached to a +[GitHub release](https://github.com/lobis/geant4-python-application/releases). +pip picks the right wheel for your platform and Python version; replace `` +with the release tag: + +```bash +pip install "geant4-python-application[gui]" \ + --find-links https://github.com/lobis/geant4-python-application/releases/expanded_assets/ +``` + +### What the `gui` extra does + +The interactive viewer is a separate module that only `Application.visualize()` +loads. Importing the package and running simulations never loads Qt, so +headless machines, containers and clusters need neither Qt nor OpenGL. + +The `gui` extra installs Qt from the `PyQt5-Qt5` package; no Homebrew, conda or +system Qt is needed. On Linux, Qt still relies on the system's OpenGL and X11 +libraries, which desktop installations already have. Minimal installations may +need them added; the CI tests run on AlmaLinux with: + +```bash +dnf install mesa-libGL fontconfig freetype libxkbcommon libxkbcommon-x11 \ + xcb-util-wm xcb-util-image xcb-util-keysyms xcb-util-renderutil dbus-libs +``` + +On Debian/Ubuntu install the equivalent packages (e.g. `libgl1`, +`libxkbcommon-x11-0` and the `libxcb-*` utility libraries). + +`Application.visualization_available()` reports whether the viewer can be +opened, without loading Qt; otherwise `app.visualize()` raises a `RuntimeError` +explaining what is missing. Scripts that open the viewer by default (e.g. +`b1.py`) can run headless with `--batch`. + +## Build from source + +Needed on platforms without wheels, and for development. Building requires a +Geant4 installation. + +### 1. Install Geant4 + +Via conda: + +```bash +conda install -c conda-forge geant4=11.2.2 +``` + +You may also need build tools, e.g. `conda install cmake ninja` (plus +`gxx_linux-64` on Linux). + +Or from source, matching the CI configuration: + +```bash +git clone https://github.com/Geant4/geant4.git ./geant4-source --depth 1 --branch v11.2.2 + +cmake -B ./geant4-build -S ./geant4-source \ + -DCMAKE_INSTALL_PREFIX=./geant4-install \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DGEANT4_USE_GDML=ON \ + -DGEANT4_INSTALL_EXAMPLES=OFF \ + -DGEANT4_INSTALL_DATA=OFF \ + -DGEANT4_BUILD_TLS_MODEL=global-dynamic \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_CXX_FLAGS=-fPIC \ + -DCMAKE_C_FLAGS=-fPIC \ + -DGEANT4_USE_SYSTEM_EXPAT=OFF + +cmake --build ./geant4-build --parallel $(nproc) --config Release --target install +``` + +If `xerces-c` is missing, install it from your package manager +(`apt-get install libxerces-c-dev`) or build it from source: + +```bash +git clone https://github.com/apache/xerces-c.git ./xerces-source +git -C ./xerces-source checkout tags/v3.2.5 + +cmake -B ./xerces-build -S ./xerces-source \ + -DCMAKE_INSTALL_PREFIX=./xerces-install \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_CXX_FLAGS=-fPIC \ + -DCMAKE_C_FLAGS=-fPIC \ + -Dnetwork-accessor=socket \ + -Dtranscoder=iconv + +cmake --build ./xerces-build --parallel $(nproc) --config Release --target install +``` + +### 2. Build the package + +```bash +pip install git+https://github.com/lobis/geant4-python-application +``` + +or from a clone: + +```bash +git clone https://github.com/lobis/geant4-python-application.git +cd geant4-python-application +pip install . +``` + +The provided `Dockerfile` also documents the required dependencies: + +```bash +docker build -t geant4-python-application . +``` + +### Qt viewer in source builds + +Off by default. It needs a Geant4 built as **shared** libraries with Qt and +OpenGL (e.g. conda-forge `geant4` with `qt[yes]`/`opengl-x11[yes]`); the viewer +then uses that Geant4's Qt: + +```bash +pip install . -Ccmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON +``` + +### Building a relocatable macOS wheel yourself + +Bundles Geant4, Xerces-C, Expat and other non-system libraries into a wheel +that takes Qt from `PyQt5-Qt5`, like the published wheels: + +```bash +PYTHON="$CONDA_PREFIX/bin/python" \ +Geant4_DIR=/usr/local/lib/cmake/Geant4 \ +tools/build_macos_self_contained.sh +pip install "$(ls dist/self-contained/*.whl)[gui]" +``` + +The wheel only installs on the macOS version (and newer) that the bundled +Geant4 was built for. The published wheels are built by +`.github/workflows/wheels.yaml`, targeting macOS 11 and manylinux_2_28. + +## Data files + +Geant4's physics data files (~2 GB) are not part of the wheel. They download +automatically the first time a simulation runs, unless an existing Geant4 +installation (found via `geant4-config`) already provides the exact dataset +versions, in which case those are used. + +```bash +python -c "import geant4_python_application as g4; print(g4.data_directory())" +``` + +- Default location: `$CONDA_PREFIX/share/geant4_python_application` (or the + equivalent `sys.prefix` for a venv). +- Override the persistent location by setting `GEANT4_PYTHON_APPLICATION_DIR` + before import, or calling `application_directory(path)` before the + application initializes. +- Use a temporary directory instead: `application_directory(temp=True)`. If + the OS partially cleans it up mid-run, delete it manually and let the + application recreate it. +- Interrupted downloads resume, failed downloads raise an error, and each + archive's MD5 checksum is verified before it is extracted. +- Uninstalling the package does **not** remove downloaded data files — remove + them manually. +- For cluster batch jobs, point `application_directory` at a shared location + to avoid redundant downloads across jobs. diff --git a/README.md b/README.md index 5de06079..21f91e84 100644 --- a/README.md +++ b/README.md @@ -23,136 +23,146 @@ looking at [geant4_pybind](https://github.com/HaarigerHarald/geant4_pybind). - The complete event data is available as an [awkward array](https://github.com/scikit-hep/awkward) -## Installation +New to Geant4 or this package? See the +[Python developer guide](PYTHON_DEVELOPER_GUIDE.md). + +## Platform support -The package is pip installable and should work on all major platforms. Geant4 is -not required to be installed on the system as the PyPI distribution includes a -statically linked version of Geant4. +Tested on macOS and Ubuntu; still undergoing broader testing. If you hit a +bug, please report it via [GitHub Issues](https://github.com/lobis/geant4-python-application/issues) +or reach out to kavyavadhwa@gmail.com. -However, for the time being, it's recommended to install from the GitHub -repository to get the latest version, which makes Geant4 a necessary dependency. +## Installation -Geant4 is available on conda-forge and can be installed with: +Prebuilt wheels for macOS (Apple Silicon and Intel) and Linux x86_64, Python +3.8 – 3.13, bundle Geant4 — no separate Geant4 (C++) installation is needed: ```bash -conda install -c conda-forge geant4=11.2.2 +pip install "geant4-python-application[gui]" ``` -This project can then be installed via pip: +The `gui` extra adds the interactive Qt viewer; leave it out for a +physics-only install. Until the package is published on PyPI, install the +wheels from a +[GitHub release](https://github.com/lobis/geant4-python-application/releases) +as described in [INSTALLATION.md](INSTALLATION.md). + +Geant4's physics data files download automatically on first use, so you can +run an example straight away: ```bash -python -m pip install git+https://github.com/lobis/geant4-python-application +python examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py --batch -n 10 ``` -You may need additional dependencies to perform the build. I found that these -are usually enuough: +`--batch` runs headless; drop it to open the Qt viewer. -```bash -conda install cmake ninja gxx_linux-64 -``` +Building from source (other platforms, development), the Qt viewer, and data +file management are covered in [INSTALLATION.md](INSTALLATION.md). -The provided `Dockerfile` can be used for development purposes and as -documentation on the required dependencies. To build the image run from the root -directory: +## Usage -```bash -docker build -t geant4-python-application . -``` +```python +import geant4_python_application as g4 -Geant4 can be installed using conda: +# Use a temporary directory for the Geant4 data files (remove this line to use the default location) +g4.application_directory(temp=True) -```bash -conda install -c conda-forge geant4 +with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + events = app.run(100) + +print(events) ``` -For development purposes however, it's recommended to install Geant4 from source -with a similar configuration to the one used in the CI: +A Geant4 reference physics list can be selected by name: -```bash -git clone https://github.com/Geant4/geant4.git ./geant4-source --depth 1 --branch v11.2.2 +```python +with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT") as app: + events = app.run(100) -cmake -B ./geant4-build -S ./geant4-source -DCMAKE_INSTALL_PREFIX=./geant4-install -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 -DGEANT4_USE_GDML=ON -DGEANT4_INSTALL_EXAMPLES=OFF -DGEANT4_INSTALL_DATA=OFF -DGEANT4_BUILD_TLS_MODEL=global-dynamic -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_CXX_FLAGS=-fPIC -DCMAKE_C_FLAGS=-fPIC -DGEANT4_USE_SYSTEM_EXPAT=OFF -cmake --build ./geant4-build --parallel $(nproc) --config Release --target install +print(g4.Application.available_physics_lists()) ``` -One common installation issue is the dependency `xerces-c`. You may be able to -install it from your package manager (e.g. `apt-get install libxerces-c-dev) but -it's also possible to build it from source. In this case make sure the -installation directory is accessible or just use the default system path. +Particle guns and Geant4's General Particle Source have a Python interface: -```bash -git clone https://github.com/apache/xerces-c.git ./xerces-source -git -C ./xerces-source checkout tags/v3.2.5 -cmake -B ./xerces-build -S ./xerces-source -DCMAKE_INSTALL_PREFIX=./xerces-install -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 -DBUILD_SHARED_LIBS=OFF -DCMAKE_CXX_FLAGS=-fPIC -DCMAKE_C_FLAGS=-fPIC -Dnetwork-accessor=socket -Dtranscoder=iconv -cmake --build ./xerces-build --parallel $(nproc) --config Release --target install +```python +with g4.Application(gdml=g4.basic_gdml) as app: + app.generator.use_gps().particle("gamma").energy(2, "MeV") + app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) + events = app.run(100) ``` -After all dependencies are met, you should be able to install the package by -running the following command from the root directory: +Advanced GPS distributions remain accessible through +`app.generator.commands(["/gps/pos/type Plane", ...])`. -```bash -pip install . -``` +Optical physics and uniform global magnetic/electric fields can be enabled directly: -### Geant4 data files +```python +with g4.Application(gdml=my_optical_gdml, optical=True) as app: + app.detector.magnetic_field = (0.0, 0.0, 1.5) # tesla + app.detector.electric_field = (0.0, 0.0, 0.5) # kV/cm + events = app.run(100) +``` -Geant4 comes with a large set of data files which are required in order to run. -The data files are the bulk of the Geant4 installation and can be quite large. -These data files are not included in the Python wheels for this package due to -its size. +Optical scintillator GDML + tuning helpers: -The python package automatically manages the download of the data files. To -check the location of the data files, run the following command: +```python +from geant4_python_application import optical as gopt -```bash -python -c "import geant4_python_application; print(geant4_python_application.get_data_path())" +with g4.Application(gdml=gopt.optical_water_gdml, optical=True) as app: + app.commands(gopt.scintillation_commands(yield_factor=1.0)) + events = app.run(100) # track.particle contains 'opticalphoton' ``` -**Uninstalling the package will not remove the data files. The user is -responsible for removing them manually.** +Recorded steps can be scored per event or into a Cartesian energy mesh: -#### Overriding the default data path +```python +energy_per_event = g4.Scoring.energy_deposit(events, volume="detector") +mesh, edges = g4.Scoring.energy_mesh(events, bins=(20, 20, 40)) +``` -The default data path can be overridden by calling `application_directory`. This -should be done before the application is initialized. +Python run/event/track/step callbacks (offline over awkward arrays): ```python -from geant4_python_application import application_directory - -application_directory("/some/other/path") +with g4.Application(gdml=g4.basic_gdml) as app: + events = app.run_with_callbacks( + 100, on_run=lambda evs: print(len(evs)), + on_event=lambda ev: None, on_track=lambda tr, ev: None, + on_step=lambda st, tr, ev: None, + ) ``` -Overriding the default data directory is encouraged when submitting batch jobs -to a cluster in order to avoid downloading the data files multiple times. In -this case the application directory should point to some shared location. +Extra physics constructors incl. DNA, CAD/VTK/ROOT/HepMC, and MPI splitting: -#### Using a temporary directory +```python +print(g4.Application.available_extra_physics()) +with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT") as app: + app.add_physics("G4EmDNAPhysics_option2") # or G4EmExtraPhysics, G4OpticalPhysics... + events = app.run(100) -A temporary directory can be used by calling: +from geant4_python_application import cad, io as gio, distributed as dist -```python -from geant4_python_application import application_directory +gdml = cad.mesh_to_gdml(*cad.cube_mesh(100.0)) # or cad.stl_ascii_to_gdml(stl_text) +gio.mesh_to_vtk(mesh, edges, "mesh.vtk") +gio.events_to_parquet(events, "events.parquet") +gio.events_to_root(events, "events.root") # needs uproot +primaries = gio.hepmc_to_primaries(open("events.hepmc").read()) -application_directory(temp=True) +rank, size = dist.get_rank_size() +start, count = dist.split_count(1000, rank, size) # mpiexec -n 2 python run.py ``` -The operating system should take care of cleaning up the temporary directory. +### Interactive Qt visualization -It is possible that the operating system will delete only some of the files in -the temporary directory which can lead to a Geant4 runtime error. In this case -it's recommended to delete the temporary directory manually and let the -application recreate it again. - -## Usage +Install with the `gui` extra (`pip install "geant4-python-application[gui]"`), +or for source builds see [INSTALLATION.md](INSTALLATION.md). Then open Geant4's +interactive Qt viewer. The call returns when the window is closed, and commands +can be entered in the viewer's command panel: ```python import geant4_python_application as g4 -# Use a temporary directory for the Geant4 data files (remove this line to use the default location) -g4.application_directory(temp=True) - with g4.Application(gdml=g4.basic_gdml, seed=137) as app: - events = app.run(n_events=100) - -print(events) + app.command("/gun/particle e-") + app.command("/gun/energy 100 MeV") + app.visualize() ``` From badd7a1ba0ff4416742ac1cacd4688f467d59024 Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sat, 10 Oct 2026 22:03:28 +0530 Subject: [PATCH 6/8] Prepare the 0.1.0 release - Ship the licences of the libraries bundled into the wheels (Geant4, CLHEP, Expat, PTL, zlib, Xerces-C) in NOTICE and licenses/. - Keep main's per-user data directory so the ~2 GB of datasets download once per user rather than once per environment. - Make Application's new optical argument keyword-only, so positional calls keep main's (n_threads, gdml, physics, seed) meaning. - State in the install docs that the package is not on PyPI yet and document installing the CI wheels until 0.1.0 is released. - Let manual Wheels runs stamp a test version (e.g. 0.1.0rc1). --- .github/workflows/wheels.yaml | 13 +- INSTALLATION.md | 38 +- NOTICE | 21 + README.md | 9 +- licenses/CLHEP-COPYING | 674 ++++++++++++++++++ licenses/CLHEP-COPYING.LESSER | 165 +++++ licenses/Expat-COPYING | 22 + licenses/Geant4-LICENSE | 73 ++ licenses/PTL-LICENSE | 21 + licenses/Xerces-C-LICENSE | 202 ++++++ licenses/Xerces-C-NOTICE | 10 + licenses/zlib-LICENSE | 23 + pyproject.toml | 2 + src/geant4_python_application/application.py | 3 +- .../files/directories.py | 10 +- 15 files changed, 1267 insertions(+), 19 deletions(-) create mode 100644 NOTICE create mode 100644 licenses/CLHEP-COPYING create mode 100644 licenses/CLHEP-COPYING.LESSER create mode 100644 licenses/Expat-COPYING create mode 100644 licenses/Geant4-LICENSE create mode 100644 licenses/PTL-LICENSE create mode 100644 licenses/Xerces-C-LICENSE create mode 100644 licenses/Xerces-C-NOTICE create mode 100644 licenses/zlib-LICENSE diff --git a/.github/workflows/wheels.yaml b/.github/workflows/wheels.yaml index 03e4f609..e9064e6d 100644 --- a/.github/workflows/wheels.yaml +++ b/.github/workflows/wheels.yaml @@ -2,6 +2,11 @@ name: Wheels on: workflow_dispatch: + inputs: + version: + description: "Version to stamp on test builds (e.g. 0.1.0rc1); empty uses git" + required: false + default: "" schedule: - cron: "0 4 * * *" # every day at 4am UTC @@ -40,7 +45,10 @@ jobs: python-version: 3.12 - name: Build sdist + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ inputs.version }} run: | + [ -n "$SETUPTOOLS_SCM_PRETEND_VERSION" ] || unset SETUPTOOLS_SCM_PRETEND_VERSION python -m pip install build python -m build --sdist . @@ -210,7 +218,10 @@ jobs: Geant4_DIR=${{ env.Geant4_DIR }} CMAKE_PREFIX_PATH=${{ env.QT_PREFIX }} GEANT4_PYTHON_APPLICATION_DIR=${{ runner.temp }}/geant4-data - CIBW_ENVIRONMENT_LINUX: GEANT4_PYTHON_APPLICATION_DIR=/tmp/geant4-data + ${{ inputs.version && format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', inputs.version) || '' }} + CIBW_ENVIRONMENT_LINUX: > + GEANT4_PYTHON_APPLICATION_DIR=/tmp/geant4-data + ${{ inputs.version && format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', inputs.version) || '' }} CIBW_BEFORE_ALL_LINUX: | set -ex diff --git a/INSTALLATION.md b/INSTALLATION.md index db1063d0..06b79c29 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -15,6 +15,11 @@ requires a Geant4 installation. ### From PyPI +> **Not published yet.** The first PyPI release will be 0.1.0. Until then, +> `pip install geant4-python-application` fails with "No matching distribution +> found"; use the [CI wheels](#from-a-ci-build-before-the-first-release) +> instead. + ```bash pip install "geant4-python-application[gui]" ``` @@ -27,16 +32,36 @@ pip install geant4-python-application ### From a GitHub release -Until the package is on PyPI, install the wheels attached to a -[GitHub release](https://github.com/lobis/geant4-python-application/releases). -pip picks the right wheel for your platform and Python version; replace `` -with the release tag: +Each [GitHub release](https://github.com/lobis/geant4-python-application/releases) +from 0.1.0 on also has the wheels attached. pip picks the right wheel for your +platform and Python version; replace `` with the release tag (e.g. +`v0.1.0`): ```bash pip install "geant4-python-application[gui]" \ --find-links https://github.com/lobis/geant4-python-application/releases/expanded_assets/ ``` +### From a CI build (before the first release) + +Every run of the +[Wheels workflow](https://github.com/lobis/geant4-python-application/actions/workflows/wheels.yaml) +keeps its wheels as downloadable artifacts for 90 days. Pick a successful run +and download the artifact for your platform: `cibw-wheels-ubuntu-latest-x86_64` +(Linux), `cibw-wheels-macos-15-arm64` (Apple Silicon) or +`cibw-wheels-macos-15-intel-x86_64` (Intel Mac). From the run page this needs a +GitHub login; with the [GitHub CLI](https://cli.github.com) (`gh auth login` +first): + +```bash +gh run download --repo lobis/geant4-python-application \ + -n cibw-wheels-ubuntu-latest-x86_64 -D ./g4-wheels +pip install "geant4-python-application[gui]" --find-links ./g4-wheels +``` + +`--find-links` takes the package from the folder and its dependencies +(including Qt for `[gui]`) from PyPI. + ### What the `gui` extra does The interactive viewer is a separate module that only `Application.visualize()` @@ -176,8 +201,9 @@ versions, in which case those are used. python -c "import geant4_python_application as g4; print(g4.data_directory())" ``` -- Default location: `$CONDA_PREFIX/share/geant4_python_application` (or the - equivalent `sys.prefix` for a venv). +- Default location, shared by all your Python environments so the data + downloads only once: `~/Library/Application Support/geant4_python_application` + on macOS, `~/.local/share/geant4_python_application` on Linux. - Override the persistent location by setting `GEANT4_PYTHON_APPLICATION_DIR` before import, or calling `application_directory(path)` before the application initializes. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..84dcaf15 --- /dev/null +++ b/NOTICE @@ -0,0 +1,21 @@ +geant4-python-application +Copyright (c) 2023, Luis Antonio Obis Aparicio and contributors. +Licensed under the BSD 3-Clause License; see LICENSE. + +The prebuilt wheels bundle the following third-party libraries. Their licence +texts are in the licenses/ directory (installed in the wheel's .dist-info). + +- Geant4 11.2.2 (https://geant4.web.cern.ch): Geant4 Software License, + licenses/Geant4-LICENSE. +- CLHEP, as shipped inside Geant4 (G4clhep): GNU LGPL v3, + licenses/CLHEP-COPYING.LESSER and licenses/CLHEP-COPYING. It is bundled as a + separate shared library that can be replaced. +- Expat, as shipped inside Geant4 (G4expat): MIT, licenses/Expat-COPYING. +- PTL, as shipped inside Geant4 (G4ptl): MIT, licenses/PTL-LICENSE. +- zlib, as shipped inside Geant4 (G4zlib, Linux wheels): zlib License, + licenses/zlib-LICENSE. +- Apache Xerces-C++ 3.2.5 (https://xerces.apache.org): Apache License 2.0, + licenses/Xerces-C-LICENSE and licenses/Xerces-C-NOTICE. + +Qt is not bundled. The optional [gui] extra installs it from the PyQt5-Qt5 +package, which carries its own licence (LGPL v3). diff --git a/README.md b/README.md index 21f91e84..8435be93 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,11 @@ pip install "geant4-python-application[gui]" ``` The `gui` extra adds the interactive Qt viewer; leave it out for a -physics-only install. Until the package is published on PyPI, install the -wheels from a -[GitHub release](https://github.com/lobis/geant4-python-application/releases) -as described in [INSTALLATION.md](INSTALLATION.md). +physics-only install. + +> **Not on PyPI yet:** the first release will be 0.1.0. Until then, install the +> wheels built by CI as described in +> [INSTALLATION.md](INSTALLATION.md#from-a-ci-build-before-the-first-release). Geant4's physics data files download automatically on first use, so you can run an example straight away: diff --git a/licenses/CLHEP-COPYING b/licenses/CLHEP-COPYING new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/licenses/CLHEP-COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/licenses/CLHEP-COPYING.LESSER b/licenses/CLHEP-COPYING.LESSER new file mode 100644 index 00000000..65c5ca88 --- /dev/null +++ b/licenses/CLHEP-COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/licenses/Expat-COPYING b/licenses/Expat-COPYING new file mode 100644 index 00000000..dcb45064 --- /dev/null +++ b/licenses/Expat-COPYING @@ -0,0 +1,22 @@ +Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd + and Clark Cooper +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/Geant4-LICENSE b/licenses/Geant4-LICENSE new file mode 100644 index 00000000..3926c4ef --- /dev/null +++ b/licenses/Geant4-LICENSE @@ -0,0 +1,73 @@ +Geant4 Software License +Version 1.0, 28 June 2006 + +Copyright (c) Copyright Holders of the Geant4 Collaboration, 1994-2006. +See http://cern.ch/geant4/license for details on the copyright holders. All +rights not expressly granted under this license are reserved. + +This software includes voluntary contributions made to Geant4. +See http://cern.ch/geant4 for more information on Geant4. + +Installation, use, reproduction, display, modification and redistribution of +this software, with or without modification, in source and binary forms, are +permitted on a non-exclusive basis. Any exercise of rights by you under this +license is subject to the following conditions: + +1. Redistributions of this software, in whole or in part, with or without + modification, must reproduce the above copyright notice and these license + conditions in this software, the user documentation and any other + materials provided with the redistributed software. + +2. The user documentation,if any,included with a redistribution,must include + the following notice:"This product includes software developed by Members + of the Geant4 Collaboration ( http://cern.ch/geant4 )." + If that is where third-party acknowledgments normally appear, this + acknowledgment must be reproduced in the modified version of this + software itself. + +3. The names "Geant4" and "The Geant4 toolkit" may not be used to endorse or + promote software,or products derived therefrom, except with prior written + permission by license@geant4.org. If this software is redistributed in + modified form, the name and reference of the modified version must be + clearly distinguishable from that of this software. + +4. You are under no obligation to provide anyone with any modifications of + this software that you may develop,including but not limited to bug fixes, + patches, upgrades or other enhancements or derivatives of the features, + functionality or performance of this software. However, if you publish or + distribute your modifications without contemporaneously requiring users + to enter into a separate written license agreement, then you are deemed + to have granted all Members and all Copyright Holders of the Geant4 + Collaboration a license to your modifications, including modifications + protected by any patent owned by you,under the conditions of this license. + +5. You may not include this software in whole or in part in any patent or + patent application in respect of any modification of this software + developed by you. + + +6. DISCLAIMER + + THIS SOFTWARE IS PROVIDED BY THE MEMBERS AND COPYRIGHT HOLDERS OF THE GEANT4 + COLLABORATION AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY, OF + SATISFACTORY QUALITY, AND FITNESS FOR A PARTICULAR PURPOSE OR USE ARE + DISCLAIMED. THE MEMBERS OF THE GEANT4 COLLABORATION AND CONTRIBUTORS MAKE NO + REPRESENTATION THAT THE SOFTWARE AND MODIFICATIONS THEREOF,WILL NOT INFRINGE + ANY PATENT, COPYRIGHT, TRADE SECRET OR OTHER PROPRIETARY RIGHT. + +7. LIMITATION OF LIABILITY + + THE MEMBERS AND COPYRIGHT HOLDERS OF THE GEANT4 COLLABORATION AND + CONTRIBUTORS SHALL HAVE NO LIABILITY FOR DIRECT,INDIRECT,SPECIAL, INCIDENTAL, + CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES OF ANY CHARACTER INCLUDING, + WITHOUT LIMITATION, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF USE, + DATA OR PROFITS, OR BUSINESS INTERRUPTION, HOWEVER CAUSED AND ON ANY THEORY + OF CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR + OTHERWISE, ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +8. This license shall terminate with immediate effect and without notice if + you fail to comply with any of the terms of this license, or if you + institute litigation against any Member or Copyright Holder of the Geant4 + Collaboration with regard to this software. diff --git a/licenses/PTL-LICENSE b/licenses/PTL-LICENSE new file mode 100644 index 00000000..a6707da8 --- /dev/null +++ b/licenses/PTL-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jonathan R. Madsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/Xerces-C-LICENSE b/licenses/Xerces-C-LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/licenses/Xerces-C-LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/Xerces-C-NOTICE b/licenses/Xerces-C-NOTICE new file mode 100644 index 00000000..46994eb8 --- /dev/null +++ b/licenses/Xerces-C-NOTICE @@ -0,0 +1,10 @@ + ========================================================================= + == NOTICE file corresponding to section 4(d) of the Apache License, == + == Version 2.0, in this case for the Apache Xerces distribution. == + ========================================================================= + + This product includes software developed by + The Apache Software Foundation (http://www.apache.org/). + + Portions of this software were originally based on the following: + - software copyright (c) 1999, IBM Corporation., http://www.ibm.com. diff --git a/licenses/zlib-LICENSE b/licenses/zlib-LICENSE new file mode 100644 index 00000000..b457317f --- /dev/null +++ b/licenses/zlib-LICENSE @@ -0,0 +1,23 @@ +Copyright notice: + + (C) 1995-2012 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + diff --git a/pyproject.toml b/pyproject.toml index d7d5d398..c3c5aadc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,8 @@ dynamic = ["version"] metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" sdist.include = ["src/geant4_python_application/_version.py"] wheel.expand-macos-universal-tags = true +# Ship the notices of the third-party libraries bundled into the wheels. +wheel.license-files = ["LICENSE", "NOTICE", "licenses/*"] [tool.setuptools_scm] # Section required write_to = "src/geant4_python_application/_version.py" diff --git a/src/geant4_python_application/application.py b/src/geant4_python_application/application.py index c02f4575..148c8b47 100644 --- a/src/geant4_python_application/application.py +++ b/src/geant4_python_application/application.py @@ -122,8 +122,9 @@ def __init__( n_threads: int = 0, gdml: str = None, physics: str = "custom", - optical: bool = False, seed: int = 0, + *, + optical: bool = False, ): geant4_python_application.install_datasets(show_progress=True) diff --git a/src/geant4_python_application/files/directories.py b/src/geant4_python_application/files/directories.py index cf46585c..c7eb80c0 100644 --- a/src/geant4_python_application/files/directories.py +++ b/src/geant4_python_application/files/directories.py @@ -2,7 +2,6 @@ import os import pathlib -import sys import tempfile import platformdirs @@ -15,13 +14,10 @@ _dirs = platformdirs.AppDirs(_app_name, _app_author) -# Keep datasets with the active Python environment by default. This makes a -# venv/conda installation relocatable as one unit instead of silently sharing -# data from the user's global Application Support directory. The environment -# variable retains an explicit deployment-time override. +# Shared by every environment of the user, so the ~2 GB of datasets download +# only once. GEANT4_PYTHON_APPLICATION_DIR overrides it, e.g. on clusters. _application_directory = os.environ.get( - "GEANT4_PYTHON_APPLICATION_DIR", - os.path.join(sys.prefix, "share", _app_name), + "GEANT4_PYTHON_APPLICATION_DIR", _dirs.user_data_dir ) From 69f76df4bb55134743d554ebaaa98b554956b03c Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa Date: Sun, 11 Oct 2026 12:35:13 +0530 Subject: [PATCH 7/8] Describe the released package and attach wheels to releases PyPI shows the README as the project page, so drop the "not on PyPI yet" notes before 0.1.0 goes out. The release job now also attaches the wheels and sdist to the GitHub release, which INSTALLATION.md already documents. --- .github/workflows/wheels.yaml | 9 +++++++++ INSTALLATION.md | 12 +++--------- README.md | 4 ---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels.yaml b/.github/workflows/wheels.yaml index e9064e6d..54ff97ae 100644 --- a/.github/workflows/wheels.yaml +++ b/.github/workflows/wheels.yaml @@ -305,6 +305,8 @@ jobs: needs: [sdist, wheels, check-wheels-size] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' + permissions: + contents: write steps: - name: Setup Python @@ -323,6 +325,13 @@ jobs: with: password: ${{ secrets.PYPI_API_TOKEN }} + - name: Attach wheels to the GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: > + gh release upload "${{ github.event.release.tag_name }}" dist/* + --repo "${{ github.repository }}" --clobber + publish-docker: name: Publish Docker image needs: [upload-pypi] diff --git a/INSTALLATION.md b/INSTALLATION.md index 06b79c29..0cbbd472 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -15,11 +15,6 @@ requires a Geant4 installation. ### From PyPI -> **Not published yet.** The first PyPI release will be 0.1.0. Until then, -> `pip install geant4-python-application` fails with "No matching distribution -> found"; use the [CI wheels](#from-a-ci-build-before-the-first-release) -> instead. - ```bash pip install "geant4-python-application[gui]" ``` @@ -42,12 +37,11 @@ pip install "geant4-python-application[gui]" \ --find-links https://github.com/lobis/geant4-python-application/releases/expanded_assets/ ``` -### From a CI build (before the first release) +### From a CI build (unreleased changes) -Every run of the +To try changes that are not released yet, use the wheels that each run of the [Wheels workflow](https://github.com/lobis/geant4-python-application/actions/workflows/wheels.yaml) -keeps its wheels as downloadable artifacts for 90 days. Pick a successful run -and download the artifact for your platform: `cibw-wheels-ubuntu-latest-x86_64` +keeps as downloadable artifacts for 90 days. Pick a successful run and download the artifact for your platform: `cibw-wheels-ubuntu-latest-x86_64` (Linux), `cibw-wheels-macos-15-arm64` (Apple Silicon) or `cibw-wheels-macos-15-intel-x86_64` (Intel Mac). From the run page this needs a GitHub login; with the [GitHub CLI](https://cli.github.com) (`gh auth login` diff --git a/README.md b/README.md index 8435be93..5a7cd97e 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,6 @@ pip install "geant4-python-application[gui]" The `gui` extra adds the interactive Qt viewer; leave it out for a physics-only install. -> **Not on PyPI yet:** the first release will be 0.1.0. Until then, install the -> wheels built by CI as described in -> [INSTALLATION.md](INSTALLATION.md#from-a-ci-build-before-the-first-release). - Geant4's physics data files download automatically on first use, so you can run an example straight away: From 4f488ef6e277438fef494a671ae63fee465cbbac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 07:07:35 +0000 Subject: [PATCH 8/8] style: pre-commit fixes --- .github/workflows/wheels.yaml | 69 ++-- CMakeLists.txt | 50 ++- INSTALLATION.md | 56 ++-- PYTHON_DEVELOPER_GUIDE.md | 294 ++++++++++-------- README.md | 32 +- docs/FEATURE_SUPPORT.md | 38 +-- .../geant4-examples/00_qt_viewer_smoke.py | 1 + .../01_reference_physics_lists.py | 1 + examples/geant4-examples/02_gps_sources.py | 1 + .../geant4-examples/03_scoring_meshes_hits.py | 1 + .../04_magnetic_electric_fields.py | 1 + .../05_optical_scintillation.py | 1 + examples/geant4-examples/06_callbacks.py | 1 + examples/geant4-examples/07_custom_physics.py | 1 + examples/geant4-examples/08_dna_medical.py | 1 + .../09_cad_vtk_root_generators.py | 1 + .../geant4-examples/10_mpi_distributed.py | 1 + .../Geant4-11.4.2-examples-python/AUDIT.md | 60 ++-- .../COMPATIBILITY.md | 68 ++-- .../Geant4-11.4.2-examples-python/README.md | 33 +- .../advanced/GoldFoilExperiment/gold_foil.py | 8 +- .../NanoparticleTherapy/brain_tumor.py | 89 ++++-- .../NanoparticleTherapy/nano_therapy.py | 109 ++++--- .../basic/B1/b1.py | 33 +- .../basic/B2/b2.py | 27 +- .../basic/B3/b3.py | 26 +- .../basic/B4/b4.py | 34 +- .../basic/B5/b5.py | 44 ++- .../extended/analysis/AnaEx01/ana_ex01.py | 41 ++- .../extended/analysis/AnaEx02/ana_ex02.py | 36 ++- .../extended/analysis/AnaEx03/ana_ex03.py | 36 ++- .../electromagnetic/TestEm1/test_em1.py | 52 +++- .../extended/eventgenerator/HepMC/hepmc.py | 8 +- .../extended/eventgenerator/exgps/exgps.py | 24 +- .../particleGun/particle_gun.py | 38 ++- .../extended/field/field01/field01.py | 47 ++- .../extended/field/field02/field02.py | 20 +- .../extended/hadronic/Hadr00/hadr00.py | 41 ++- .../medical/GammaTherapy/gamma_therapy.py | 48 ++- .../electronScattering/electron_scattering.py | 45 ++- .../extended/optical/LXe/lxe.py | 5 +- .../extended/optical/OpNovice/op_novice.py | 14 +- .../extended/optical/wls/wls.py | 1 + .../extended/parameterisations/Par01/par01.py | 8 +- .../extended/physicslists/factory/factory.py | 17 +- .../radioactivedecay/rdecay01/rdecay01.py | 34 +- .../extended/runAndEvent/RE01/re01.py | 6 +- .../Geant4-11.4.2-examples-python/run_gdml.py | 17 +- licenses/Geant4-LICENSE | 2 +- licenses/zlib-LICENSE | 1 - .../geant4_application/DetectorConstruction.h | 2 +- src/geant4_application/src/Application.cpp | 22 +- src/geant4_application/src/DataModel.cpp | 22 +- .../src/DetectorConstruction.cpp | 19 +- src/geant4_python_application/__init__.py | 14 +- src/geant4_python_application/application.py | 4 +- src/geant4_python_application/cad.py | 28 +- src/geant4_python_application/events.py | 4 +- src/geant4_python_application/generator.py | 8 +- src/geant4_python_application/io.py | 30 +- src/geant4_python_application/optical.py | 4 +- tests/test_analysis.py | 3 +- tests/test_capabilities.py | 8 +- tests/test_run.py | 10 +- tools/audit_examples.py | 16 +- tools/relink_qt_wheel.py | 40 ++- tools/smoke_viewer.py | 4 +- 67 files changed, 1225 insertions(+), 635 deletions(-) diff --git a/.github/workflows/wheels.yaml b/.github/workflows/wheels.yaml index 54ff97ae..e0cf12f1 100644 --- a/.github/workflows/wheels.yaml +++ b/.github/workflows/wheels.yaml @@ -4,7 +4,8 @@ on: workflow_dispatch: inputs: version: - description: "Version to stamp on test builds (e.g. 0.1.0rc1); empty uses git" + description: + "Version to stamp on test builds (e.g. 0.1.0rc1); empty uses git" required: false default: "" @@ -146,7 +147,8 @@ jobs: matrix.arch }}-macos${{ env.MACOSX_DEPLOYMENT_TARGET }} - name: Build and Install Xerces-C - if: runner.os == 'macOS' && steps.cache-xerces.outputs.cache-hit != 'true' + if: + runner.os == 'macOS' && steps.cache-xerces.outputs.cache-hit != 'true' run: | git clone https://github.com/apache/xerces-c.git xerces-source git -C xerces-source checkout tags/v${{ env.XERCES_VERSION }} @@ -154,7 +156,8 @@ jobs: cmake --build xerces-build --parallel --config Release --target install - name: Save Xerces-C Installation - if: runner.os == 'macOS' && steps.cache-xerces.outputs.cache-hit != 'true' + if: + runner.os == 'macOS' && steps.cache-xerces.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: ${{ github.workspace }}/xerces @@ -167,12 +170,13 @@ jobs: with: path: ${{ env.Geant4_DIR }} key: - geant4-v${{ env.GEANT4_VERSION }}-shared-qt${{ env.QT_VERSION }}-pyqt5-qt5-${{ - env.PYQT5_QT5_VERSION }}-cpp${{ env.CPP_STANDARD }}-${{ matrix.arch - }}-macos${{ env.MACOSX_DEPLOYMENT_TARGET }} + geant4-v${{ env.GEANT4_VERSION }}-shared-qt${{ env.QT_VERSION + }}-pyqt5-qt5-${{ env.PYQT5_QT5_VERSION }}-cpp${{ env.CPP_STANDARD + }}-${{ matrix.arch }}-macos${{ env.MACOSX_DEPLOYMENT_TARGET }} - name: Build and Install Geant4 (shared, Qt viewer, without datasets) - if: runner.os == 'macOS' && steps.cache-geant4.outputs.cache-hit != 'true' + if: + runner.os == 'macOS' && steps.cache-geant4.outputs.cache-hit != 'true' run: | git clone https://github.com/Geant4/geant4.git geant4-source --depth 1 --branch v${{ env.GEANT4_VERSION }} # Typo in Geant4 11.2.2's g4tools (fixed in later releases) that Xcode 16.3+ clang rejects. @@ -182,7 +186,8 @@ jobs: rm -rf geant4-source geant4-build - name: Save Geant4 Installation - if: runner.os == 'macOS' && steps.cache-geant4.outputs.cache-hit != 'true' + if: + runner.os == 'macOS' && steps.cache-geant4.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: ${{ env.Geant4_DIR }} @@ -215,13 +220,14 @@ jobs: CIBW_ENVIRONMENT_MACOS: > MACOSX_DEPLOYMENT_TARGET=${{ env.MACOSX_DEPLOYMENT_TARGET }} - Geant4_DIR=${{ env.Geant4_DIR }} - CMAKE_PREFIX_PATH=${{ env.QT_PREFIX }} - GEANT4_PYTHON_APPLICATION_DIR=${{ runner.temp }}/geant4-data - ${{ inputs.version && format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', inputs.version) || '' }} + Geant4_DIR=${{ env.Geant4_DIR }} CMAKE_PREFIX_PATH=${{ env.QT_PREFIX + }} GEANT4_PYTHON_APPLICATION_DIR=${{ runner.temp }}/geant4-data ${{ + inputs.version && format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', + inputs.version) || '' }} CIBW_ENVIRONMENT_LINUX: > - GEANT4_PYTHON_APPLICATION_DIR=/tmp/geant4-data - ${{ inputs.version && format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', inputs.version) || '' }} + GEANT4_PYTHON_APPLICATION_DIR=/tmp/geant4-data ${{ inputs.version && + format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', inputs.version) || '' + }} CIBW_BEFORE_ALL_LINUX: | set -ex @@ -242,27 +248,28 @@ jobs: # Bundle Geant4 into the wheel but leave Qt out; tools/relink_qt_wheel.py # then points the Qt-linked libraries at the PyQt5-Qt5 package. CIBW_REPAIR_WHEEL_COMMAND_MACOS: > - rm -rf /tmp/delocated && - delocate-wheel --require-archs {delocate_archs} --exclude .framework/Versions/5/Qt -w /tmp/delocated -v {wheel} && - python -m pip install wheel && - python ${{ github.workspace }}/tools/relink_qt_wheel.py /tmp/delocated/*.whl -w {dest_dir} + rm -rf /tmp/delocated && delocate-wheel --require-archs + {delocate_archs} --exclude .framework/Versions/5/Qt -w + /tmp/delocated -v {wheel} && python -m pip install wheel && python + ${{ github.workspace }}/tools/relink_qt_wheel.py + /tmp/delocated/*.whl -w {dest_dir} CIBW_REPAIR_WHEEL_COMMAND_LINUX: > - rm -rf /tmp/repaired && - auditwheel repair -w /tmp/repaired {wheel} - --exclude libQt5Core.so.5 --exclude libQt5Gui.so.5 --exclude libQt5Widgets.so.5 - --exclude libQt5OpenGL.so.5 --exclude libQt5PrintSupport.so.5 --exclude libQt5DBus.so.5 && - python -m pip install wheel && - python /project/tools/relink_qt_wheel.py /tmp/repaired/*.whl -w {dest_dir} + rm -rf /tmp/repaired && auditwheel repair -w /tmp/repaired {wheel} + --exclude libQt5Core.so.5 --exclude libQt5Gui.so.5 --exclude + libQt5Widgets.so.5 --exclude libQt5OpenGL.so.5 --exclude + libQt5PrintSupport.so.5 --exclude libQt5DBus.so.5 && python -m pip + install wheel && python /project/tools/relink_qt_wheel.py + /tmp/repaired/*.whl -w {dest_dir} # Runtime libraries PyQt5-Qt5 expects from the system, plus a virtual display. CIBW_BEFORE_TEST_LINUX: > - dnf install -y xorg-x11-server-Xvfb mesa-dri-drivers mesa-libGL fontconfig freetype - libxkbcommon libxkbcommon-x11 xcb-util-wm xcb-util-image xcb-util-keysyms - xcb-util-renderutil dbus-libs + dnf install -y xorg-x11-server-Xvfb mesa-dri-drivers mesa-libGL + fontconfig freetype libxkbcommon libxkbcommon-x11 xcb-util-wm + xcb-util-image xcb-util-keysyms xcb-util-renderutil dbus-libs CIBW_TEST_COMMAND_MACOS: > python -m pytest {project}/tests -vv --reruns 10 --reruns-delay 30 - --only-rerun "(?i)http|timeout|connection|socket|resolve" && - python {project}/tools/smoke_viewer.py + --only-rerun "(?i)http|timeout|connection|socket|resolve" && python + {project}/tools/smoke_viewer.py CIBW_TEST_COMMAND_LINUX: > python -m pytest {project}/tests -vv --reruns 10 --reruns-delay 30 --only-rerun "(?i)http|timeout|connection|socket|resolve" && @@ -329,8 +336,8 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: > - gh release upload "${{ github.event.release.tag_name }}" dist/* - --repo "${{ github.repository }}" --clobber + gh release upload "${{ github.event.release.tag_name }}" dist/* --repo + "${{ github.repository }}" --clobber publish-docker: name: Publish Docker image diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e8c97c6..6fb9cd01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,8 +17,10 @@ project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) set(PYTHON_MODULE_NAME _geant4_application) set(VIS_MODULE_NAME _geant4_vis) -option(GEANT4_PYTHON_APPLICATION_VISUALIZATION - "Build the interactive Geant4 Qt viewer as a separate, lazily imported module" OFF) +option( + GEANT4_PYTHON_APPLICATION_VISUALIZATION + "Build the interactive Geant4 Qt viewer as a separate, lazily imported module" + OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -31,12 +33,12 @@ find_package( if (GEANT4_PYTHON_APPLICATION_VISUALIZATION) # The viewer module and the physics module must share one Geant4 (its # managers are singletons), which only works with shared libraries. - find_package( - Geant4 11.2.2 CONFIG REQUIRED - COMPONENTS gdml ui_all vis_all) + find_package(Geant4 11.2.2 CONFIG REQUIRED COMPONENTS gdml ui_all vis_all) if (NOT Geant4_shared_FOUND) - message(FATAL_ERROR - "GEANT4_PYTHON_APPLICATION_VISUALIZATION requires Geant4 built with shared libraries") + message( + FATAL_ERROR + "GEANT4_PYTHON_APPLICATION_VISUALIZATION requires Geant4 built with shared libraries" + ) endif () else () # "static" is optional: Geant4Config fails if it is required but only shared @@ -80,11 +82,23 @@ endif () # visualization drivers, UI sessions and vis manager). The physics module must # not link any of them, so that importing the package never loads Qt. set(GEANT4_QT_LIBRARIES - G4OpenGL G4ToolsSG G4interfaces G4visQt3D G4vis_management G4FR G4GMocren - G4RayTracer G4Tree G4VRML G4visHepRep G4OpenInventor G4visVtk) + G4OpenGL + G4ToolsSG + G4interfaces + G4visQt3D + G4vis_management + G4FR + G4GMocren + G4RayTracer + G4Tree + G4VRML + G4visHepRep + G4OpenInventor + G4visVtk) set(GEANT4_PHYSICS_LIBRARIES ${Geant4_LIBRARIES}) foreach (lib IN LISTS GEANT4_QT_LIBRARIES) - list(REMOVE_ITEM GEANT4_PHYSICS_LIBRARIES Geant4::${lib} Geant4::${lib}-static) + list(REMOVE_ITEM GEANT4_PHYSICS_LIBRARIES Geant4::${lib} + Geant4::${lib}-static) endforeach () file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS @@ -125,7 +139,8 @@ if (GEANT4_PYTHON_APPLICATION_VISUALIZATION) # The OpenGL Qt viewer (/vis/open OGL) is unaffected. set(GEANT4_VIS_LIBRARIES ${Geant4_LIBRARIES}) list(REMOVE_ITEM GEANT4_VIS_LIBRARIES Geant4::G4visQt3D) - target_include_directories(${VIS_MODULE_NAME} PRIVATE ${Geant4_INCLUDE_DIRS}) + target_include_directories(${VIS_MODULE_NAME} + PRIVATE ${Geant4_INCLUDE_DIRS}) target_link_libraries(${VIS_MODULE_NAME} PRIVATE ${GEANT4_VIS_LIBRARIES} pybind11::headers) # Homebrew may expose Qt 6 from its global include directory while this @@ -133,16 +148,16 @@ if (GEANT4_PYTHON_APPLICATION_VISUALIZATION) # first so framework headers do not accidentally include their Qt 6 peers. if (APPLE AND DEFINED Qt5Core_DIR) get_filename_component(QT5_PREFIX "${Qt5Core_DIR}/../../.." ABSOLUTE) - target_include_directories(${VIS_MODULE_NAME} SYSTEM BEFORE PRIVATE - "${QT5_PREFIX}/include") + target_include_directories(${VIS_MODULE_NAME} SYSTEM BEFORE + PRIVATE "${QT5_PREFIX}/include") endif () list(APPEND EXTENSION_MODULES ${VIS_MODULE_NAME}) endif () # Wheel repair tools (delocate/auditwheel) replace these with the bundled -# library locations; source installs keep finding Geant4 where it was built. -# Qt frameworks linked by @rpath (as when building against PyQt5-Qt5's Qt) -# also need their directory listed for delocate to resolve them. +# library locations; source installs keep finding Geant4 where it was built. Qt +# frameworks linked by @rpath (as when building against PyQt5-Qt5's Qt) also +# need their directory listed for delocate to resolve them. set(EXTENSION_RPATH "${GEANT4_PREFIX}/lib") if (APPLE AND DEFINED QT5_PREFIX) list(APPEND EXTENSION_RPATH "${QT5_PREFIX}/lib") @@ -156,7 +171,8 @@ foreach (module IN LISTS EXTENSION_MODULES) endif () set_target_properties( ${module} - PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${PROJECT_NAME} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY + ${PROJECT_BINARY_DIR}/${PROJECT_NAME} CXX_VISIBILITY_PRESET "hidden" CUDA_VISIBILITY_PRESET "hidden" BUILD_RPATH "${EXTENSION_RPATH}" diff --git a/INSTALLATION.md b/INSTALLATION.md index 0cbbd472..5b321f8b 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,12 +4,12 @@ Prebuilt wheels bundle a compiled Geant4 11.2.2, so on supported platforms you **do not need to install Geant4 (C++) yourself**. Building from source still requires a Geant4 installation. -| Platform | Python | Prebuilt wheel | Qt viewer (`[gui]`) | -| --------------------- | ----------- | -------------- | ------------------- | -| macOS, Apple Silicon | 3.8 – 3.13 | yes | yes | -| macOS, Intel | 3.8 – 3.13 | yes | yes | -| Linux x86_64 | 3.8 – 3.13 | yes | yes (needs a desktop with OpenGL) | -| Linux aarch64, Windows | — | no | build from source | +| Platform | Python | Prebuilt wheel | Qt viewer (`[gui]`) | +| ---------------------- | ---------- | -------------- | --------------------------------- | +| macOS, Apple Silicon | 3.8 – 3.13 | yes | yes | +| macOS, Intel | 3.8 – 3.13 | yes | yes | +| Linux x86_64 | 3.8 – 3.13 | yes | yes (needs a desktop with OpenGL) | +| Linux aarch64, Windows | — | no | build from source | ## Install a prebuilt wheel @@ -27,7 +27,8 @@ pip install geant4-python-application ### From a GitHub release -Each [GitHub release](https://github.com/lobis/geant4-python-application/releases) +Each +[GitHub release](https://github.com/lobis/geant4-python-application/releases) from 0.1.0 on also has the wheels attached. pip picks the right wheel for your platform and Python version; replace `` with the release tag (e.g. `v0.1.0`): @@ -41,8 +42,9 @@ pip install "geant4-python-application[gui]" \ To try changes that are not released yet, use the wheels that each run of the [Wheels workflow](https://github.com/lobis/geant4-python-application/actions/workflows/wheels.yaml) -keeps as downloadable artifacts for 90 days. Pick a successful run and download the artifact for your platform: `cibw-wheels-ubuntu-latest-x86_64` -(Linux), `cibw-wheels-macos-15-arm64` (Apple Silicon) or +keeps as downloadable artifacts for 90 days. Pick a successful run and download +the artifact for your platform: `cibw-wheels-ubuntu-latest-x86_64` (Linux), +`cibw-wheels-macos-15-arm64` (Apple Silicon) or `cibw-wheels-macos-15-intel-x86_64` (Intel Mac). From the run page this needs a GitHub login; with the [GitHub CLI](https://cli.github.com) (`gh auth login` first): @@ -53,14 +55,14 @@ gh run download --repo lobis/geant4-python-application \ pip install "geant4-python-application[gui]" --find-links ./g4-wheels ``` -`--find-links` takes the package from the folder and its dependencies -(including Qt for `[gui]`) from PyPI. +`--find-links` takes the package from the folder and its dependencies (including +Qt for `[gui]`) from PyPI. ### What the `gui` extra does The interactive viewer is a separate module that only `Application.visualize()` -loads. Importing the package and running simulations never loads Qt, so -headless machines, containers and clusters need neither Qt nor OpenGL. +loads. Importing the package and running simulations never loads Qt, so headless +machines, containers and clusters need neither Qt nor OpenGL. The `gui` extra installs Qt from the `PyQt5-Qt5` package; no Homebrew, conda or system Qt is needed. On Linux, Qt still relies on the system's OpenGL and X11 @@ -170,8 +172,8 @@ pip install . -Ccmake.define.GEANT4_PYTHON_APPLICATION_VISUALIZATION=ON ### Building a relocatable macOS wheel yourself -Bundles Geant4, Xerces-C, Expat and other non-system libraries into a wheel -that takes Qt from `PyQt5-Qt5`, like the published wheels: +Bundles Geant4, Xerces-C, Expat and other non-system libraries into a wheel that +takes Qt from `PyQt5-Qt5`, like the published wheels: ```bash PYTHON="$CONDA_PREFIX/bin/python" \ @@ -180,8 +182,8 @@ tools/build_macos_self_contained.sh pip install "$(ls dist/self-contained/*.whl)[gui]" ``` -The wheel only installs on the macOS version (and newer) that the bundled -Geant4 was built for. The published wheels are built by +The wheel only installs on the macOS version (and newer) that the bundled Geant4 +was built for. The published wheels are built by `.github/workflows/wheels.yaml`, targeting macOS 11 and manylinux_2_28. ## Data files @@ -195,18 +197,18 @@ versions, in which case those are used. python -c "import geant4_python_application as g4; print(g4.data_directory())" ``` -- Default location, shared by all your Python environments so the data - downloads only once: `~/Library/Application Support/geant4_python_application` - on macOS, `~/.local/share/geant4_python_application` on Linux. +- Default location, shared by all your Python environments so the data downloads + only once: `~/Library/Application Support/geant4_python_application` on macOS, + `~/.local/share/geant4_python_application` on Linux. - Override the persistent location by setting `GEANT4_PYTHON_APPLICATION_DIR` - before import, or calling `application_directory(path)` before the - application initializes. -- Use a temporary directory instead: `application_directory(temp=True)`. If - the OS partially cleans it up mid-run, delete it manually and let the - application recreate it. + before import, or calling `application_directory(path)` before the application + initializes. +- Use a temporary directory instead: `application_directory(temp=True)`. If the + OS partially cleans it up mid-run, delete it manually and let the application + recreate it. - Interrupted downloads resume, failed downloads raise an error, and each archive's MD5 checksum is verified before it is extracted. - Uninstalling the package does **not** remove downloaded data files — remove them manually. -- For cluster batch jobs, point `application_directory` at a shared location - to avoid redundant downloads across jobs. +- For cluster batch jobs, point `application_directory` at a shared location to + avoid redundant downloads across jobs. diff --git a/PYTHON_DEVELOPER_GUIDE.md b/PYTHON_DEVELOPER_GUIDE.md index 2f4ad057..3cdc6892 100644 --- a/PYTHON_DEVELOPER_GUIDE.md +++ b/PYTHON_DEVELOPER_GUIDE.md @@ -5,10 +5,9 @@ shows how to build small simulations in Python. It is intentionally practical: the Python package provides a high-level application, while the Geant4 kernel does the particle transport in native C++. -It is not a complete replacement for the official Geant4 Application -Developer Guide. Use that guide for detailed physics, geometry, and toolkit -reference. Use this guide for the Python interface and this repository's -supported workflow. +It is not a complete replacement for the official Geant4 Application Developer +Guide. Use that guide for detailed physics, geometry, and toolkit reference. Use +this guide for the Python interface and this repository's supported workflow. ## 1. The simulation idea @@ -21,9 +20,9 @@ A Geant4 simulation normally has these pieces: 4. **Run and event actions**: code that runs at run/event boundaries. 5. **Scoring**: the quantities extracted from steps, tracks, or volumes. -In this Python package, geometry is supplied as GDML, physics and generators -are configured through `Application`, and the built-in native actions record -events, tracks, and steps into an Awkward Array. +In this Python package, geometry is supplied as GDML, physics and generators are +configured through `Application`, and the built-in native actions record events, +tracks, and steps into an Awkward Array. The normal lifecycle is: @@ -77,8 +76,8 @@ different number of tracks, steps, and primary particles. ## 4. Choosing the physics list -A physics list selects the electromagnetic, decay, hadronic, optical, and -other processes used during transport: +A physics list selects the electromagnetic, decay, hadronic, optical, and other +processes used during transport: ```python import geant4_python_application as g4 @@ -116,12 +115,14 @@ The command interface exposes ordinary Geant4 UI commands: ```python with g4.Application(gdml=g4.basic_gdml) as app: - app.commands([ - "/gun/particle e-", - "/gun/energy 10 MeV", - "/gun/position 0 0 -10 cm", - "/gun/direction 0 0 1", - ]) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 10 MeV", + "/gun/position 0 0 -10 cm", + "/gun/direction 0 0 1", + ] + ) events = app.run(100) ``` @@ -131,8 +132,8 @@ For advanced GPS distributions, use `/gps/...` commands through ## 6. Geometry with GDML This package does not expose arbitrary Python subclasses of -`G4VUserDetectorConstruction`. Instead, provide a GDML string or file. A -minimal GDML detector has: +`G4VUserDetectorConstruction`. Instead, provide a GDML string or file. A minimal +GDML detector has: - `` constants - `` or references to Geant4 NIST materials @@ -170,8 +171,8 @@ with g4.Application(gdml=gdml) as app: app.detector.check_overlaps() ``` -The exact names used for scoring and sensitive volumes are the GDML physical -or logical names, so inspect them rather than guessing. +The exact names used for scoring and sensitive volumes are the GDML physical or +logical names, so inspect them rather than guessing. ## 7. Fields and sensitive volumes @@ -336,10 +337,10 @@ with g4.Application(gdml=g4.basic_gdml, seed=1234) as app: events = app.run(1000) ``` -Results can still vary with different Geant4 versions, physics lists, -thread counts, platforms, and random-engine behavior. Validate a simulation by -checking geometry overlaps, inspecting particle/energy distributions, and -running enough events for the statistical uncertainty to be meaningful. +Results can still vary with different Geant4 versions, physics lists, thread +counts, platforms, and random-engine behavior. Validate a simulation by checking +geometry overlaps, inspecting particle/energy distributions, and running enough +events for the statistical uncertainty to be meaningful. ## 14. What this interface does not currently expose @@ -372,20 +373,20 @@ these topics and are useful as runnable companion exercises. ## 16. The application state machine The Python `Application` is a thin controller around a native Geant4 -application. It starts a child process, sends configuration calls to the -native application, and returns data to the parent Python process. This design -keeps the Geant4 kernel isolated and means that configuration must happen in -the order expected by Geant4. +application. It starts a child process, sends configuration calls to the native +application, and returns data to the parent Python process. This design keeps +the Geant4 kernel isolated and means that configuration must happen in the order +expected by Geant4. The useful states are: -| State | What you can do | -| --- | --- | -| Created | Set Python constructor options such as GDML, physics, seed, and thread count. | -| Started | Configure the manager, physics, detector, and generator. | -| Initialized | Inspect the initialized detector and run events. | -| Running | Geant4 transports events and records native actions. | -| Finished | Analyze or persist the returned Awkward Array in Python. | +| State | What you can do | +| ----------- | ----------------------------------------------------------------------------- | +| Created | Set Python constructor options such as GDML, physics, seed, and thread count. | +| Started | Configure the manager, physics, detector, and generator. | +| Initialized | Inspect the initialized detector and run events. | +| Running | Geant4 transports events and records native actions. | +| Finished | Analyze or persist the returned Awkward Array in Python. | Most configuration must occur before the first call to `run`. For example, physics constructors cannot be added after initialization: @@ -421,10 +422,17 @@ def main(): seed=2025, n_threads=4, ) as app: - app.set_event_fields({ - "run", "id", "track_id", "track_particle", - "step_energy", "step_volume", "step_position", - }) + app.set_event_fields( + { + "run", + "id", + "track_id", + "track_particle", + "step_energy", + "step_volume", + "step_position", + } + ) app.generator.particle("gamma").energy(2, "MeV") app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) app.detector.sensitive_volumes = {"box"} @@ -481,9 +489,9 @@ The hierarchy is: solid -> logical volume -> physical volume -> world ``` -A solid describes shape and dimensions. A logical volume combines a solid and -a material. A physical volume places a logical volume inside its mother. The -world is the top-level physical geometry used by the setup. +A solid describes shape and dimensions. A logical volume combines a solid and a +material. A physical volume places a logical volume inside its mother. The world +is the top-level physical geometry used by the setup. ### 19.2 A small custom detector @@ -543,10 +551,10 @@ volume, and units first. ## 20. Materials and optical properties -The repository's GDML examples use Geant4 NIST material names such as -`G4_AIR`, `G4_WATER`, and `G4_Si`. This is the simplest way to begin. For a -custom material, define its density and composition in GDML according to the -Geant4 GDML schema. +The repository's GDML examples use Geant4 NIST material names such as `G4_AIR`, +`G4_WATER`, and `G4_Si`. This is the simplest way to begin. For a custom +material, define its density and composition in GDML according to the Geant4 +GDML schema. Optical transport needs both optical physics and optical material properties. For example, a material that should transmit photons needs a refractive index @@ -585,14 +593,16 @@ Use GPS commands for distributions in position, angle, or energy: ```python with g4.Application(gdml=g4.basic_gdml) as app: app.generator.use_gps() - app.generator.commands([ - "/gps/particle gamma", - "/gps/ene/type Mono", - "/gps/ene/mono 1 MeV", - "/gps/pos/type Point", - "/gps/pos/centre 0 0 -10 cm", - "/gps/direction 0 0 1", - ]) + app.generator.commands( + [ + "/gps/particle gamma", + "/gps/ene/type Mono", + "/gps/ene/mono 1 MeV", + "/gps/pos/type Point", + "/gps/pos/centre 0 0 -10 cm", + "/gps/direction 0 0 1", + ] + ) events = app.run(100) ``` @@ -604,12 +614,14 @@ is useful when the source comes from a file or another simulation: ```python import awkward as ak -primaries = ak.Array({ - "particle": ["gamma", "gamma", "e-"], - "energy": [1.0, 2.0, 5.0], - "position": {"x": [0, 0, 0], "y": [0, 0, 0], "z": [-10, -10, -10]}, - "direction": {"x": [0, 0, 0], "y": [0, 0, 0], "z": [1, 1, 1]}, -}) +primaries = ak.Array( + { + "particle": ["gamma", "gamma", "e-"], + "energy": [1.0, 2.0, 5.0], + "position": {"x": [0, 0, 0], "y": [0, 0, 0], "z": [-10, -10, -10]}, + "direction": {"x": [0, 0, 0], "y": [0, 0, 0], "z": [1, 1, 1]}, + } +) with g4.Application(gdml=g4.basic_gdml) as app: events = app.run(primaries) @@ -621,8 +633,8 @@ documents limitations for this input path in multithreaded runs. ## 22. Physics choices and statistical thinking A simulation result is determined by geometry, materials, source, physics list, -production cuts, random seed, and event count. Changing any of these changes -the result or its interpretation. +production cuts, random seed, and event count. Changing any of these changes the +result or its interpretation. Use a small run for functional validation, then a larger run for physics: @@ -730,8 +742,8 @@ with g4.Application(gdml=g4.basic_gdml, n_threads=8) as app: ``` The linked Geant4 library must have been compiled with multithreading. More -threads are not always faster: geometry initialization, memory pressure, -output construction, and CPU oversubscription can reduce the gain. +threads are not always faster: geometry initialization, memory pressure, output +construction, and CPU oversubscription can reduce the gain. Benchmark with the same event count and seed policy. Validate event counts and aggregate quantities, but do not require event ordering to be identical across @@ -807,8 +819,8 @@ def test_smoke(): and verify the environment used by Python is the same environment used to install Geant4. -**`n_threads` fails.** Check `g4.Application.multithreading_available()` and -use `n_threads=0` with a serial Geant4 build. +**`n_threads` fails.** Check `g4.Application.multithreading_available()` and use +`n_threads=0` with a serial Geant4 build. **A volume cannot be found.** Print `app.detector.logical_volumes` and `app.detector.physical_volumes`; GDML names are case-sensitive. @@ -821,37 +833,37 @@ physical volume name. mother volume dimensions, placement coordinates, and units. **The run is unexpectedly slow.** Reduce fields, disable verbose output, use a -smaller diagnostic run, and benchmark thread counts. Optical photon tracking -can be substantially more expensive than the primary transport. +smaller diagnostic run, and benchmark thread counts. Optical photon tracking can +be substantially more expensive than the primary transport. ## 33. Mapping native Geant4 concepts to this Python interface -| Native Geant4 concept | Python-interface equivalent | -| --- | --- | -| `G4RunManager` | `Application` and its native manager setup | -| `G4VUserDetectorConstruction` | GDML supplied to `Application` | -| `G4VUserPhysicsList` | `physics="..."` and curated `add_physics(...)` | -| `G4VUserPrimaryGeneratorAction` | `app.generator` or `/gun` and `/gps` commands | -| `G4RunManager::BeamOn` | `app.run(n_events)` | +| Native Geant4 concept | Python-interface equivalent | +| ------------------------------------------------ | ------------------------------------------------------- | +| `G4RunManager` | `Application` and its native manager setup | +| `G4VUserDetectorConstruction` | GDML supplied to `Application` | +| `G4VUserPhysicsList` | `physics="..."` and curated `add_physics(...)` | +| `G4VUserPrimaryGeneratorAction` | `app.generator` or `/gun` and `/gps` commands | +| `G4RunManager::BeamOn` | `app.run(n_events)` | | `G4UserRunAction` / event / track / step actions | Built-in native recording plus offline Python callbacks | -| Sensitive detector / hits | Recorded steps, `events.hits()`, and Python scoring | -| Command-based scoring | `NativeScoringMesh` | -| Geant4 visualization manager | `app.visualize()` with Qt/OpenGL build support | -| Geant4 MT run manager | `n_threads=N` | -| MPI job splitting | `distributed.get_rank_size()` and `split_count()` | +| Sensitive detector / hits | Recorded steps, `events.hits()`, and Python scoring | +| Command-based scoring | `NativeScoringMesh` | +| Geant4 visualization manager | `app.visualize()` with Qt/OpenGL build support | +| Geant4 MT run manager | `n_threads=N` | +| MPI job splitting | `distributed.get_rank_size()` and `split_count()` | ## 34. Scope boundary This guide is deliberately detailed about the supported interface, but the interface is not a one-to-one Python binding of the entire Geant4 class library. If your project requires custom C++ user actions, arbitrary physics processes, -custom field equations, native digitizers, parallel worlds, advanced biasing, -or CUDA transport, you will need native Geant4 C++ or a lower-level binding. +custom field equations, native digitizers, parallel worlds, advanced biasing, or +CUDA transport, you will need native Geant4 C++ or a lower-level binding. That boundary is useful: use this package when a GDML detector, configured physics list, standard generator, native transport, and Python analysis are a -good fit. Move to native C++ when transport-time customization is central to -the experiment. +good fit. Move to native C++ when transport-time customization is central to the +experiment. ## 35. Suggested projects @@ -874,9 +886,9 @@ field, scoring definition, event count, seed policy, and validation checks. ## 36. How to read this book This book is organized as a progression rather than a catalogue of APIs. Read -the first five chapters in order. After that, choose a project and return to -the relevant chapters while implementing it. Every serious simulation should -be understandable in four layers: +the first five chapters in order. After that, choose a project and return to the +relevant chapters while implementing it. Every serious simulation should be +understandable in four layers: 1. **Physical question** - what quantity are you trying to estimate? 2. **Monte Carlo model** - what particles, materials, processes, and geometry @@ -887,8 +899,8 @@ be understandable in four layers: Do not begin by copying a large detector. Begin with a small experiment for which you can predict the direction of the answer. A simulation becomes useful -when it is falsifiable: you should know what result would convince you that -the geometry, physics, or source is wrong. +when it is falsifiable: you should know what result would convince you that the +geometry, physics, or source is wrong. ## 37. Monte Carlo foundations @@ -944,16 +956,25 @@ import geant4_python_application as g4 def run_experiment(n_events=1000): with g4.Application(gdml=g4.basic_gdml, seed=42) as app: - app.set_event_fields({ - "run", "id", "track_particle", "track_id", - "step_energy", "step_volume", "step_position", - }) - app.commands([ - "/gun/particle e-", - "/gun/energy 1 MeV", - "/gun/position 0 0 -300 mm", - "/gun/direction 0 0 1", - ]) + app.set_event_fields( + { + "run", + "id", + "track_particle", + "track_id", + "step_energy", + "step_volume", + "step_position", + } + ) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 1 MeV", + "/gun/position 0 0 -300 mm", + "/gun/direction 0 0 1", + ] + ) return app.run(n_events) @@ -1031,22 +1052,22 @@ For each custom material, document: - the version of the data used. Do not extrapolate optical tables silently beyond their defined domain. If a -simulation depends on a material parameter, include a validation plot or a -small numerical check in the project repository. +simulation depends on a material parameter, include a validation plot or a small +numerical check in the project repository. ## 41. Selecting a physics list responsibly -A reference physics list is a coherent collection of models and processes. It -is usually safer than assembling individual processes without understanding -their interactions. Select a list based on particle types, energy range, -target materials, and required precision. +A reference physics list is a coherent collection of models and processes. It is +usually safer than assembling individual processes without understanding their +interactions. Select a list based on particle types, energy range, target +materials, and required precision. Use this decision process: 1. Identify all primary and likely secondary particle species. 2. Identify the lowest and highest relevant energies. -3. Identify whether electromagnetic, hadronic, decay, optical, or DNA physics - is required. +3. Identify whether electromagnetic, hadronic, decay, optical, or DNA physics is + required. 4. Choose a documented reference list that covers the domain. 5. Run a small sensitivity comparison against another plausible list. 6. Record the choice and rationale. @@ -1137,21 +1158,21 @@ Validation has three distinct meanings: 1. **Software validation**: does the program run and produce structurally valid output? -2. **Physics verification**: does the implementation reproduce a known result - or limiting case? +2. **Physics verification**: does the implementation reproduce a known result or + limiting case? 3. **Experimental validation**: does the simulation agree with measured data within the combined uncertainty? For every new detector, create a validation table: -| Test | Expected behavior | Result | Status | -| --- | --- | --- | --- | -| Geometry load | All named volumes exist | ... | pass/fail | -| Overlap check | No overlaps in target region | ... | pass/fail | -| Empty field | Straight neutral-particle path | ... | pass/fail | -| Energy conservation | Balance within tolerance | ... | pass/fail | -| Physics comparison | Agreement with reference | ... | pass/fail | -| Thread comparison | Aggregate agreement | ... | pass/fail | +| Test | Expected behavior | Result | Status | +| ------------------- | ------------------------------ | ------ | --------- | +| Geometry load | All named volumes exist | ... | pass/fail | +| Overlap check | No overlaps in target region | ... | pass/fail | +| Empty field | Straight neutral-particle path | ... | pass/fail | +| Energy conservation | Balance within tolerance | ... | pass/fail | +| Physics comparison | Agreement with reference | ... | pass/fail | +| Thread comparison | Aggregate agreement | ... | pass/fail | Keep failed tests visible. A simulation project becomes trustworthy when its failures are discoverable rather than hidden. @@ -1202,12 +1223,14 @@ def run_energy(energy_mev, n_events, seed): gdml = Path("detector.gdml").read_text() with g4.Application(gdml=gdml, physics="FTFP_BERT", seed=seed) as app: app.set_event_fields({"id", "step_energy", "step_volume"}) - app.commands([ - "/gun/particle gamma", - f"/gun/energy {energy_mev} MeV", - "/gun/position 0 0 -300 mm", - "/gun/direction 0 0 1", - ]) + app.commands( + [ + "/gun/particle gamma", + f"/gun/energy {energy_mev} MeV", + "/gun/position 0 0 -300 mm", + "/gun/direction 0 0 1", + ] + ) events = app.run(n_events) g4.io.events_to_parquet(events, f"results/events_{energy_mev}MeV.parquet") return g4.Scoring.energy_deposit(events, volume="target") @@ -1450,13 +1473,13 @@ python run_simulation.py --events 100000 --energy 5 --threads 8 python run_simulation.py --gdml detector.gdml --output results/run.parquet ``` -Command-line arguments turn a script into a reproducible experiment: the -command itself records the most important choices. +Command-line arguments turn a script into a reproducible experiment: the command +itself records the most important choices. ## 55. Write the analysis program separately -Do not rerun Geant4 just to change a plot or a summary statistic. Save the -event data and write a separate analysis program: +Do not rerun Geant4 just to change a plot or a summary statistic. Save the event +data and write a separate analysis program: ```python from pathlib import Path @@ -1507,8 +1530,8 @@ def load_detector(config: DetectorConfig) -> str: return text ``` -Validate the configuration before starting a long run. Fail early when a file -is missing, a volume name is wrong, or the event count is negative. +Validate the configuration before starting a long run. Fail early when a file is +missing, a volume name is wrong, or the event count is negative. ## 57. Write tests before scaling up @@ -1560,10 +1583,17 @@ def build_parser(): def configure(app): - app.set_event_fields({ - "run", "id", "primaries", "track_particle", - "track_creator_process", "step_energy", "step_volume", - }) + app.set_event_fields( + { + "run", + "id", + "primaries", + "track_particle", + "track_creator_process", + "step_energy", + "step_volume", + } + ) app.generator.use_gps() app.generator.particle("gamma").energy(2, "MeV") app.generator.position(0, 0, -10, "cm").direction(0, 0, 1) diff --git a/README.md b/README.md index 5a7cd97e..f5cc690e 100644 --- a/README.md +++ b/README.md @@ -28,24 +28,25 @@ New to Geant4 or this package? See the ## Platform support -Tested on macOS and Ubuntu; still undergoing broader testing. If you hit a -bug, please report it via [GitHub Issues](https://github.com/lobis/geant4-python-application/issues) -or reach out to kavyavadhwa@gmail.com. +Tested on macOS and Ubuntu; still undergoing broader testing. If you hit a bug, +please report it via +[GitHub Issues](https://github.com/lobis/geant4-python-application/issues) or +reach out to kavyavadhwa@gmail.com. ## Installation -Prebuilt wheels for macOS (Apple Silicon and Intel) and Linux x86_64, Python -3.8 – 3.13, bundle Geant4 — no separate Geant4 (C++) installation is needed: +Prebuilt wheels for macOS (Apple Silicon and Intel) and Linux x86_64, Python 3.8 +– 3.13, bundle Geant4 — no separate Geant4 (C++) installation is needed: ```bash pip install "geant4-python-application[gui]" ``` -The `gui` extra adds the interactive Qt viewer; leave it out for a -physics-only install. +The `gui` extra adds the interactive Qt viewer; leave it out for a physics-only +install. -Geant4's physics data files download automatically on first use, so you can -run an example straight away: +Geant4's physics data files download automatically on first use, so you can run +an example straight away: ```bash python examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py --batch -n 10 @@ -91,7 +92,8 @@ with g4.Application(gdml=g4.basic_gdml) as app: Advanced GPS distributions remain accessible through `app.generator.commands(["/gps/pos/type Plane", ...])`. -Optical physics and uniform global magnetic/electric fields can be enabled directly: +Optical physics and uniform global magnetic/electric fields can be enabled +directly: ```python with g4.Application(gdml=my_optical_gdml, optical=True) as app: @@ -122,8 +124,10 @@ Python run/event/track/step callbacks (offline over awkward arrays): ```python with g4.Application(gdml=g4.basic_gdml) as app: events = app.run_with_callbacks( - 100, on_run=lambda evs: print(len(evs)), - on_event=lambda ev: None, on_track=lambda tr, ev: None, + 100, + on_run=lambda evs: print(len(evs)), + on_event=lambda ev: None, + on_track=lambda tr, ev: None, on_step=lambda st, tr, ev: None, ) ``` @@ -133,7 +137,9 @@ Extra physics constructors incl. DNA, CAD/VTK/ROOT/HepMC, and MPI splitting: ```python print(g4.Application.available_extra_physics()) with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT") as app: - app.add_physics("G4EmDNAPhysics_option2") # or G4EmExtraPhysics, G4OpticalPhysics... + app.add_physics( + "G4EmDNAPhysics_option2" + ) # or G4EmExtraPhysics, G4OpticalPhysics... events = app.run(100) from geant4_python_application import cad, io as gio, distributed as dist diff --git a/docs/FEATURE_SUPPORT.md b/docs/FEATURE_SUPPORT.md index 70eb7699..ca830d30 100644 --- a/docs/FEATURE_SUPPORT.md +++ b/docs/FEATURE_SUPPORT.md @@ -4,25 +4,25 @@ This matrix distinguishes native Geant4 execution from Python adapters. A feature is not marked supported merely because a similarly named proof script exists. -| Capability | Status | Current implementation | -|---|---|---| -| Detector construction | Partial | Complete GDML strings/files; no Python subclass of `G4VUserDetectorConstruction` | -| Run/event/track/step actions | Partial | Native built-in C++ actions record events; user callbacks run after transport | -| Multiple primary vertices | Supported | Nested Awkward `primaries` records generate and preserve all vertices in each event | -| Sensitive detectors/hits | Partial | Logical volumes can be marked sensitive and all steps are recorded; no custom Python `ProcessHits` or native typed hit collection | -| Parameterised geometry | Partial | Expanded placements can be generated in Python/GDML; no live `G4VPVParameterisation` callback | -| Physics lists/processes | Partial | Reference lists and a curated set of `G4VPhysicsConstructor` classes; no arbitrary Python process/particle subclass | -| Fields/equations/steppers | Partial | Uniform global B and E with native Geant4 integration; no user equation/stepper binding | -| Optical materials/processes | Supported (GDML) | RINDEX, scintillation, WLS and optical surfaces execute natively; no Python PMT/SD callback | -| Repeated geometry | Adapter | Regular layouts can be expanded to ordinary GDML placements; no live parameterisation callback | -| Native scoring meshes | Supported (energy) | `NativeScoringMesh` drives `G4ScoringManager`; Python `Scoring` remains a separate post-run tool | -| MPI/distributed | Partial | `mpi4py` rank discovery and event splitting; no Geant4 MPI manager or automatic merge | -| DNA | Partial | DNA physics and chemistry constructors are selectable; chemistry scheduler/time-step actions are not exposed | -| ROOT | Adapter | `uproot` summary I/O, not Geant4 ROOT analysis classes | -| VTK | Adapter | Legacy VTK mesh export, not Geant4 VTK visualization classes | -| CAD | Adapter | ASCII STL/tessellated GDML, not STEP/IGES or a CAD kernel | -| Pythia/HepMC | Partial adapter | Minimal HepMC2 primary import; no Pythia native integration | -| DICOM/medical | Partial | GDML medical geometries and dose post-processing; no DICOM image/voxel pipeline | +| Capability | Status | Current implementation | +| ---------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| Detector construction | Partial | Complete GDML strings/files; no Python subclass of `G4VUserDetectorConstruction` | +| Run/event/track/step actions | Partial | Native built-in C++ actions record events; user callbacks run after transport | +| Multiple primary vertices | Supported | Nested Awkward `primaries` records generate and preserve all vertices in each event | +| Sensitive detectors/hits | Partial | Logical volumes can be marked sensitive and all steps are recorded; no custom Python `ProcessHits` or native typed hit collection | +| Parameterised geometry | Partial | Expanded placements can be generated in Python/GDML; no live `G4VPVParameterisation` callback | +| Physics lists/processes | Partial | Reference lists and a curated set of `G4VPhysicsConstructor` classes; no arbitrary Python process/particle subclass | +| Fields/equations/steppers | Partial | Uniform global B and E with native Geant4 integration; no user equation/stepper binding | +| Optical materials/processes | Supported (GDML) | RINDEX, scintillation, WLS and optical surfaces execute natively; no Python PMT/SD callback | +| Repeated geometry | Adapter | Regular layouts can be expanded to ordinary GDML placements; no live parameterisation callback | +| Native scoring meshes | Supported (energy) | `NativeScoringMesh` drives `G4ScoringManager`; Python `Scoring` remains a separate post-run tool | +| MPI/distributed | Partial | `mpi4py` rank discovery and event splitting; no Geant4 MPI manager or automatic merge | +| DNA | Partial | DNA physics and chemistry constructors are selectable; chemistry scheduler/time-step actions are not exposed | +| ROOT | Adapter | `uproot` summary I/O, not Geant4 ROOT analysis classes | +| VTK | Adapter | Legacy VTK mesh export, not Geant4 VTK visualization classes | +| CAD | Adapter | ASCII STL/tessellated GDML, not STEP/IGES or a CAD kernel | +| Pythia/HepMC | Partial adapter | Minimal HepMC2 primary import; no Pythia native integration | +| DICOM/medical | Partial | GDML medical geometries and dose post-processing; no DICOM image/voxel pipeline | ## Binding work still required diff --git a/examples/geant4-examples/00_qt_viewer_smoke.py b/examples/geant4-examples/00_qt_viewer_smoke.py index c03216e0..5150ba0d 100644 --- a/examples/geant4-examples/00_qt_viewer_smoke.py +++ b/examples/geant4-examples/00_qt_viewer_smoke.py @@ -9,6 +9,7 @@ command panel. Close the window to return (app.visualize blocks). Run: python 00_qt_viewer_smoke.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/01_reference_physics_lists.py b/examples/geant4-examples/01_reference_physics_lists.py index 930f2982..d2630ac9 100644 --- a/examples/geant4-examples/01_reference_physics_lists.py +++ b/examples/geant4-examples/01_reference_physics_lists.py @@ -4,6 +4,7 @@ and handling an unknown list name. Run: python 01_reference_physics_lists.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/02_gps_sources.py b/examples/geant4-examples/02_gps_sources.py index 17020635..f1834172 100644 --- a/examples/geant4-examples/02_gps_sources.py +++ b/examples/geant4-examples/02_gps_sources.py @@ -4,6 +4,7 @@ commands and awkward-array primaries. Run: python 02_gps_sources.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/03_scoring_meshes_hits.py b/examples/geant4-examples/03_scoring_meshes_hits.py index af76a60d..83c4b45a 100644 --- a/examples/geant4-examples/03_scoring_meshes_hits.py +++ b/examples/geant4-examples/03_scoring_meshes_hits.py @@ -4,6 +4,7 @@ Cartesian energy mesh (numpy histogramdd), and hit filtering. Run: python 03_scoring_meshes_hits.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/04_magnetic_electric_fields.py b/examples/geant4-examples/04_magnetic_electric_fields.py index eab0e4e7..60c45fbd 100644 --- a/examples/geant4-examples/04_magnetic_electric_fields.py +++ b/examples/geant4-examples/04_magnetic_electric_fields.py @@ -4,6 +4,7 @@ E uses G4EqMagElectricField + ClassicalRK4; B-only uses G4GlobalMagFieldMessenger. Run: python 04_magnetic_electric_fields.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/05_optical_scintillation.py b/examples/geant4-examples/05_optical_scintillation.py index a4edd276..0c52f0d0 100644 --- a/examples/geant4-examples/05_optical_scintillation.py +++ b/examples/geant4-examples/05_optical_scintillation.py @@ -4,6 +4,7 @@ Geant4 OpNovice) with G4OpticalPhysics. Verifies opticalphoton production. Run: python 05_optical_scintillation.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/06_callbacks.py b/examples/geant4-examples/06_callbacks.py index 43e7ff2d..e584a6c7 100644 --- a/examples/geant4-examples/06_callbacks.py +++ b/examples/geant4-examples/06_callbacks.py @@ -4,6 +4,7 @@ the awkward event model (on_run / on_event / on_track / on_step). Run: python 06_callbacks.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/07_custom_physics.py b/examples/geant4-examples/07_custom_physics.py index 23321e23..1a2d62ac 100644 --- a/examples/geant4-examples/07_custom_physics.py +++ b/examples/geant4-examples/07_custom_physics.py @@ -4,6 +4,7 @@ reference or custom list, before initialize/run. Run: python 07_custom_physics.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/08_dna_medical.py b/examples/geant4-examples/08_dna_medical.py index d0fd4967..a7a1dbb8 100644 --- a/examples/geant4-examples/08_dna_medical.py +++ b/examples/geant4-examples/08_dna_medical.py @@ -5,6 +5,7 @@ physical stage is run here. Run: python 08_dna_medical.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/09_cad_vtk_root_generators.py b/examples/geant4-examples/09_cad_vtk_root_generators.py index 5ae8770a..6708ed4c 100644 --- a/examples/geant4-examples/09_cad_vtk_root_generators.py +++ b/examples/geant4-examples/09_cad_vtk_root_generators.py @@ -4,6 +4,7 @@ parquet/ROOT, HepMC-ASCII -> primaries. Writes to /tmp. Run: python 09_cad_vtk_root_generators.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/10_mpi_distributed.py b/examples/geant4-examples/10_mpi_distributed.py index 3db25a6a..fdaa6ad1 100644 --- a/examples/geant4-examples/10_mpi_distributed.py +++ b/examples/geant4-examples/10_mpi_distributed.py @@ -6,6 +6,7 @@ Run serial: python 10_mpi_distributed.py Run MPI: mpiexec -n 2 python 10_mpi_distributed.py """ + from __future__ import annotations diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md b/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md index 50c6ebaa..89f59ff1 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/AUDIT.md @@ -6,35 +6,35 @@ - Python ports passing: 27 - Python ports failing/timing out: 0 -| Python port | Status | Seconds | -|---|---:|---:| -| `advanced/GoldFoilExperiment/gold_foil.py` | passed | 3.794 | -| `advanced/NanoparticleTherapy/brain_tumor.py` | passed | 4.751 | -| `advanced/NanoparticleTherapy/nano_therapy.py` | passed | 4.411 | -| `basic/B1/b1.py` | passed | 1.739 | -| `basic/B2/b2.py` | passed | 1.933 | -| `basic/B3/b3.py` | passed | 1.779 | -| `basic/B4/b4.py` | passed | 2.447 | -| `basic/B5/b5.py` | passed | 2.266 | -| `extended/analysis/AnaEx01/ana_ex01.py` | passed | 2.004 | -| `extended/analysis/AnaEx02/ana_ex02.py` | passed | 1.198 | -| `extended/analysis/AnaEx03/ana_ex03.py` | passed | 1.104 | -| `extended/electromagnetic/TestEm1/test_em1.py` | passed | 1.353 | -| `extended/eventgenerator/HepMC/hepmc.py` | passed | 1.063 | -| `extended/eventgenerator/exgps/exgps.py` | passed | 1.899 | -| `extended/eventgenerator/particleGun/particle_gun.py` | passed | 1.755 | -| `extended/field/field01/field01.py` | passed | 1.364 | -| `extended/field/field02/field02.py` | passed | 0.966 | -| `extended/hadronic/Hadr00/hadr00.py` | passed | 1.949 | -| `extended/medical/GammaTherapy/gamma_therapy.py` | passed | 1.333 | -| `extended/medical/electronScattering/electron_scattering.py` | passed | 1.794 | -| `extended/optical/LXe/lxe.py` | passed | 1.556 | -| `extended/optical/OpNovice/op_novice.py` | passed | 1.399 | -| `extended/optical/wls/wls.py` | passed | 1.367 | -| `extended/parameterisations/Par01/par01.py` | passed | 1.592 | -| `extended/physicslists/factory/factory.py` | passed | 1.134 | -| `extended/radioactivedecay/rdecay01/rdecay01.py` | passed | 1.167 | -| `extended/runAndEvent/RE01/re01.py` | passed | 1.376 | -| `run_gdml.py` | utility | | +| Python port | Status | Seconds | +| ------------------------------------------------------------ | ------: | ------: | +| `advanced/GoldFoilExperiment/gold_foil.py` | passed | 3.794 | +| `advanced/NanoparticleTherapy/brain_tumor.py` | passed | 4.751 | +| `advanced/NanoparticleTherapy/nano_therapy.py` | passed | 4.411 | +| `basic/B1/b1.py` | passed | 1.739 | +| `basic/B2/b2.py` | passed | 1.933 | +| `basic/B3/b3.py` | passed | 1.779 | +| `basic/B4/b4.py` | passed | 2.447 | +| `basic/B5/b5.py` | passed | 2.266 | +| `extended/analysis/AnaEx01/ana_ex01.py` | passed | 2.004 | +| `extended/analysis/AnaEx02/ana_ex02.py` | passed | 1.198 | +| `extended/analysis/AnaEx03/ana_ex03.py` | passed | 1.104 | +| `extended/electromagnetic/TestEm1/test_em1.py` | passed | 1.353 | +| `extended/eventgenerator/HepMC/hepmc.py` | passed | 1.063 | +| `extended/eventgenerator/exgps/exgps.py` | passed | 1.899 | +| `extended/eventgenerator/particleGun/particle_gun.py` | passed | 1.755 | +| `extended/field/field01/field01.py` | passed | 1.364 | +| `extended/field/field02/field02.py` | passed | 0.966 | +| `extended/hadronic/Hadr00/hadr00.py` | passed | 1.949 | +| `extended/medical/GammaTherapy/gamma_therapy.py` | passed | 1.333 | +| `extended/medical/electronScattering/electron_scattering.py` | passed | 1.794 | +| `extended/optical/LXe/lxe.py` | passed | 1.556 | +| `extended/optical/OpNovice/op_novice.py` | passed | 1.399 | +| `extended/optical/wls/wls.py` | passed | 1.367 | +| `extended/parameterisations/Par01/par01.py` | passed | 1.592 | +| `extended/physicslists/factory/factory.py` | passed | 1.134 | +| `extended/radioactivedecay/rdecay01/rdecay01.py` | passed | 1.167 | +| `extended/runAndEvent/RE01/re01.py` | passed | 1.376 | +| `run_gdml.py` | utility | | Only entries marked `passed` are validated as runnable Python ports. diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md b/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md index 5cab6a65..33b520c6 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/COMPATIBILITY.md @@ -1,55 +1,55 @@ # Compatibility with the official Geant4 examples -This directory contains Python adaptations, not mechanical translations of -the C++ sources. They exercise the same detector concept using functionality +This directory contains Python adaptations, not mechanical translations of the +C++ sources. They exercise the same detector concept using functionality currently exposed by `geant4_python_application`. ## Runnable adaptations -| Official family | Python entry point | Coverage | -|---|---|---| -| basic/B1 | `basic/B1/b1.py` | Geometry, beam, volume energy scoring | -| basic/B2 | `basic/B2/b2.py` | Target, tracker chambers, field, step-derived hits | -| basic/B3 | `basic/B3/b3.py` | PET geometry and crystal/patient energy scoring | -| basic/B4 | `basic/B4/b4.py` | Sampling calorimeter and layer energy scoring | -| basic/B5 | `basic/B5/b5.py` | Spectrometer geometry, field, detector energy scoring | -| GoldFoilExperiment | `advanced/GoldFoilExperiment/gold_foil.py` | Rutherford-scattering workflow and CSV output | -| NanoparticleTherapy | `advanced/NanoparticleTherapy/*.py` | Geometric/physics adaptations and dose summaries | -| extended/eventgenerator/exgps | `extended/eventgenerator/exgps/exgps.py` | Native GPS and primary-spectrum output | -| extended/eventgenerator/particleGun | `extended/eventgenerator/particleGun/particle_gun.py` | Python-sampled source modes and transport | -| extended/eventgenerator/HepMC | `extended/eventgenerator/HepMC/hepmc.py` | Minimal HepMC2 parser adapter; no native HepMC SDK | -| extended/field/field01 | `extended/field/field01/field01.py` | Uniform magnetic-field tracking study | -| extended/field/field02 | `extended/field/field02/field02.py` | Native uniform electric field and absorber scoring | -| extended/optical/OpNovice | `extended/optical/OpNovice/op_novice.py` | Native optical transport and photon counting | -| extended/optical/LXe | `extended/optical/LXe/lxe.py` | LXe scintillation/transport; post-run photon analysis replaces PMT hit callbacks | -| extended/optical/wls | `extended/optical/wls/wls.py` | Native WLS absorption/re-emission in a GDML fiber | -| extended/analysis/AnaEx01 | `extended/analysis/AnaEx01/ana_ex01.py` | Calorimeter energy analysis and CSV ntuple | -| extended/analysis/AnaEx02 | `extended/analysis/AnaEx02/ana_ex02.py` | Nested event persistence through Parquet | -| extended/analysis/AnaEx03 | `extended/analysis/AnaEx03/ana_ex03.py` | ROOT summary adapter through uproot | -| extended/electromagnetic/TestEm1 | `extended/electromagnetic/TestEm1/test_em1.py` | Electromagnetic process and energy-deposit survey | -| extended/hadronic/Hadr00 | `extended/hadronic/Hadr00/hadr00.py` | Hadronic target and secondary-particle survey | -| extended/medical/GammaTherapy | `extended/medical/GammaTherapy/gamma_therapy.py` | Simplified therapy geometry with native energy mesh | -| extended/medical/electronScattering | `extended/medical/electronScattering/electron_scattering.py` | Simplified foil-scattering benchmark | -| extended/parameterisations/Par01 | `extended/parameterisations/Par01/par01.py` | Static parameterisation expanded to GDML placements | -| extended/physicslists/factory | `extended/physicslists/factory/factory.py` | Runtime selection from native Geant4 reference lists | -| extended/radioactivedecay/rdecay01 | `extended/radioactivedecay/rdecay01/rdecay01.py` | Native radioactive decay and product survey | -| extended/runAndEvent/RE01 | `extended/runAndEvent/RE01/re01.py` | Multiple primary vertices preserved per event | +| Official family | Python entry point | Coverage | +| ----------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------- | +| basic/B1 | `basic/B1/b1.py` | Geometry, beam, volume energy scoring | +| basic/B2 | `basic/B2/b2.py` | Target, tracker chambers, field, step-derived hits | +| basic/B3 | `basic/B3/b3.py` | PET geometry and crystal/patient energy scoring | +| basic/B4 | `basic/B4/b4.py` | Sampling calorimeter and layer energy scoring | +| basic/B5 | `basic/B5/b5.py` | Spectrometer geometry, field, detector energy scoring | +| GoldFoilExperiment | `advanced/GoldFoilExperiment/gold_foil.py` | Rutherford-scattering workflow and CSV output | +| NanoparticleTherapy | `advanced/NanoparticleTherapy/*.py` | Geometric/physics adaptations and dose summaries | +| extended/eventgenerator/exgps | `extended/eventgenerator/exgps/exgps.py` | Native GPS and primary-spectrum output | +| extended/eventgenerator/particleGun | `extended/eventgenerator/particleGun/particle_gun.py` | Python-sampled source modes and transport | +| extended/eventgenerator/HepMC | `extended/eventgenerator/HepMC/hepmc.py` | Minimal HepMC2 parser adapter; no native HepMC SDK | +| extended/field/field01 | `extended/field/field01/field01.py` | Uniform magnetic-field tracking study | +| extended/field/field02 | `extended/field/field02/field02.py` | Native uniform electric field and absorber scoring | +| extended/optical/OpNovice | `extended/optical/OpNovice/op_novice.py` | Native optical transport and photon counting | +| extended/optical/LXe | `extended/optical/LXe/lxe.py` | LXe scintillation/transport; post-run photon analysis replaces PMT hit callbacks | +| extended/optical/wls | `extended/optical/wls/wls.py` | Native WLS absorption/re-emission in a GDML fiber | +| extended/analysis/AnaEx01 | `extended/analysis/AnaEx01/ana_ex01.py` | Calorimeter energy analysis and CSV ntuple | +| extended/analysis/AnaEx02 | `extended/analysis/AnaEx02/ana_ex02.py` | Nested event persistence through Parquet | +| extended/analysis/AnaEx03 | `extended/analysis/AnaEx03/ana_ex03.py` | ROOT summary adapter through uproot | +| extended/electromagnetic/TestEm1 | `extended/electromagnetic/TestEm1/test_em1.py` | Electromagnetic process and energy-deposit survey | +| extended/hadronic/Hadr00 | `extended/hadronic/Hadr00/hadr00.py` | Hadronic target and secondary-particle survey | +| extended/medical/GammaTherapy | `extended/medical/GammaTherapy/gamma_therapy.py` | Simplified therapy geometry with native energy mesh | +| extended/medical/electronScattering | `extended/medical/electronScattering/electron_scattering.py` | Simplified foil-scattering benchmark | +| extended/parameterisations/Par01 | `extended/parameterisations/Par01/par01.py` | Static parameterisation expanded to GDML placements | +| extended/physicslists/factory | `extended/physicslists/factory/factory.py` | Runtime selection from native Geant4 reference lists | +| extended/radioactivedecay/rdecay01 | `extended/radioactivedecay/rdecay01/rdecay01.py` | Native radioactive decay and product survey | +| extended/runAndEvent/RE01 | `extended/runAndEvent/RE01/re01.py` | Multiple primary vertices preserved per event | Each entry point opens Qt by default and accepts `--batch` for non-interactive execution. ## Standalone GDML geometries -The official Desktop tree currently contains 144 `.gdml` files. A GDML file -that includes a complete `` and world volume can be tried directly: +The official Desktop tree currently contains 144 `.gdml` files. A GDML file that +includes a complete `` and world volume can be tried directly: ```bash python run_gdml.py /absolute/path/to/detector.gdml --batch -n 10 ``` Many official GDML files are fragments included by a C++ detector or another -GDML file. Those fragments are not standalone examples and must not be passed -to the runner by themselves. +GDML file. Those fragments are not standalone examples and must not be passed to +the runner by themselves. ## Supported concepts, but not faithful class-for-class ports diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md b/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md index bceb3e64..57d619da 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/README.md @@ -7,29 +7,40 @@ viewer by default; pass `--batch` to run without a window. ## Included ports - `basic/B1/b1.py`: medical phantom with tissue and bone scoring volumes. -- `basic/B2/b2.py`: fixed target, six tracker chambers, hits, and transverse field. +- `basic/B2/b2.py`: fixed target, six tracker chambers, hits, and transverse + field. - `basic/B3/b3.py`: schematic PET ring with crystal and patient energy scoring. - `basic/B4/b4.py`: lead/liquid-argon sampling calorimeter. -- `basic/B5/b5.py`: double-arm spectrometer adaptation with field and detector scoring. +- `basic/B5/b5.py`: double-arm spectrometer adaptation with field and detector + scoring. - `advanced/GoldFoilExperiment/gold_foil.py`: Rutherford gold-foil scattering. -- `extended/eventgenerator/exgps/exgps.py`: GPS source distributions and primary analysis. -- `extended/eventgenerator/particleGun/particle_gun.py`: sampled particle-gun modes. -- `extended/eventgenerator/HepMC/hepmc.py`: minimal HepMC2 ASCII primary adapter. +- `extended/eventgenerator/exgps/exgps.py`: GPS source distributions and primary + analysis. +- `extended/eventgenerator/particleGun/particle_gun.py`: sampled particle-gun + modes. +- `extended/eventgenerator/HepMC/hepmc.py`: minimal HepMC2 ASCII primary + adapter. - `extended/field/field02/field02.py`: constant electric-field transport. - `extended/optical/OpNovice/op_novice.py`: optical generation and transport. -- `extended/optical/LXe/lxe.py`: liquid-xenon scintillation and optical transport. +- `extended/optical/LXe/lxe.py`: liquid-xenon scintillation and optical + transport. - `extended/optical/wls/wls.py`: wavelength-shifting fiber transport. - `extended/analysis/AnaEx01/ana_ex01.py`: sampling-calorimeter analysis output. - `extended/analysis/AnaEx02/ana_ex02.py`: nested Parquet event output. - `extended/analysis/AnaEx03/ana_ex03.py`: ROOT summary output via uproot. - `extended/field/field01/field01.py`: magnetic-field tracking study. -- `extended/radioactivedecay/rdecay01/rdecay01.py`: radioactive-decay track survey. +- `extended/radioactivedecay/rdecay01/rdecay01.py`: radioactive-decay track + survey. - `extended/electromagnetic/TestEm1/test_em1.py`: EM process survey. -- `extended/medical/GammaTherapy/gamma_therapy.py`: photon-therapy phantom and native dose mesh. -- `extended/medical/electronScattering/electron_scattering.py`: foil-scattering benchmark adaptation. +- `extended/medical/GammaTherapy/gamma_therapy.py`: photon-therapy phantom and + native dose mesh. +- `extended/medical/electronScattering/electron_scattering.py`: foil-scattering + benchmark adaptation. - `extended/hadronic/Hadr00/hadr00.py`: hadronic target and secondary survey. -- `extended/parameterisations/Par01/par01.py`: expanded parameterised detector layout. -- `extended/physicslists/factory/factory.py`: native reference-physics-list factory selection. +- `extended/parameterisations/Par01/par01.py`: expanded parameterised detector + layout. +- `extended/physicslists/factory/factory.py`: native reference-physics-list + factory selection. - `extended/runAndEvent/RE01/re01.py`: multiple primary vertices in each event. - `run_gdml.py`: a generic Qt runner for standalone GDML detector geometries. diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py index c0e9ac07..10229a2f 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/GoldFoilExperiment/gold_foil.py @@ -97,8 +97,12 @@ def main() -> None: # /gun/ commands: energy in MeV, position in cm (PrimaryGeneratorAction.cpp) app._send_and_recv(Message("generator", "set_particle", ("alpha",), {})) app._send_and_recv(Message("generator", "set_energy", (5.5,), {})) - app._send_and_recv(Message("generator", "set_position", ([0.0, 0.0, -5.0],), {})) - app._send_and_recv(Message("generator", "set_direction", ([0.0, 0.0, 1.0],), {})) + app._send_and_recv( + Message("generator", "set_position", ([0.0, 0.0, -5.0],), {}) + ) + app._send_and_recv( + Message("generator", "set_direction", ([0.0, 0.0, 1.0],), {}) + ) if not args.batch: if not app.visualization_available(): diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py index 2a2d8f86..7fb61c55 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/brain_tumor.py @@ -20,10 +20,16 @@ import math import awkward as ak + import geant4_python_application as g4 -DENSITY = {"Skull": 1.85, "Brain": 1.03, "Tumor": 1.127, - "ClusterAu": 19.32, "ClusterTissue": 1.127} +DENSITY = { + "Skull": 1.85, + "Brain": 1.03, + "Tumor": 1.127, + "ClusterAu": 19.32, + "ClusterTissue": 1.127, +} KEV_TO_J = 1.602176634e-16 TX, TZ = 20.0, 10.0 # tumour centre (mm) ALPHA, BETA = 0.3, 0.03 # /Gy, glioma-like LQ params @@ -60,8 +66,12 @@ def geometry(with_gold: bool = True) -> str: ''' -GUN = ["/gun/particle gamma", "/gun/energy 100 keV", - f"/gun/position {TX / 10} 0 -12 cm", "/gun/direction 0 0 1"] +GUN = [ + "/gun/particle gamma", + "/gun/energy 100 keV", + f"/gun/position {TX / 10} 0 -12 cm", + "/gun/direction 0 0 1", +] def run_case(n: int, with_gold: bool) -> dict[str, float]: @@ -70,7 +80,9 @@ def run_case(n: int, with_gold: bool) -> dict[str, float]: app.commands(GUN) events = app.run(n) e, v = events.track.step.energy, events.track.step.volume - out = {r: float(ak.sum(e[v == r])) for r in ("Skull", "Brain", "Tumor", "NanoCluster")} + out = { + r: float(ak.sum(e[v == r])) for r in ("Skull", "Brain", "Tumor", "NanoCluster") + } out["TumorTotal"] = out["Tumor"] + out["NanoCluster"] return out @@ -92,30 +104,37 @@ def main() -> None: if not args.batch: with g4.Application(gdml=geometry(True), seed=137) as app: app.commands(GUN) - app.visualize([ - "/vis/open OGL", "/vis/drawVolume", - "/vis/viewer/set/background black", - "/vis/viewer/set/style surface", - "/vis/geometry/set/colour World 1 1 1 0", - "/vis/geometry/set/colour SkullLV 0.9 0.8 0.5 0.12", - "/vis/geometry/set/colour BrainLV 1 0.5 0.7 0.18", - "/vis/geometry/set/colour TumorLV 0 1 0 0.5", - "/vis/geometry/set/colour NanoClusterLV 1 0 0 1", - "/vis/modeling/trajectories/create/drawByParticleID", - "/vis/modeling/trajectories/drawByParticleID-0/set gamma yellow", - "/vis/modeling/trajectories/drawByParticleID-0/set e- red", - "/vis/modeling/trajectories/drawByParticleID-0/set e+ blue", - "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", - "/vis/scene/endOfEventAction accumulate 100", - ]) + app.visualize( + [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/background black", + "/vis/viewer/set/style surface", + "/vis/geometry/set/colour World 1 1 1 0", + "/vis/geometry/set/colour SkullLV 0.9 0.8 0.5 0.12", + "/vis/geometry/set/colour BrainLV 1 0.5 0.7 0.18", + "/vis/geometry/set/colour TumorLV 0 1 0 0.5", + "/vis/geometry/set/colour NanoClusterLV 1 0 0 1", + "/vis/modeling/trajectories/create/drawByParticleID", + "/vis/modeling/trajectories/drawByParticleID-0/set gamma yellow", + "/vis/modeling/trajectories/drawByParticleID-0/set e- red", + "/vis/modeling/trajectories/drawByParticleID-0/set e+ blue", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ] + ) return v_cl = 4 / 3 * math.pi * 0.4**3 v_tu = 4 / 3 * math.pi * 1.2**3 - v_cl v_br = 4 / 3 * math.pi * 7.8**3 - 4 / 3 * math.pi * 1.2**3 v_sk = 4 / 3 * math.pi * 8.5**3 - 4 / 3 * math.pi * 7.8**3 - m = {"Brain": v_br * DENSITY["Brain"], "Tumor": v_tu * DENSITY["Tumor"], - "Skull": v_sk * DENSITY["Skull"]} + m = { + "Brain": v_br * DENSITY["Brain"], + "Tumor": v_tu * DENSITY["Tumor"], + "Skull": v_sk * DENSITY["Skull"], + } no, yes = run_case(args.events, False), run_case(args.events, True) der = yes["TumorTotal"] / no["TumorTotal"] if no["TumorTotal"] else float("nan") @@ -123,14 +142,24 @@ def main() -> None: # Scale: give no-Au tumour a clinical 2 Gy fraction; same fluence with Au gives 2*DER. s_no, s_au = survival(2.0), survival(2.0 * der) print(f"events: {args.events}, beam 100 keV -> tumour at ({TX},0,{TZ}) mm") - print(f"[no-Au ] TumorTotal {no['TumorTotal']:.1f} keV | Brain {no['Brain']:.1f} keV | Skull {no['Skull']:.1f} keV") - print(f"[with-Au] TumorTotal {yes['TumorTotal']:.1f} keV | Brain {yes['Brain']:.1f} keV | Skull {yes['Skull']:.1f} keV") + print( + f"[no-Au ] TumorTotal {no['TumorTotal']:.1f} keV | Brain {no['Brain']:.1f} keV | Skull {no['Skull']:.1f} keV" + ) + print( + f"[with-Au] TumorTotal {yes['TumorTotal']:.1f} keV | Brain {yes['Brain']:.1f} keV | Skull {yes['Skull']:.1f} keV" + ) print(f"DER (tumour Au/no-Au): {der:.2f}") - print(f"At 2 Gy to tumour without Au: survival {s_no:.3f} (reduction {(1 - s_no) * 100:.1f}%)") - print(f"Same fluence with Au -> {2 * der:.1f} Gy: survival {s_au:.3e} (reduction {(1 - s_au) * 100:.2f}%)") - print("Report: Geant4 proves DOSE boost (photoelectric in Au Z=79); " - "reduction % comes from LQ model, not from cells dying in code. " - "Solid AuSphere exaggerates (real DER 1.1-2); skull/brain doses show sparing.") + print( + f"At 2 Gy to tumour without Au: survival {s_no:.3f} (reduction {(1 - s_no) * 100:.1f}%)" + ) + print( + f"Same fluence with Au -> {2 * der:.1f} Gy: survival {s_au:.3e} (reduction {(1 - s_au) * 100:.2f}%)" + ) + print( + "Report: Geant4 proves DOSE boost (photoelectric in Au Z=79); " + "reduction % comes from LQ model, not from cells dying in code. " + "Solid AuSphere exaggerates (real DER 1.1-2); skull/brain doses show sparing." + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py index 1dc10e79..65e3243a 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/advanced/NanoparticleTherapy/nano_therapy.py @@ -21,10 +21,16 @@ import math import awkward as ak + import geant4_python_application as g4 # Densities (g/cm3) for dose = energy / mass estimates. -DENSITY = {"Phantom": 1.0, "Tumor": 1.127, "NanoCluster_Au": 19.32, "NanoCluster_tissue": 1.127} +DENSITY = { + "Phantom": 1.0, + "Tumor": 1.127, + "NanoCluster_Au": 19.32, + "NanoCluster_tissue": 1.127, +} KEV_TO_JOULE = 1.602176634e-16 @@ -56,10 +62,14 @@ def run_case(events_n: int, with_gold: bool) -> dict[str, float]: """Run one configuration, return deposited energy (keV) per region.""" with g4.Application(gdml=geometry(with_gold), seed=137) as app: app.set_event_fields({"id", "step_energy", "step_volume"}) - app.commands([ - "/gun/particle gamma", "/gun/energy 100 keV", - "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1", - ]) + app.commands( + [ + "/gun/particle gamma", + "/gun/energy 100 keV", + "/gun/position 0 0 -10 cm", + "/gun/direction 0 0 1", + ] + ) events = app.run(events_n) energy = events.track.step.energy @@ -83,35 +93,45 @@ def main() -> None: if not args.batch: with g4.Application(gdml=geometry(with_gold=True), seed=137) as app: - app.commands([ - "/gun/particle gamma", "/gun/energy 100 keV", - "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1", - ]) - app.visualize([ - "/vis/open OGL", "/vis/drawVolume", - "/vis/viewer/set/background black", - "/vis/viewer/set/style surface", - # colour-code by logical volume: phantom=blue, tumour=green, Au=red - "/vis/geometry/set/colour World 1 1 1 0", - "/vis/geometry/set/colour PhantomLV 0 0.3 1 0.15", - "/vis/geometry/set/colour TumorLV 0 1 0 0.35", - "/vis/geometry/set/colour NanoClusterLV 1 0 0 1", - # colour-code tracks: gamma=yellow, e-=red, rest=white - "/vis/modeling/trajectories/create/drawByParticleID", - "/vis/modeling/trajectories/drawByParticleID-0/set gamma yellow", - "/vis/modeling/trajectories/drawByParticleID-0/set e- red", - "/vis/modeling/trajectories/drawByParticleID-0/set e+ blue", - "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", - "/vis/scene/endOfEventAction accumulate 100", - ]) + app.commands( + [ + "/gun/particle gamma", + "/gun/energy 100 keV", + "/gun/position 0 0 -10 cm", + "/gun/direction 0 0 1", + ] + ) + app.visualize( + [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/background black", + "/vis/viewer/set/style surface", + # colour-code by logical volume: phantom=blue, tumour=green, Au=red + "/vis/geometry/set/colour World 1 1 1 0", + "/vis/geometry/set/colour PhantomLV 0 0.3 1 0.15", + "/vis/geometry/set/colour TumorLV 0 1 0 0.35", + "/vis/geometry/set/colour NanoClusterLV 1 0 0 1", + # colour-code tracks: gamma=yellow, e-=red, rest=white + "/vis/modeling/trajectories/create/drawByParticleID", + "/vis/modeling/trajectories/drawByParticleID-0/set gamma yellow", + "/vis/modeling/trajectories/drawByParticleID-0/set e- red", + "/vis/modeling/trajectories/drawByParticleID-0/set e+ blue", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ] + ) return # Volumes/masses: cluster r=5mm, tumour r=15mm, phantom 90mm box. v_cluster = 4 / 3 * math.pi * 0.5**3 # cm3 v_tumor = 4 / 3 * math.pi * 1.5**3 - v_cluster v_phantom = 9.0**3 - 4 / 3 * math.pi * 1.5**3 - masses = {"Phantom": v_phantom * DENSITY["Phantom"], - "Tumor": v_tumor * DENSITY["Tumor"]} + masses = { + "Phantom": v_phantom * DENSITY["Phantom"], + "Tumor": v_tumor * DENSITY["Tumor"], + } no_au = run_case(args.events, with_gold=False) with_au = run_case(args.events, with_gold=True) @@ -119,18 +139,29 @@ def main() -> None: masses_no = dict(masses, NanoCluster=v_cluster * DENSITY["NanoCluster_tissue"]) print(f"events per case: {args.events}, beam: 100 keV gamma") - for label, res, m in (("no-Au ", no_au, masses_no), ("with-Au", with_au, masses_au)): - print(f"[{label}] Phantom(healthy): {res['Phantom']:.1f} keV " - f"({dose_gy(res['Phantom'], m['Phantom']):.3e} Gy) | " - f"Tumor rim: {res['Tumor']:.1f} keV | " - f"Cluster: {res['NanoCluster']:.1f} keV | " - f"TumorTotal: {res['TumorTotal']:.1f} keV") - - der = with_au["TumorTotal"] / no_au["TumorTotal"] if no_au["TumorTotal"] else float("nan") + for label, res, m in ( + ("no-Au ", no_au, masses_no), + ("with-Au", with_au, masses_au), + ): + print( + f"[{label}] Phantom(healthy): {res['Phantom']:.1f} keV " + f"({dose_gy(res['Phantom'], m['Phantom']):.3e} Gy) | " + f"Tumor rim: {res['Tumor']:.1f} keV | " + f"Cluster: {res['NanoCluster']:.1f} keV | " + f"TumorTotal: {res['TumorTotal']:.1f} keV" + ) + + der = ( + with_au["TumorTotal"] / no_au["TumorTotal"] + if no_au["TumorTotal"] + else float("nan") + ) print(f"Dose Enhancement Ratio (TumorTotal Au / no-Au): {der:.3f}") - print("Assignment: explain why DER>1 (photoelectric ~ Z^3/E^3 in Au), " - "why 100 keV shows it but MV beam would show less, and what the " - "Phantom dose says about healthy-tissue sparing.") + print( + "Assignment: explain why DER>1 (photoelectric ~ Z^3/E^3 in Au), " + "why 100 keV shows it but MV beam would show less, and what the " + "Phantom dose says about healthy-tissue sparing." + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py index 4ffa6f02..47270b06 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B1/b1.py @@ -5,10 +5,10 @@ import argparse import awkward as ak -import geant4_python_application as g4 +import geant4_python_application as g4 -GDML = R''' +GDML = R""" @@ -29,7 +29,7 @@ -''' +""" def main() -> None: @@ -40,16 +40,25 @@ def main() -> None: with g4.Application(gdml=GDML, seed=137) as app: app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) - app.commands([ - "/gun/particle e-", "/gun/energy 10 MeV", - "/gun/position 0 0 -20 cm", "/gun/direction 0 0 1", - ]) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 10 MeV", + "/gun/position 0 0 -20 cm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: - app.visualize([ - "/vis/open OGL", "/vis/drawVolume", "/vis/viewer/set/style wireframe", - "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", - "/vis/scene/endOfEventAction accumulate 100", - ]) + app.visualize( + [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ] + ) return events = app.run(args.events) diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py index 59335d22..2cb4c9e1 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B2/b2.py @@ -5,6 +5,7 @@ import argparse import awkward as ak + import geant4_python_application as g4 @@ -19,22 +20,23 @@ def geometry() -> str: ) volumes = "".join( f'' - f'' for i in range(6) + f'' + for i in range(6) ) placements = "".join( f'' f'' for i in range(6) ) - return f''' + return f""" -{''.join(chambers)} +{"".join(chambers)} {volumes} -{placements}''' +{placements}""" def main() -> None: @@ -43,9 +45,18 @@ def main() -> None: parser.add_argument("--batch", action="store_true") args = parser.parse_args() with g4.Application(gdml=geometry(), physics="FTFP_BERT", seed=137) as app: - app.set_event_fields({"id", "track_id", "step_energy", "step_volume", "step_position"}) + app.set_event_fields( + {"id", "track_id", "step_energy", "step_volume", "step_position"} + ) app.detector.magnetic_field = (0.2, 0.0, 0.0) - app.commands(["/gun/particle proton", "/gun/energy 3 GeV", "/gun/position 0 0 -20 cm", "/gun/direction 0 0 1"]) + app.commands( + [ + "/gun/particle proton", + "/gun/energy 3 GeV", + "/gun/position 0 0 -20 cm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: app.visualize() return @@ -54,7 +65,9 @@ def main() -> None: tracker_hits = steps[ (steps.energy > 0) & ak.str.starts_with(steps.volume, "Chamber") ] - print(f"events: {len(events)}, tracker hits: {len(ak.flatten(tracker_hits.energy, axis=None))}") + print( + f"events: {len(events)}, tracker hits: {len(ak.flatten(tracker_hits.energy, axis=None))}" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py index deedc2b9..d0bacae9 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B3/b3.py @@ -6,6 +6,7 @@ import math import awkward as ak + import geant4_python_application as g4 @@ -22,15 +23,15 @@ def geometry() -> str: f'' f'' ) - return f''' + return f""" -{''.join(placements)} -''' +{"".join(placements)} +""" def main() -> None: @@ -41,14 +42,27 @@ def main() -> None: with g4.Application(gdml=geometry(), physics="FTFP_BERT", seed=137) as app: app.add_physics("G4RadioactiveDecayPhysics") app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) - app.commands(["/gun/particle gamma", "/gun/energy 511 keV", "/gun/position 0 0 0 mm", "/gun/direction 1 0 0"]) + app.commands( + [ + "/gun/particle gamma", + "/gun/energy 511 keV", + "/gun/position 0 0 0 mm", + "/gun/direction 1 0 0", + ] + ) if not args.batch: app.visualize() return events = app.run(args.events) - crystal = ak.sum(events.track.step.energy[ak.str.starts_with(events.track.step.volume, "Crystal")]) + crystal = ak.sum( + events.track.step.energy[ + ak.str.starts_with(events.track.step.volume, "Crystal") + ] + ) patient = ak.sum(events.track.step.energy[events.track.step.volume == "Patient"]) - print(f"events: {len(events)}, crystal energy: {float(crystal):.3f} keV, patient energy: {float(patient):.3f} keV") + print( + f"events: {len(events)}, crystal energy: {float(crystal):.3f} keV, patient energy: {float(patient):.3f} keV" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py index 4150daab..aad722d6 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B4/b4.py @@ -5,6 +5,7 @@ import argparse import awkward as ak + import geant4_python_application as g4 @@ -18,7 +19,7 @@ def geometry() -> str: f'' f'' ) - return f''' + return f""" @@ -26,8 +27,8 @@ def geometry() -> str: - {''.join(layers)} -''' + {"".join(layers)} +""" def main() -> None: @@ -38,16 +39,25 @@ def main() -> None: with g4.Application(gdml=geometry(), seed=137) as app: app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) - app.commands([ - "/gun/particle e-", "/gun/energy 1 GeV", - "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1", - ]) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 1 GeV", + "/gun/position 0 0 -10 cm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: - app.visualize([ - "/vis/open OGL", "/vis/drawVolume", "/vis/viewer/set/style wireframe", - "/vis/scene/add/trajectories smooth", "/tracking/storeTrajectory 1", - "/vis/scene/endOfEventAction accumulate 100", - ]) + app.visualize( + [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ] + ) return events = app.run(args.events) diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py index 52338777..bc61c1a5 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/basic/B5/b5.py @@ -5,20 +5,26 @@ import argparse import awkward as ak + import geant4_python_application as g4 def geometry() -> str: placements = [] specs = [ - ("Hodoscope1", "HodoLV", -180), ("Chamber1", "ChamberLV", -130), - ("FieldRegion", "FieldLV", 0), ("Hodoscope2", "HodoLV", 130), - ("Chamber2", "ChamberLV", 170), ("ECal", "ECalLV", 220), + ("Hodoscope1", "HodoLV", -180), + ("Chamber1", "ChamberLV", -130), + ("FieldRegion", "FieldLV", 0), + ("Hodoscope2", "HodoLV", 130), + ("Chamber2", "ChamberLV", 170), + ("ECal", "ECalLV", 220), ("HCal", "HCalLV", 290), ] for name, logical, z in specs: - placements.append(f'') - return f''' + placements.append( + f'' + ) + return f""" @@ -28,8 +34,8 @@ def geometry() -> str: -{''.join(placements)} -''' +{"".join(placements)} +""" def main() -> None: @@ -39,15 +45,33 @@ def main() -> None: args = parser.parse_args() with g4.Application(gdml=geometry(), physics="FTFP_BERT", seed=137) as app: app.detector.magnetic_field = (0.0, 1.0, 0.0) - app.set_event_fields({"id", "track_id", "step_energy", "step_time", "step_volume", "step_position"}) - app.commands(["/gun/particle proton", "/gun/energy 1 GeV", "/gun/position 0 0 -35 cm", "/gun/direction 0 0 1"]) + app.set_event_fields( + { + "id", + "track_id", + "step_energy", + "step_time", + "step_volume", + "step_position", + } + ) + app.commands( + [ + "/gun/particle proton", + "/gun/energy 1 GeV", + "/gun/position 0 0 -35 cm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: app.visualize() return events = app.run(args.events) for volume in ("Hodoscope", "Chamber", "ECal", "HCal"): mask = ak.str.starts_with(events.track.step.volume, volume) - print(f"{volume} energy: {float(ak.sum(events.track.step.energy[mask])):.3f} keV") + print( + f"{volume} energy: {float(ak.sum(events.track.step.energy[mask])):.3f} keV" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py index 059d4b68..0017b378 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx01/ana_ex01.py @@ -3,13 +3,13 @@ from __future__ import annotations import argparse +from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path import awkward as ak -import geant4_python_application as g4 import numpy as np -from importlib.util import module_from_spec, spec_from_file_location +import geant4_python_application as g4 def load_b4_geometry(): @@ -29,16 +29,43 @@ def main() -> None: args = p.parse_args() with g4.Application(gdml=load_b4_geometry(), physics="FTFP_BERT", seed=137) as app: app.set_event_fields({"id", "track_id", "step_energy", "step_volume"}) - app.commands(["/gun/particle e-", "/gun/energy 1 GeV", "/gun/position 0 0 -10 cm", "/gun/direction 0 0 1"]) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 1 GeV", + "/gun/position 0 0 -10 cm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: app.visualize() return events = app.run(args.events) step = events.track.step - absorber = ak.to_numpy(ak.sum(ak.flatten(step.energy[ak.str.starts_with(step.volume, "Absorber")], axis=-1), axis=1)) - gap = ak.to_numpy(ak.sum(ak.flatten(step.energy[ak.str.starts_with(step.volume, "Gap")], axis=-1), axis=1)) - np.savetxt(args.output, np.column_stack((np.arange(len(events)), absorber, gap)), delimiter=",", header="event,EAbs_keV,EGap_keV", comments="") - print(f"events: {len(events)}, wrote {args.output}; mean EAbs={absorber.mean():.3f} keV, EGap={gap.mean():.3f} keV") + absorber = ak.to_numpy( + ak.sum( + ak.flatten( + step.energy[ak.str.starts_with(step.volume, "Absorber")], axis=-1 + ), + axis=1, + ) + ) + gap = ak.to_numpy( + ak.sum( + ak.flatten(step.energy[ak.str.starts_with(step.volume, "Gap")], axis=-1), + axis=1, + ) + ) + np.savetxt( + args.output, + np.column_stack((np.arange(len(events)), absorber, gap)), + delimiter=",", + header="event,EAbs_keV,EGap_keV", + comments="", + ) + print( + f"events: {len(events)}, wrote {args.output}; mean EAbs={absorber.mean():.3f} keV, EGap={gap.mean():.3f} keV" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py index 1b8e59e1..8a0a3f33 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx02/ana_ex02.py @@ -1,16 +1,36 @@ """Python adaptation of AnaEx02 with nested Parquet event output.""" + from __future__ import annotations + import argparse from pathlib import Path + import geant4_python_application as g4 + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); p.add_argument("-o","--output",type=Path,default=Path("ana_ex02.parquet")); a=p.parse_args() - with g4.Application(gdml=g4.basic_gdml,physics="FTFP_BERT",seed=138) as app: - app.commands(["/gun/particle e-","/gun/energy 100 MeV","/gun/position 0 0 -40 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) - g4.io.events_to_parquet(events,a.output) - assert len(g4.io.events_from_parquet(a.output))==len(events) + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--batch", action="store_true") + p.add_argument("-o", "--output", type=Path, default=Path("ana_ex02.parquet")) + a = p.parse_args() + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=138) as app: + app.commands( + [ + "/gun/particle e-", + "/gun/energy 100 MeV", + "/gun/position 0 0 -40 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + g4.io.events_to_parquet(events, a.output) + assert len(g4.io.events_from_parquet(a.output)) == len(events) print(f"events: {len(events)}, wrote {a.output}") -if __name__=="__main__": main() + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py index eee5b842..155295af 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/analysis/AnaEx03/ana_ex03.py @@ -1,19 +1,39 @@ """Python adaptation of AnaEx03 with ROOT summary output via uproot.""" + from __future__ import annotations + import argparse from pathlib import Path + import geant4_python_application as g4 + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); p.add_argument("-o","--output",type=Path,default=Path("ana_ex03.root")); a=p.parse_args() - with g4.Application(gdml=g4.basic_gdml,physics="FTFP_BERT",seed=139) as app: - app.commands(["/gun/particle gamma","/gun/energy 10 MeV","/gun/position 0 0 -40 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--batch", action="store_true") + p.add_argument("-o", "--output", type=Path, default=Path("ana_ex03.root")) + a = p.parse_args() + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=139) as app: + app.commands( + [ + "/gun/particle gamma", + "/gun/energy 10 MeV", + "/gun/position 0 0 -40 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) try: - g4.io.events_to_root(events,a.output) - assert len(g4.io.events_from_root(a.output)["id"])==len(events) + g4.io.events_to_root(events, a.output) + assert len(g4.io.events_from_root(a.output)["id"]) == len(events) print(f"events: {len(events)}, wrote {a.output}") except ImportError as exc: print(f"events: {len(events)}, ROOT output skipped: {exc}") -if __name__=="__main__": main() + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py index 0b239d45..92bac55b 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/electromagnetic/TestEm1/test_em1.py @@ -1,19 +1,51 @@ """Python adaptation of extended/electromagnetic/TestEm1 process survey.""" + from __future__ import annotations + import argparse from collections import Counter + import awkward as ak + import geant4_python_application as g4 + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--material",default="G4_Al"); p.add_argument("--batch",action="store_true"); a=p.parse_args() - gdml=f'''''' - with g4.Application(gdml=gdml,seed=137) as app: - app.set_event_fields({"id","track_id","step_energy","step_process","step_position","step_track_kinetic_energy"}) - app.commands(["/gun/particle e-","/gun/energy 10 MeV","/gun/position 0 0 -40 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) - processes=Counter(ak.to_list(ak.flatten(events.track.step.process,axis=None))) - print(f"events: {len(events)}, material: {a.material}, deposited: {float(ak.sum(events.track.step.energy)):.3f} keV") + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--material", default="G4_Al") + p.add_argument("--batch", action="store_true") + a = p.parse_args() + gdml = f'''''' + with g4.Application(gdml=gdml, seed=137) as app: + app.set_event_fields( + { + "id", + "track_id", + "step_energy", + "step_process", + "step_position", + "step_track_kinetic_energy", + } + ) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 10 MeV", + "/gun/position 0 0 -40 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + processes = Counter(ak.to_list(ak.flatten(events.track.step.process, axis=None))) + print( + f"events: {len(events)}, material: {a.material}, deposited: {float(ak.sum(events.track.step.energy)):.3f} keV" + ) print(f"process calls: {dict(processes)}") -if __name__=="__main__": main() + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py index e71665ee..451129d4 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/HepMC/hepmc.py @@ -20,7 +20,13 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input", nargs="?", type=Path) - parser.add_argument("-n", "--events", type=int, default=1, help="accepted for the common audit interface") + parser.add_argument( + "-n", + "--events", + type=int, + default=1, + help="accepted for the common audit interface", + ) parser.add_argument("--batch", action="store_true") args = parser.parse_args() text = args.input.read_text() if args.input else SAMPLE diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py index e228d9ec..e1ae9341 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/exgps/exgps.py @@ -6,10 +6,11 @@ from pathlib import Path import awkward as ak -import geant4_python_application as g4 import numpy as np -GDML = '''''' +import geant4_python_application as g4 + +GDML = """""" def main() -> None: @@ -20,17 +21,26 @@ def main() -> None: args = p.parse_args() with g4.Application(gdml=GDML, seed=137) as app: app.generator.use_gps().particle("gamma") - app.generator.commands([ - "/gps/pos/type Volume", "/gps/pos/shape Sphere", "/gps/pos/radius 4 cm", - "/gps/ang/type iso", "/gps/ene/type Lin", "/gps/ene/min 100 keV", "/gps/ene/max 2 MeV", - ]) + app.generator.commands( + [ + "/gps/pos/type Volume", + "/gps/pos/shape Sphere", + "/gps/pos/radius 4 cm", + "/gps/ang/type iso", + "/gps/ene/type Lin", + "/gps/ene/min 100 keV", + "/gps/ene/max 2 MeV", + ] + ) if not args.batch: app.visualize() return events = app.run(args.events) energy = ak.to_numpy(ak.flatten(events.primaries.energy)) np.savez(args.output, energy_keV=energy) - print(f"events: {len(events)}, energy range: {energy.min():.2f}-{energy.max():.2f} keV; wrote {args.output}") + print( + f"events: {len(events)}, energy range: {energy.min():.2f}-{energy.max():.2f} keV; wrote {args.output}" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py index 6474a204..4041e19d 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/eventgenerator/particleGun/particle_gun.py @@ -6,12 +6,18 @@ import math import awkward as ak -import geant4_python_application as g4 import numpy as np +import geant4_python_application as g4 + def _particle(position, direction, energy=1000.0): - return {"particle": "geantino", "energy": energy, "position": dict(zip("xyz", position)), "direction": dict(zip("xyz", direction))} + return { + "particle": "geantino", + "energy": energy, + "position": dict(zip("xyz", position)), + "direction": dict(zip("xyz", direction)), + } def primaries(count: int, mode: int, seed: int) -> ak.Array: @@ -22,11 +28,17 @@ def primaries(count: int, mode: int, seed: int) -> ak.Array: if mode == 1: position = (50 * math.cos(phi), 50 * math.sin(phi), rng.uniform(-50, 50)) radial = (math.cos(phi), math.sin(phi), 0.0) - records.append({"primaries": [ - _particle(position, radial), - _particle(tuple(-x for x in position), tuple(-x for x in radial)), - _particle((0.0, 0.0, 0.0), (0.0, 0.0, 1.0)), - ]}) + records.append( + { + "primaries": [ + _particle(position, radial), + _particle( + tuple(-x for x in position), tuple(-x for x in radial) + ), + _particle((0.0, 0.0, 0.0), (0.0, 0.0, 1.0)), + ] + } + ) continue if mode == 2: energy = float(rng.choice([100, 250, 500, 1000], p=[0.1, 0.3, 0.4, 0.2])) @@ -36,12 +48,20 @@ def primaries(count: int, mode: int, seed: int) -> ak.Array: radius = np.cbrt(rng.uniform(20**3, 80**3)) costheta = rng.uniform(-1, 1) sintheta = math.sqrt(1 - costheta**2) - position = (radius * sintheta * math.cos(phi), radius * sintheta * math.sin(phi), radius * costheta) + position = ( + radius * sintheta * math.cos(phi), + radius * sintheta * math.sin(phi), + radius * costheta, + ) direction = tuple(-value / radius for value in position) energy = 1000.0 else: alpha = rng.uniform(0, math.radians(15 if mode == 3 else 30)) - direction = (math.sin(alpha) * math.cos(phi), math.sin(alpha) * math.sin(phi), math.cos(alpha)) + direction = ( + math.sin(alpha) * math.cos(phi), + math.sin(alpha) * math.sin(phi), + math.cos(alpha), + ) position, energy = (0.0, 0.0, -100.0), 1000.0 records.append(_particle(position, direction, energy)) return ak.Array(records) diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py index bc7d221a..6089df76 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field01/field01.py @@ -1,17 +1,46 @@ """Python adaptation of extended/field/field01 magnetic tracking study.""" + from __future__ import annotations + import argparse + import awkward as ak + import geant4_python_application as g4 + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--field",type=float,default=1.0); p.add_argument("--batch",action="store_true"); a=p.parse_args() - with g4.Application(gdml=g4.basic_gdml,seed=137) as app: - app.detector.magnetic_field=(0.0,a.field,0.0) - app.set_event_fields({"id","track_id","step_position","step_volume","step_track_kinetic_energy"}) - app.commands(["/gun/particle e-","/gun/energy 100 MeV","/gun/position 0 0 -20 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) - nsteps=len(ak.flatten(events.track.step.position.x,axis=None)) + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--field", type=float, default=1.0) + p.add_argument("--batch", action="store_true") + a = p.parse_args() + with g4.Application(gdml=g4.basic_gdml, seed=137) as app: + app.detector.magnetic_field = (0.0, a.field, 0.0) + app.set_event_fields( + { + "id", + "track_id", + "step_position", + "step_volume", + "step_track_kinetic_energy", + } + ) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 100 MeV", + "/gun/position 0 0 -20 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + nsteps = len(ak.flatten(events.track.step.position.x, axis=None)) print(f"events: {len(events)}, B=(0,{a.field},0) T, recorded steps: {nsteps}") -if __name__=="__main__": main() + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py index ec614067..f364cb82 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/field/field02/field02.py @@ -5,9 +5,10 @@ import argparse import awkward as ak + import geant4_python_application as g4 -GDML = '''''' +GDML = """""" def main() -> None: @@ -18,15 +19,26 @@ def main() -> None: args = p.parse_args() with g4.Application(gdml=GDML, physics="FTFP_BERT", seed=137) as app: app.detector.electric_field = (args.field, 0.0, 0.0) - app.set_event_fields({"id", "track_id", "step_energy", "step_volume", "step_position"}) - app.commands(["/gun/particle e-", "/gun/energy 10 MeV", "/gun/position 0 0 -20 cm", "/gun/direction 0 0 1"]) + app.set_event_fields( + {"id", "track_id", "step_energy", "step_volume", "step_position"} + ) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 10 MeV", + "/gun/position 0 0 -20 cm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: app.visualize() return events = app.run(args.events) steps = events.track.step edep = float(ak.sum(steps.energy[steps.volume == "Absorber"])) - print(f"events: {len(events)}, E=({args.field},0,0) kV/cm, absorber energy: {edep:.3f} keV") + print( + f"events: {len(events)}, E=({args.field},0,0) kV/cm, absorber energy: {edep:.3f} keV" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py index 227ab146..b3b53800 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/hadronic/Hadr00/hadr00.py @@ -1,18 +1,39 @@ """Python adaptation of extended/hadronic/Hadr00 target interactions.""" + from __future__ import annotations + import argparse from collections import Counter + import awkward as ak + import geant4_python_application as g4 + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--physics",default="FTFP_BERT"); p.add_argument("--batch",action="store_true"); a=p.parse_args() - gdml='''''' - with g4.Application(gdml=gdml,physics=a.physics,seed=137) as app: - app.commands(["/gun/particle proton","/gun/energy 15 GeV","/gun/position 0 0 -9 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) - secondaries=events.track.particle[events.track.parent_id>0] - counts=Counter(ak.to_list(ak.flatten(secondaries,axis=None))) - print(f"events: {len(events)}, physics: {a.physics}, secondaries: {dict(counts)}") -if __name__=="__main__": main() + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--physics", default="FTFP_BERT") + p.add_argument("--batch", action="store_true") + a = p.parse_args() + gdml = """""" + with g4.Application(gdml=gdml, physics=a.physics, seed=137) as app: + app.commands( + [ + "/gun/particle proton", + "/gun/energy 15 GeV", + "/gun/position 0 0 -9 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + secondaries = events.track.particle[events.track.parent_id > 0] + counts = Counter(ak.to_list(ak.flatten(secondaries, axis=None))) + print(f"events: {len(events)}, physics: {a.physics}, secondaries: {dict(counts)}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py index 0dd16e33..0ddc9490 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/GammaTherapy/gamma_therapy.py @@ -1,19 +1,47 @@ """Python adaptation of extended/medical/GammaTherapy dose scoring.""" + from __future__ import annotations + import argparse from pathlib import Path + import awkward as ak + import geant4_python_application as g4 -GDML='''''' +GDML = """""" + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); p.add_argument("-o","--output",type=Path,default=Path("gamma_therapy_mesh.csv")); a=p.parse_args() - with g4.Application(gdml=GDML,physics="FTFP_BERT",seed=137) as app: - mesh=g4.NativeScoringMesh(app,"phantomMesh","doseEnergy").box((150,150,200),(10,10,20),center=(0,0,0)) - app.commands(["/gun/particle gamma","/gun/energy 6 MeV","/gun/position 0 0 -45 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events); mesh.dump(a.output) - dose=float(ak.sum(events.track.step.energy[events.track.step.volume=="Phantom"])) - print(f"events: {len(events)}, phantom deposited energy: {dose:.3f} keV; mesh: {a.output}") -if __name__=="__main__": main() + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--batch", action="store_true") + p.add_argument("-o", "--output", type=Path, default=Path("gamma_therapy_mesh.csv")) + a = p.parse_args() + with g4.Application(gdml=GDML, physics="FTFP_BERT", seed=137) as app: + mesh = g4.NativeScoringMesh(app, "phantomMesh", "doseEnergy").box( + (150, 150, 200), (10, 10, 20), center=(0, 0, 0) + ) + app.commands( + [ + "/gun/particle gamma", + "/gun/energy 6 MeV", + "/gun/position 0 0 -45 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + mesh.dump(a.output) + dose = float( + ak.sum(events.track.step.energy[events.track.step.volume == "Phantom"]) + ) + print( + f"events: {len(events)}, phantom deposited energy: {dose:.3f} keV; mesh: {a.output}" + ) + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py index 83e0db82..1191dccb 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/medical/electronScattering/electron_scattering.py @@ -1,19 +1,42 @@ """Python adaptation of the NRCC electron-scattering benchmark geometry.""" + from __future__ import annotations + import argparse + import awkward as ak + import geant4_python_application as g4 -GDML='''''' +GDML = """""" + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=1000); p.add_argument("--batch",action="store_true"); a=p.parse_args() - with g4.Application(gdml=GDML,seed=137) as app: - app.set_event_fields({"id","track_id","step_volume_post","step_momentum","step_position"}) - app.commands(["/gun/particle e-","/gun/energy 13 MeV","/gun/position 0 0 -30 cm","/gun/direction 0 0 1"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) - step=events.track.step; crossing=step[step.volume_post=="ScoringPlane"] - n=len(ak.flatten(crossing.position.x,axis=None)) - print(f"events: {len(events)}, scoring-plane crossings: {n}") -if __name__=="__main__": main() + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=1000) + p.add_argument("--batch", action="store_true") + a = p.parse_args() + with g4.Application(gdml=GDML, seed=137) as app: + app.set_event_fields( + {"id", "track_id", "step_volume_post", "step_momentum", "step_position"} + ) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 13 MeV", + "/gun/position 0 0 -30 cm", + "/gun/direction 0 0 1", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + step = events.track.step + crossing = step[step.volume_post == "ScoringPlane"] + n = len(ak.flatten(crossing.position.x, axis=None)) + print(f"events: {len(events)}, scoring-plane crossings: {n}") + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py index 5a5156d4..71af9c02 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/LXe/lxe.py @@ -10,6 +10,7 @@ import argparse import awkward as ak + import geant4_python_application as g4 @@ -19,7 +20,9 @@ def main() -> None: parser.add_argument("--batch", action="store_true") args = parser.parse_args() - with g4.Application(gdml=g4.optical.lxe_scintillator_gdml, optical=True, seed=137) as app: + with g4.Application( + gdml=g4.optical.lxe_scintillator_gdml, optical=True, seed=137 + ) as app: app.commands(g4.optical.scintillation_commands(max_cerenkov=100)) app.commands( [ diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py index 07b54017..96a3c93a 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/OpNovice/op_novice.py @@ -5,6 +5,7 @@ import argparse import awkward as ak + import geant4_python_application as g4 @@ -13,9 +14,18 @@ def main() -> None: p.add_argument("-n", "--events", type=int, default=10) p.add_argument("--batch", action="store_true") args = p.parse_args() - with g4.Application(gdml=g4.optical.optical_water_gdml, optical=True, seed=137) as app: + with g4.Application( + gdml=g4.optical.optical_water_gdml, optical=True, seed=137 + ) as app: app.commands(g4.optical.scintillation_commands(max_cerenkov=100)) - app.commands(["/gun/particle e-", "/gun/energy 10 MeV", "/gun/position 0 0 0 mm", "/gun/direction 1 0 0"]) + app.commands( + [ + "/gun/particle e-", + "/gun/energy 10 MeV", + "/gun/position 0 0 0 mm", + "/gun/direction 1 0 0", + ] + ) if not args.batch: app.visualize() return diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py index b8293dc7..46f3f088 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/optical/wls/wls.py @@ -10,6 +10,7 @@ import argparse import awkward as ak + import geant4_python_application as g4 diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py index a562728b..c691fbec 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/parameterisations/Par01/par01.py @@ -21,7 +21,13 @@ def main() -> None: gdml = g4.geometry.linear_array_gdml(args.cells, spacing_mm=15, box_size_mm=5) with g4.Application(gdml=gdml, seed=137) as app: - app.commands(["/gun/particle geantino", "/gun/position -120 0 0 mm", "/gun/direction 1 0 0"]) + app.commands( + [ + "/gun/particle geantino", + "/gun/position -120 0 0 mm", + "/gun/direction 1 0 0", + ] + ) if not args.batch: app.visualize() return diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py index d70905c3..6fb0124f 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/physicslists/factory/factory.py @@ -15,15 +15,26 @@ def main() -> None: args = parser.parse_args() available = g4.Application.available_physics_lists() if args.physics not in available: - raise SystemExit(f"unknown physics list {args.physics!r}; choose from: {', '.join(available)}") + raise SystemExit( + f"unknown physics list {args.physics!r}; choose from: {', '.join(available)}" + ) with g4.Application(gdml=g4.basic_gdml, physics=args.physics, seed=137) as app: - app.commands(["/gun/particle proton", "/gun/energy 1 GeV", "/gun/position 0 0 -400 mm", "/gun/direction 0 0 1"]) + app.commands( + [ + "/gun/particle proton", + "/gun/energy 1 GeV", + "/gun/position 0 0 -400 mm", + "/gun/direction 0 0 1", + ] + ) if not args.batch: app.visualize() return events = app.run(args.events) - print(f"physics list: {args.physics}; events: {len(events)}; factory lists: {len(available)}") + print( + f"physics list: {args.physics}; events: {len(events)}; factory lists: {len(available)}" + ) if __name__ == "__main__": diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py index 7e1983de..ccff8acf 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/radioactivedecay/rdecay01/rdecay01.py @@ -1,18 +1,38 @@ """Python adaptation of extended/radioactivedecay/rdecay01.""" + from __future__ import annotations + import argparse from collections import Counter + import awkward as ak + import geant4_python_application as g4 + def main(): - p=argparse.ArgumentParser(description=__doc__); p.add_argument("-n","--events",type=int,default=100); p.add_argument("--batch",action="store_true"); a=p.parse_args() - with g4.Application(gdml=g4.basic_gdml,physics="FTFP_BERT",seed=137) as app: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("-n", "--events", type=int, default=100) + p.add_argument("--batch", action="store_true") + a = p.parse_args() + with g4.Application(gdml=g4.basic_gdml, physics="FTFP_BERT", seed=137) as app: app.add_physics("G4RadioactiveDecayPhysics") app.initialize() # ion definitions must exist before /gun/ion - app.commands(["/gun/particle ion","/gun/ion 10 24 0 0","/gun/energy 0 keV","/gun/position 0 0 0 cm"]) - if not a.batch: app.visualize(); return - events=app.run(a.events) - particles=Counter(ak.to_list(ak.flatten(events.track.particle,axis=None))) + app.commands( + [ + "/gun/particle ion", + "/gun/ion 10 24 0 0", + "/gun/energy 0 keV", + "/gun/position 0 0 0 cm", + ] + ) + if not a.batch: + app.visualize() + return + events = app.run(a.events) + particles = Counter(ak.to_list(ak.flatten(events.track.particle, axis=None))) print(f"events: {len(events)}, tracked particles: {dict(particles)}") -if __name__=="__main__": main() + + +if __name__ == "__main__": + main() diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py index 33cb7a25..8a8d4506 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/extended/runAndEvent/RE01/re01.py @@ -10,6 +10,7 @@ import argparse import awkward as ak + import geant4_python_application as g4 @@ -28,7 +29,10 @@ def main() -> None: parser.add_argument("--batch", action="store_true") args = parser.parse_args() source = ak.Array( - [{"primaries": [primary("gamma", -10.0), primary("e-", 10.0)]} for _ in range(args.events)] + [ + {"primaries": [primary("gamma", -10.0), primary("e-", 10.0)]} + for _ in range(args.events) + ] ) with g4.Application(gdml=g4.basic_gdml, seed=137) as app: diff --git a/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py b/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py index d37df0d8..40662bf9 100644 --- a/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py +++ b/examples/geant4-examples/Geant4-11.4.2-examples-python/run_gdml.py @@ -27,13 +27,16 @@ def main() -> None: if args.batch: print(f"events simulated: {len(app.run(args.events))}") else: - app.visualize([ - "/vis/open OGL", "/vis/drawVolume", - "/vis/viewer/set/style wireframe", - "/vis/scene/add/trajectories smooth", - "/tracking/storeTrajectory 1", - "/vis/scene/endOfEventAction accumulate 100", - ]) + app.visualize( + [ + "/vis/open OGL", + "/vis/drawVolume", + "/vis/viewer/set/style wireframe", + "/vis/scene/add/trajectories smooth", + "/tracking/storeTrajectory 1", + "/vis/scene/endOfEventAction accumulate 100", + ] + ) if __name__ == "__main__": diff --git a/licenses/Geant4-LICENSE b/licenses/Geant4-LICENSE index 3926c4ef..7fbe7107 100644 --- a/licenses/Geant4-LICENSE +++ b/licenses/Geant4-LICENSE @@ -26,7 +26,7 @@ license is subject to the following conditions: software itself. 3. The names "Geant4" and "The Geant4 toolkit" may not be used to endorse or - promote software,or products derived therefrom, except with prior written + promote software,or products derived there from, except with prior written permission by license@geant4.org. If this software is redistributed in modified form, the name and reference of the modified version must be clearly distinguishable from that of this software. diff --git a/licenses/zlib-LICENSE b/licenses/zlib-LICENSE index b457317f..3676638c 100644 --- a/licenses/zlib-LICENSE +++ b/licenses/zlib-LICENSE @@ -20,4 +20,3 @@ Copyright notice: Jean-loup Gailly Mark Adler jloup@gzip.org madler@alumni.caltech.edu - diff --git a/src/geant4_application/include/geant4_application/DetectorConstruction.h b/src/geant4_application/include/geant4_application/DetectorConstruction.h index 0de18e02..fa8b4e6b 100644 --- a/src/geant4_application/include/geant4_application/DetectorConstruction.h +++ b/src/geant4_application/include/geant4_application/DetectorConstruction.h @@ -8,8 +8,8 @@ #include #include #include -#include #include +#include class G4GlobalMagFieldMessenger; diff --git a/src/geant4_application/src/Application.cpp b/src/geant4_application/src/Application.cpp index ae383717..7be11ab7 100644 --- a/src/geant4_application/src/Application.cpp +++ b/src/geant4_application/src/Application.cpp @@ -6,12 +6,11 @@ #include "geant4_application/RunAction.h" #include "geant4_application/SteppingVerbose.h" -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include @@ -21,12 +20,13 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include #include -#include +#include +#include +#include #include #include diff --git a/src/geant4_application/src/DataModel.cpp b/src/geant4_application/src/DataModel.cpp index 56562d12..04fd0ef2 100644 --- a/src/geant4_application/src/DataModel.cpp +++ b/src/geant4_application/src/DataModel.cpp @@ -43,18 +43,18 @@ void InsertEvent(const G4Event* event, Builders& builders) { if (builders.fields.find("primaries") != builders.fields.end()) { builders.primaries.begin_list(); for (auto primaryVertex = event->GetPrimaryVertex(); primaryVertex != nullptr; primaryVertex = primaryVertex->GetNext()) { - for (int i = 0; i < primaryVertex->GetNumberOfParticle(); i++) { - const auto primary = primaryVertex->GetPrimary(i); - const auto particle = primary->GetParticleDefinition(); - const auto energy = primary->GetKineticEnergy(); - const auto position = primaryVertex->GetPosition(); - const auto direction = primary->GetMomentumDirection(); + for (int i = 0; i < primaryVertex->GetNumberOfParticle(); i++) { + const auto primary = primaryVertex->GetPrimary(i); + const auto particle = primary->GetParticleDefinition(); + const auto energy = primary->GetKineticEnergy(); + const auto position = primaryVertex->GetPosition(); + const auto direction = primary->GetMomentumDirection(); - builders.primaries.content().content<0>().append(particle->GetParticleName()); - builders.primaries.content().content<1>().append(energy / units::energy); - builders.primaries.content().content<2>().append(position / units::length); - builders.primaries.content().content<3>().append(direction); - } + builders.primaries.content().content<0>().append(particle->GetParticleName()); + builders.primaries.content().content<1>().append(energy / units::energy); + builders.primaries.content().content<2>().append(position / units::length); + builders.primaries.content().content<3>().append(direction); + } } builders.primaries.end_list(); } diff --git a/src/geant4_application/src/DetectorConstruction.cpp b/src/geant4_application/src/DetectorConstruction.cpp index b7b1ca92..3eaa68d9 100644 --- a/src/geant4_application/src/DetectorConstruction.cpp +++ b/src/geant4_application/src/DetectorConstruction.cpp @@ -3,18 +3,18 @@ #include "geant4_application/Application.h" #include "geant4_application/SensitiveDetector.h" -#include -#include -#include #include -#include -#include +#include +#include #include #include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include #include #include @@ -39,11 +39,12 @@ class UniformEMField : public G4ElectroMagneticField { field[5] = eField.z(); } G4bool DoesFieldChangeEnergy() const override { return true; } + private: G4ThreeVector bField; G4ThreeVector eField; }; -} // namespace +}// namespace G4ThreadLocal G4GlobalMagFieldMessenger* DetectorConstruction::magneticFieldMessenger = nullptr; diff --git a/src/geant4_python_application/__init__.py b/src/geant4_python_application/__init__.py index 29df4537..33b8cdc1 100644 --- a/src/geant4_python_application/__init__.py +++ b/src/geant4_python_application/__init__.py @@ -1,5 +1,10 @@ from __future__ import annotations +from geant4_python_application import cad as cad +from geant4_python_application import distributed as distributed +from geant4_python_application import geometry as geometry +from geant4_python_application import io as io +from geant4_python_application import optical as optical from geant4_python_application._geant4_application import ( awkward_version, geant4_version, @@ -12,15 +17,10 @@ application_data_directory as data_directory, ) from geant4_python_application.files.datasets import install_datasets -from geant4_python_application.generator import Generator -from geant4_python_application.scoring import NativeScoringMesh, Scoring from geant4_python_application.files.directories import application_directory from geant4_python_application.gdml import basic_gdml -from geant4_python_application import optical as optical -from geant4_python_application import cad as cad -from geant4_python_application import io as io -from geant4_python_application import distributed as distributed -from geant4_python_application import geometry as geometry +from geant4_python_application.generator import Generator +from geant4_python_application.scoring import NativeScoringMesh, Scoring __version__ = version __version__tuple__ = version_tuple diff --git a/src/geant4_python_application/application.py b/src/geant4_python_application/application.py index 148c8b47..efe5de61 100644 --- a/src/geant4_python_application/application.py +++ b/src/geant4_python_application/application.py @@ -209,9 +209,7 @@ def setup_manager(self, n_threads: int = 0) -> Application: def setup_physics( self, physics_list: str = "custom", *, optical: bool = False ) -> Application: - self._send_and_recv( - Message("", "setup_physics", (physics_list, optical), {}) - ) + self._send_and_recv(Message("", "setup_physics", (physics_list, optical), {})) return self @staticmethod diff --git a/src/geant4_python_application/cad.py b/src/geant4_python_application/cad.py index 74987ab4..b5c4e5f8 100644 --- a/src/geant4_python_application/cad.py +++ b/src/geant4_python_application/cad.py @@ -75,16 +75,28 @@ def cube_mesh(size_mm: float = 100.0) -> tuple[list, list]: """Return (vertices, faces) for an axis-aligned cube centred at origin.""" h = size_mm / 2.0 v = [ - (-h, -h, -h), (h, -h, -h), (h, h, -h), (-h, h, -h), - (-h, -h, h), (h, -h, h), (h, h, h), (-h, h, h), + (-h, -h, -h), + (h, -h, -h), + (h, h, -h), + (-h, h, -h), + (-h, -h, h), + (h, -h, h), + (h, h, h), + (-h, h, h), ] f = [ - (0, 1, 2), (0, 2, 3), # -z - (4, 6, 5), (4, 7, 6), # +z - (0, 4, 5), (0, 5, 1), # -y - (3, 2, 6), (3, 6, 7), # +y - (0, 3, 7), (0, 7, 4), # -x - (1, 5, 6), (1, 6, 2), # +x + (0, 1, 2), + (0, 2, 3), # -z + (4, 6, 5), + (4, 7, 6), # +z + (0, 4, 5), + (0, 5, 1), # -y + (3, 2, 6), + (3, 6, 7), # +y + (0, 3, 7), + (0, 7, 4), # -x + (1, 5, 6), + (1, 6, 2), # +x ] return v, f diff --git a/src/geant4_python_application/events.py b/src/geant4_python_application/events.py index 1d1b2d49..c5aaa39e 100644 --- a/src/geant4_python_application/events.py +++ b/src/geant4_python_application/events.py @@ -24,7 +24,9 @@ def hits(self, volume: str | None = None) -> ak.Array: if volume is not None: hits = hits[hits.volume == volume] # Keep available hit fields; event-field selection may omit time or position. - fields = [field for field in ("position", "time", "energy") if field in hits.fields] + fields = [ + field for field in ("position", "time", "energy") if field in hits.fields + ] hits = hits[fields] return ak.Array(hits, with_name="Geant4Hits") diff --git a/src/geant4_python_application/generator.py b/src/geant4_python_application/generator.py index 4b69a268..fd592760 100644 --- a/src/geant4_python_application/generator.py +++ b/src/geant4_python_application/generator.py @@ -16,7 +16,9 @@ def __init__(self, application: Application): @property def type(self) -> str: - return self._application._send_and_recv(Message("generator", "get_type", (), {})) + return self._application._send_and_recv( + Message("generator", "get_type", (), {}) + ) @type.setter def type(self, generator_type: str): @@ -42,9 +44,7 @@ def energy(self, value: float, unit: str = "MeV") -> Generator: self._application.command(f"{command} {value} {unit}") return self - def position( - self, x: float, y: float, z: float, unit: str = "cm" - ) -> Generator: + def position(self, x: float, y: float, z: float, unit: str = "cm") -> Generator: if self.type == "gps": self._application.command("/gps/pos/type Point") self._application.command(f"/gps/pos/centre {x} {y} {z} {unit}") diff --git a/src/geant4_python_application/io.py b/src/geant4_python_application/io.py index cdb66ec3..11286807 100644 --- a/src/geant4_python_application/io.py +++ b/src/geant4_python_application/io.py @@ -63,7 +63,9 @@ def events_from_parquet(filename: str | Path) -> ak.Array: return ak.from_parquet(Path(filename)) -def events_to_root(events: ak.Array, filename: str | Path, treename: str = "events") -> Path: +def events_to_root( + events: ak.Array, filename: str | Path, treename: str = "events" +) -> Path: """Write flat per-event summary + hits to ROOT via uproot (requires uproot).""" try: import uproot @@ -76,7 +78,11 @@ def events_to_root(events: ak.Array, filename: str | Path, treename: str = "even import geant4_python_application as g4 edep = np.asarray(g4.Scoring.energy_deposit(events), dtype=np.float64) - ids = np.asarray(events.id, dtype=np.int64) if "id" in events.fields else np.arange(len(events)) + ids = ( + np.asarray(events.id, dtype=np.int64) + if "id" in events.fields + else np.arange(len(events)) + ) with uproot.recreate(out) as f: f[treename] = {"id": ids, "edep_keV": edep} return out @@ -93,10 +99,22 @@ def events_from_root(filename: str | Path, treename: str = "events") -> dict: _PDG_TO_G4 = { - 11: "e-", -11: "e+", 22: "gamma", 13: "mu-", -13: "mu+", - 2112: "neutron", 2212: "proton", 211: "pi+", -211: "pi-", - 111: "pi0", 321: "kaon+", -321: "kaon-", 12: "nu_e", - -12: "anti_nu_e", 14: "nu_mu", -14: "anti_nu_mu", + 11: "e-", + -11: "e+", + 22: "gamma", + 13: "mu-", + -13: "mu+", + 2112: "neutron", + 2212: "proton", + 211: "pi+", + -211: "pi-", + 111: "pi0", + 321: "kaon+", + -321: "kaon-", + 12: "nu_e", + -12: "anti_nu_e", + 14: "nu_mu", + -14: "anti_nu_mu", } diff --git a/src/geant4_python_application/optical.py b/src/geant4_python_application/optical.py index 15287350..413f424b 100644 --- a/src/geant4_python_application/optical.py +++ b/src/geant4_python_application/optical.py @@ -189,7 +189,9 @@ ] -def scintillation_commands(stack_photons: bool = True, max_cerenkov: int = 100) -> list[str]: +def scintillation_commands( + stack_photons: bool = True, max_cerenkov: int = 100 +) -> list[str]: """Return UI commands tuning scintillation/Cerenkov at run time (Geant4 11.2).""" return [ f"/process/optical/scintillation/setStackPhotons {'true' if stack_photons else 'false'}", diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 352ac736..59da7177 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -7,8 +7,7 @@ import pytest import requests -from geant4_python_application import Application -from geant4_python_application import Scoring +from geant4_python_application import Application, Scoring complexGdml = requests.get( "https://raw.githubusercontent.com/rest-for-physics/restG4/dc3a8f42cea4978206a13325261fa85ec1b26261/examples/13.IAXO/geometry/setup.gdml" diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 739b8b42..92d61253 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -1,7 +1,6 @@ from __future__ import annotations import awkward as ak -import numpy as np import geant4_python_application as g4 from geant4_python_application import cad @@ -72,7 +71,12 @@ def test_callbacks(): on_step=lambda st, tr, ev: counts.__setitem__("step", counts["step"] + 1), ) assert len(events) == 2 - assert counts == {"run": 1, "event": 2, "track": counts["track"], "step": counts["step"]} + assert counts == { + "run": 1, + "event": 2, + "track": counts["track"], + "step": counts["step"], + } assert counts["track"] > 0 and counts["step"] > 0 diff --git a/tests/test_run.py b/tests/test_run.py index a501f6f3..eac82675 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -54,10 +54,12 @@ def test_multiple_primary_vertices_per_event(): "direction": {"x": 0.0, "y": 0.0, "z": 1.0}, "position": {"x": x, "y": 0.0, "z": 0.0}, } - source = ak.Array([ - {"primaries": [particle("gamma", -1.0), particle("e-", 1.0)]}, - {"primaries": [particle("gamma", 0.0)]}, - ]) + source = ak.Array( + [ + {"primaries": [particle("gamma", -1.0), particle("e-", 1.0)]}, + {"primaries": [particle("gamma", 0.0)]}, + ] + ) with Application(gdml=basic_gdml) as app: events = app.run(source) assert ak.to_list(ak.num(events.primaries)) == [2, 1] diff --git a/tools/audit_examples.py b/tools/audit_examples.py index 6e5ce1ac..30af3b4b 100755 --- a/tools/audit_examples.py +++ b/tools/audit_examples.py @@ -14,7 +14,9 @@ def official_inventory(root: Path) -> dict: - targets = sorted(p.parent.relative_to(root).as_posix() for p in root.rglob("CMakeLists.txt")) + targets = sorted( + p.parent.relative_to(root).as_posix() for p in root.rglob("CMakeLists.txt") + ) gdml = sorted(root.rglob("*.gdml")) standalone: list[str] = [] fragments: list[str] = [] @@ -48,7 +50,9 @@ def python_ports(root: Path, events: int, timeout: int) -> list[dict]: results: list[dict] = [] for path in sorted(root.rglob("*.py")): if path.name == "run_gdml.py": - results.append({"path": path.relative_to(root).as_posix(), "status": "utility"}) + results.append( + {"path": path.relative_to(root).as_posix(), "status": "utility"} + ) continue text = path.read_text(errors="replace") if "--batch" not in text: @@ -111,7 +115,9 @@ def markdown(report: dict) -> str: "|---|---:|---:|", ] for item in results: - lines.append(f"| `{item['path']}` | {item['status']} | {item.get('seconds', '')} |") + lines.append( + f"| `{item['path']}` | {item['status']} | {item.get('seconds', '')} |" + ) lines.append("") lines.append("Only entries marked `passed` are validated as runnable Python ports.") return "\n".join(lines) + "\n" @@ -139,7 +145,9 @@ def main() -> int: args.markdown.parent.mkdir(parents=True, exist_ok=True) args.markdown.write_text(rendered) print(rendered, end="") - failed = any(item["status"] in {"failed", "timeout"} for item in report["python_ports"]) + failed = any( + item["status"] in {"failed", "timeout"} for item in report["python_ports"] + ) return 1 if failed else 0 diff --git a/tools/relink_qt_wheel.py b/tools/relink_qt_wheel.py index 0bf5e0f6..b4175dd5 100644 --- a/tools/relink_qt_wheel.py +++ b/tools/relink_qt_wheel.py @@ -32,9 +32,11 @@ def run(*args: str) -> str: - result = subprocess.run(args, capture_output=True, text=True) + result = subprocess.run(args, check=False, capture_output=True, text=True) if result.returncode != 0: - sys.exit(f"Command failed ({result.returncode}): {' '.join(args)}\n{result.stderr}{result.stdout}") + sys.exit( + f"Command failed ({result.returncode}): {' '.join(args)}\n{result.stderr}{result.stdout}" + ) return result.stdout @@ -61,14 +63,18 @@ def rpath_to_qt(root: Path, binary: Path, origin: str) -> str: def relink_macos(root: Path, binary: Path) -> bool: load_commands = run("otool", "-L", str(binary)).splitlines()[1:] - references = [m.groups() for line in load_commands if (m := MACOS_QT_FRAMEWORK.match(line))] + references = [ + m.groups() for line in load_commands if (m := MACOS_QT_FRAMEWORK.match(line)) + ] if not references: return False for old, framework in references: if old != f"@rpath/{framework}": run("install_name_tool", "-change", old, f"@rpath/{framework}", str(binary)) rpath = rpath_to_qt(root, binary, "@loader_path") - rpaths = re.findall(r"^\s+path (\S+) \(offset", run("otool", "-l", str(binary)), re.M) + rpaths = re.findall( + r"^\s+path (\S+) \(offset", run("otool", "-l", str(binary)), re.M + ) # Absolute rpaths only point into the build machine (e.g. its Qt or Geant4). for stale in (p for p in rpaths if p.startswith("/")): run("install_name_tool", "-delete_rpath", stale, str(binary)) @@ -84,7 +90,9 @@ def relink_linux(root: Path, binary: Path) -> bool: needed = run("patchelf", "--print-needed", str(binary)).split() if not any(LINUX_QT_LIBRARY.match(name) for name in needed): return False - entries = [e for e in run("patchelf", "--print-rpath", str(binary)).strip().split(":") if e] + entries = [ + e for e in run("patchelf", "--print-rpath", str(binary)).strip().split(":") if e + ] rpath = rpath_to_qt(root, binary, "$ORIGIN") if rpath not in entries: run("patchelf", "--set-rpath", ":".join([*entries, rpath]), str(binary)) @@ -93,8 +101,12 @@ def relink_linux(root: Path, binary: Path) -> bool: def main() -> None: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - parser.add_argument("wheel", type=Path, help="wheel already repaired by delocate/auditwheel") - parser.add_argument("-w", "--wheel-dir", type=Path, required=True, help="output directory") + parser.add_argument( + "wheel", type=Path, help="wheel already repaired by delocate/auditwheel" + ) + parser.add_argument( + "-w", "--wheel-dir", type=Path, required=True, help="output directory" + ) args = parser.parse_args() relink = relink_macos if sys.platform == "darwin" else relink_linux @@ -102,13 +114,21 @@ def main() -> None: root = Path(tmp) / "wheel" unpack(args.wheel, root) - bundled = [str(p.relative_to(root)) for p in root.rglob("*") if BUNDLED_QT.match(p.name)] + bundled = [ + str(p.relative_to(root)) + for p in root.rglob("*") + if BUNDLED_QT.match(p.name) + ] if bundled: - sys.exit(f"Qt was bundled into the wheel; exclude it during repair: {bundled}") + sys.exit( + f"Qt was bundled into the wheel; exclude it during repair: {bundled}" + ) relinked = [b for b in binaries(root) if relink(root, b)] if not relinked: - sys.exit("No bundled binary links Qt; was the wheel built with the viewer enabled?") + sys.exit( + "No bundled binary links Qt; was the wheel built with the viewer enabled?" + ) (root / MARKER).write_text("PyQt5-Qt5\n") args.wheel_dir.mkdir(parents=True, exist_ok=True) diff --git a/tools/smoke_viewer.py b/tools/smoke_viewer.py index 2e195763..18355038 100644 --- a/tools/smoke_viewer.py +++ b/tools/smoke_viewer.py @@ -16,7 +16,9 @@ VIEWER_SECONDS = 45 if not g4.Application.visualization_available(): - sys.exit("Qt viewer unavailable: viewer module missing or the [gui] extra not installed") + sys.exit( + "Qt viewer unavailable: viewer module missing or the [gui] extra not installed" + ) # The viewer module must resolve every Qt library it links against. subprocess.run(