Repository navigation
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to support heavy-ion beams by moving particle identification from internal id values to PDG codes, and by deriving heavy-ion A/Z from ion PDG codes across SHIELD-HIT12A, Geant4, and FLUKA converters.
Changes:
- Refactors particle dictionaries/maps to be keyed by PDG codes and updates parsing paths to use
particle.pdg. - Adds heavy-ion handling by extracting
AandZfrom PDG ion encodings (≥ 1,000,000,000) in multiple backends. - Refactors
convert_beam_energyto take a per-particle metadata dict instead of(particles_dict, particle_id, a, ...).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pyproject.toml | Adds Poetry metadata fields under [tool.poetry] (duplicating [project] metadata). |
| converter/common.py | Changes convert_beam_energy API to accept particle metadata directly. |
| converter/shieldhit/parser.py | Switches to PDG-based particle selection and adds heavy-ion A/Z extraction. |
| converter/geant4/Geant4MacroGenerator.py | Switches to PDG-based particle mapping and adds heavy-ion ion-source handling. |
| converter/fluka/helper_parsers/scoring_parser.py | Switches FLUKA scoring particle filters to use PDG codes. |
| converter/fluka/helper_parsers/beam_parser.py | Switches FLUKA beam parsing to use PDG codes and adds heavy-ion PDG support. |
Suppressed comments (4)
converter/shieldhit/parser.py:220
- For heavy ions (pdg >= 1_000_000_000), beam_config.particle is left as None, but BeamConfig.str expects an int particle code (and only emits the heavy-ion HIPROJ line when particle == 25). This will generate an invalid beam.dat (JPART0 "None") and omit HIPROJ for ions.
self.beam_config.particle = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("particle")
self.beam_config.particle_name = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("name", 'HEAVYION' )
if PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("a") is not None:
self.beam_config.heavy_ion_a = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("a")
if PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("z") is not None:
self.beam_config.heavy_ion_z = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("z")
pdg = json["beam"]["particle"]["pdg"]
if pdg>= 1000000000:
self.beam_config.heavy_ion_a = pdg % 10000 // 10
self.beam_config.heavy_ion_z = pdg % 10000000 // 10000
converter/shieldhit/parser.py:286
- _parse_beam_energy calls convert_beam_energy(particle_parser_metadata, a, input_energy, input_energy_unit), but convert_beam_energy is now defined as convert_beam_energy(particle_parser_metadata, energy, energy_unit). This will raise a TypeError at runtime.
def _parse_beam_energy(self, json):
"""Parse beam energy-related fields with correct energy unit"""
particle_id = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("particle", 25)
input_energy = json["beam"]["energy"]
input_energy_unit = json["beam"].get("energyUnit", "MeV")
a = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("a", 1)
pdg = json["beam"]["particle"]["pdg"]
if pdg >= 1000000000:
a = json["beam"]["particle"]["a"]
particle_parser_metadata = {
'name': 'HEAVYION',
'allowed_units': ['MeV', 'MeV/nucl'],
'target_unit': 'MeV/nucl'
}
elif particle_id is not None:
particle_parser_metadata = PARTICLE_DICT[json["beam"]["particle"]["pdg"]]
else:
raise ValueError(f"Unsupported particle pdg: {json['beam']['particle']['pdg']}")
energy, energy_unit, energy_scale_factor = convert_beam_energy(particle_parser_metadata, a, input_energy,
input_energy_unit)
converter/shieldhit/parser.py:281
- Unsupported non-ion PDGs currently fall into a KeyError path: particle_id defaults to 25 when the PDG is missing from PARTICLE_DICT, making the
elif particle_id is not None:branch always taken and then indexing PARTICLE_DICT[pdg]. The intended ValueError for unsupported particles is effectively unreachable for pdg < 1_000_000_000.
particle_id = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("particle", 25)
input_energy = json["beam"]["energy"]
input_energy_unit = json["beam"].get("energyUnit", "MeV")
a = PARTICLE_DICT.get(json["beam"]["particle"]["pdg"], {}).get("a", 1)
pdg = json["beam"]["particle"]["pdg"]
if pdg >= 1000000000:
a = json["beam"]["particle"]["a"]
particle_parser_metadata = {
'name': 'HEAVYION',
'allowed_units': ['MeV', 'MeV/nucl'],
'target_unit': 'MeV/nucl'
}
elif particle_id is not None:
particle_parser_metadata = PARTICLE_DICT[json["beam"]["particle"]["pdg"]]
else:
raise ValueError(f"Unsupported particle pdg: {json['beam']['particle']['pdg']}")
converter/geant4/Geant4MacroGenerator.py:107
- This code now reads beam.particle.pdg, but repository Geant4 test resources still provide only particle.id (e.g. tests/geant4/resources/project.json:402-407). Defaulting
pdgto 2 is also not a valid proton PDG (2212), so current fixtures/configs will either KeyError or map to an invalid particle.
beam = self.data.get("beam", {})
particle = beam.get("particle", {})
particle_pdg = beam.get("particle", {}).get("pdg", 2)
pos = beam.get("position", [0, 0, 0])
direction = beam.get("direction", [0, 0, 1])
a = GEANT4_PARTICLE_MAP.get(particle_pdg, {}).get("a", 1)
z = GEANT4_PARTICLE_MAP.get(particle_pdg, {}).get("z", a)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Several concrete correctness bugs remain (a wrong-arity convert_beam_energy call that raises TypeError, heavy ions losing SHIELD-HIT particle=25, and mislabeled Geant4 PDG codes), and existing tests still use the old id field.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (6)
Previously missed (1) — in code that hasn't changed since the last review.
converter/shieldhit/parser.py:266
particle_iddefaults to25when the pdg is not found, so the laterelif particle_id is not None(line 278) is always true and theelsebranch raisingUnsupported particle pdg(line 281) is unreachable. For an unsupported non-ion pdg this then evaluatesPARTICLE_DICT[json["beam"]["particle"]["pdg"]]and raises aKeyErrorinstead of the intendedValueError. Remove the25default so unsupported particles fall through to theelsebranch.
converter/geant4/Geant4MacroGenerator.py:137
convert_beam_energywas refactored to the signature(particle_parser_metadata, energy, energy_unit), but this call still passesaas an extra positional argument. Python will raiseTypeError: convert_beam_energy() takes 3 positional arguments but 4 were given, so every beam-energy computation in the Geant4 generator fails at runtime. Drop theaargument (the mass number is now read fromparticle_parser_metadata).
energy, _, energy_scale_factor = convert_beam_energy(particle_parser_metadata, a, input_energy, input_energy_unit)
converter/geant4/Geant4MacroGenerator.py:43
- PDG code 14 is the muon neutrino, not the antimuon. The antimuon (
mu+) has PDG code-13(mu-is13). As written, amu+beam/filter (pdg-13) will not be found, while pdg14incorrectly maps tomu+. The sibling FLUKA map in this same PR uses-13forMUON+. Use-13here.
14: {
converter/geant4/Geant4MacroGenerator.py:48
- PDG code 111 is the neutral pion (pi0), not
pi-. The negative pion (pi-) has PDG code-211(pi+is211). The sibling FLUKA map uses-211forPION-and111forPIZERO. Using111here mislabels pi0 aspi-and leaves the real pi- (-211) unmapped. Use-211.
111: {
converter/geant4/Geant4MacroGenerator.py:19
- PDG code 22 is the photon (gamma), not geantino. Mapping
22togeantinomeans a photon beam would be emitted as a geantino (a non-interacting pseudo-particle) in the generated macro, which is incorrect. Map22to Geant4'sgamma, and if a geantino entry is still required give it a distinct key.
22: { # what is geantino and why it is twice in the list?
"name": "geantino",
converter/common.py:110
- Leftover commented-out code from the refactor should be removed, and
a=particle_parser_metadata.get('a', 1)is missing spaces around the assignment operator (pycodestyle E225 / yapf, both enforced via pre-commit).
# if particle_id not in particles_dict and particle_id < 1000000000:
# raise ValueError(f"Unsupported particle_id: {particle_id}")
# particle_parser_metadata = particles_dict[particle_id]
allowed_units = particle_parser_metadata["allowed_units"]
a=particle_parser_metadata.get('a', 1)
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Balanced
This pull request introduces a major refactor to unify particle handling across the codebase by switching from internal integer IDs to PDG codes for particle identification. It also improves support for heavy ions and ensures consistent particle mapping and energy conversion in both FLUKA and Geant4 backends. Additionally, utility functions for extracting heavy ion properties from PDG codes are added and used throughout. The most important changes are summarized below.
Particle Identification and Metadata Refactor:
All particle references in FLUKA and Geant4 code are now handled using PDG codes instead of custom integer IDs, ensuring standardization and easier interoperability. (
converter/fluka/helper_parsers/beam_parser.py,converter/geant4/Geant4MacroGenerator.py,converter/fluka/helper_parsers/scoring_parser.py,converter/common.py, [1] [2] [3] [4] [5] [6] [7]Particle metadata dictionaries (
PARTICLE_DICT,GEANT4_PARTICLE_MAP) have been updated to use PDG codes as keys and to include all relevant properties for energy conversion and name mapping. (converter/fluka/helper_parsers/beam_parser.py,converter/geant4/Geant4MacroGenerator.py, [1] [2]Heavy Ion and Utility Functions:
Added utility functions
is_heavy_ion,extract_mass_number, andextract_atomic_numbertoconverter/common.pyfor extracting heavy ion properties from PDG codes, and refactored code to use these utilities wherever heavy ions are handled. (converter/common.py, [1] [2] [3] [4]Improved handling of heavy ions in both FLUKA and Geant4: heavy ions are now correctly recognized, parsed, and named based on their PDG code, and custom filters or ion creation commands are generated as needed. (
converter/fluka/helper_parsers/beam_parser.py,converter/fluka/helper_parsers/scoring_parser.py,converter/geant4/Geant4MacroGenerator.py, [1] [2] [3] [4] [5]Energy Conversion and Parsing:
Refactored
convert_beam_energyto accept a particle metadata dictionary (includinga, allowed units, etc.) instead of relying on a global dictionary and particle ID, improving flexibility and clarity. (converter/common.py, [1] [2]Updated all usages of energy conversion and particle name parsing to use the new PDG-based approach and metadata structure, ensuring correctness for both light and heavy ions. (
converter/fluka/helper_parsers/beam_parser.py,converter/geant4/Geant4MacroGenerator.py, [1] [2]Output Formatting Improvements:
converter/fluka/cards/card.py, converter/fluka/cards/card.pyL28-R35)Filter and Scoring Updates:
converter/fluka/helper_parsers/scoring_parser.py,converter/geant4/Geant4MacroGenerator.py, [1] [2]These changes collectively modernize and standardize particle handling, improve robustness for heavy ion support, and make the codebase more maintainable and extensible.