Skip to content

Handle heavy ions - #268

Open
kanitsch wants to merge 25 commits into
masterfrom
handle-heavy-ions
Open

kanitsch wants to merge 25 commits into
masterfrom
handle-heavy-ions

Conversation

@kanitsch

@kanitsch kanitsch commented Jun 24, 2026 •

Copy link
Copy Markdown

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, and extract_atomic_number to converter/common.py for 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_energy to accept a particle metadata dictionary (including a, 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:

  • Improved formatting logic in FLUKA card output to handle long integer codes (such as heavy ion PDG codes) more gracefully, printing them as integers when necessary to fit field width constraints. (converter/fluka/cards/card.py, converter/fluka/cards/card.pyL28-R35)

Filter and Scoring Updates:

  • Updated filter creation and scoring logic to use PDG codes and new particle metadata, including correct handling of custom filters for heavy ions and correct Geant4 macro generation for scoring filters involving ions. (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.

Copilot AI lite review requested due to automatic review settings August 23, 2026 20:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 A and Z from PDG ion encodings (≥ 1,000,000,000) in multiple backends.
  • Refactors convert_beam_energy to 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 pdg to 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.

Comment thread pyproject.toml
Comment thread converter/common.py Outdated
Comment thread converter/shieldhit/parser.py Outdated
Comment thread converter/shieldhit/parser.py Outdated
Comment thread converter/geant4/Geant4MacroGenerator.py Outdated
Comment thread converter/geant4/Geant4MacroGenerator.py
Comment thread converter/fluka/helper_parsers/scoring_parser.py Outdated
Comment thread converter/fluka/helper_parsers/beam_parser.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_id defaults to 25 when the pdg is not found, so the later elif particle_id is not None (line 278) is always true and the else branch raising Unsupported particle pdg (line 281) is unreachable. For an unsupported non-ion pdg this then evaluates PARTICLE_DICT[json["beam"]["particle"]["pdg"]] and raises a KeyError instead of the intended ValueError. Remove the 25 default so unsupported particles fall through to the else branch.

converter/geant4/Geant4MacroGenerator.py:137

  • convert_beam_energy was refactored to the signature (particle_parser_metadata, energy, energy_unit), but this call still passes a as an extra positional argument. Python will raise TypeError: convert_beam_energy() takes 3 positional arguments but 4 were given, so every beam-energy computation in the Geant4 generator fails at runtime. Drop the a argument (the mass number is now read from particle_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- is 13). As written, a mu+ beam/filter (pdg -13) will not be found, while pdg 14 incorrectly maps to mu+. The sibling FLUKA map in this same PR uses -13 for MUON+. Use -13 here.
    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+ is 211). The sibling FLUKA map uses -211 for PION- and 111 for PIZERO. Using 111 here mislabels pi0 as pi- 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 22 to geantino means a photon beam would be emitted as a geantino (a non-interacting pseudo-particle) in the generated macro, which is incorrect. Map 22 to Geant4's gamma, 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

Comment thread converter/shieldhit/parser.py Outdated
Comment thread converter/geant4/Geant4MacroGenerator.py Outdated
Comment thread converter/geant4/Geant4MacroGenerator.py Outdated
Comment thread converter/fluka/helper_parsers/beam_parser.py Outdated

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants