Refactor ParticleType to use PDG Monte Carlo numbering scheme (#3756)

Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com>
Co-authored-by: Amanda Lund <alund1187@gmail.com>
This commit is contained in:
Paul Romano 2026-02-03 01:23:24 -06:00 committed by GitHub
parent fc0d9eec65
commit b41e22f68b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1401 additions and 558 deletions

View file

@ -372,6 +372,7 @@ list(APPEND libopenmc_SOURCES
src/particle.cpp
src/particle_data.cpp
src/particle_restart.cpp
src/particle_type.cpp
src/photon.cpp
src/physics.cpp
src/physics_common.cpp

View file

@ -10,7 +10,7 @@ may also be written after each batch when multiple files are requested
(``collision_track.N.h5``) or when the run is performed in parallel. The file
contains the information needed to reconstruct each recorded collision.
The current revision of the collision track file format is 1.0.
The current revision of the collision track file format is 1.1.
**/**
@ -37,9 +37,9 @@ The current revision of the collision track file format is 1.0.
- ``material_id`` (*int*) -- ID of the material containing the collision site.
- ``universe_id`` (*int*) -- ID of the universe containing the collision site.
- ``n_collision`` (*int*) -- Collision counter for the particle history.
- ``particle`` (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, 3=positron).
- ``parent_id`` (*int64*) -- Unique ID of the parent particle.
- ``progeny_id`` (*int64*) -- Progeny ID of the particle.
- ``particle`` (*int32_t*) -- Particle type (PDG number).
- ``parent_id`` (*int64_t*) -- Unique ID of the parent particle.
- ``progeny_id`` (*int64_t*) -- Progeny ID of the particle.
In an MPI run, OpenMC writes the combined dataset by gathering collision-track
entries from all ranks before flushing them to disk, so the final file appears

View file

@ -4,7 +4,7 @@
Particle Restart File Format
============================
The current version of the particle restart file format is 2.0.
The current version of the particle restart file format is 2.1.
**/**
@ -26,8 +26,7 @@ The current version of the particle restart file format is 2.0.
- **run_mode** (*char[]*) -- Run mode used, either 'fixed source',
'eigenvalue', or 'particle restart'.
- **id** (*int8_t*) -- Unique identifier of the particle.
- **type** (*int*) -- Particle type (0=neutron, 1=photon, 2=electron,
3=positron)
- **type** (*int32_t*) -- Particle type (PDG number)
- **weight** (*double*) -- Weight of the particle.
- **energy** (*double*) -- Energy of the particle in eV for
continuous-energy mode, or the energy group of the particle for

View file

@ -721,7 +721,10 @@ attributes/sub-elements:
is present.
:particle:
The source particle type, either ``neutron`` or ``photon``.
The source particle type, specified as a PDG number or a string alias (e.g.,
``neutron``/``n``, ``photon``/``gamma``, ``electron``, ``positron``,
``proton``/``p``, ``deuteron``/``d``, ``triton``/``t``, ``alpha``, or GNDS
nuclide names like ``Fe57``).
*Default*: neutron
@ -1537,7 +1540,8 @@ sub-elements/attributes:
*Default*: None
:particle_type:
The particle that the weight windows will apply to (e.g., 'neutron')
The particle that the weight windows will apply to, specified as a PDG
code or string (e.g., ``neutron``).
*Default*: 'neutron'
@ -1597,7 +1601,8 @@ mesh-based weight windows.
*Default*: None
:particle_type:
The particle that the weight windows will apply to (e.g., 'neutron')
The particle that the weight windows will apply to, specified as a PDG
code or string (e.g., ``neutron``).
*Default*: neutron

View file

@ -15,6 +15,8 @@ following the same format.
**/**
:Attributes: - **filetype** (*char[]*) -- String indicating the type of file.
- **version** (*int[2]*) -- Major and minor version of the source
file format.
:Datasets:
@ -22,5 +24,5 @@ following the same format.
particle. The compound type has fields ``r``, ``u``, ``E``,
``time``, ``wgt``, ``delayed_group``, ``surf_id`` and ``particle``,
which represent the position, direction, energy, time, weight,
delayed group, surface ID, and particle type (0=neutron, 1=photon,
2=electron, 3=positron), respectively.
delayed group, surface ID, and particle type (PDG number),
respectively.

View file

@ -4,7 +4,7 @@
State Point File Format
=======================
The current version of the statepoint file format is 18.1.
The current version of the statepoint file format is 18.2.
**/**
@ -56,8 +56,8 @@ The current version of the statepoint file format is 18.1.
``time``, ``wgt``, ``delayed_group``, ``surf_id``, and
``particle``, which represent the position, direction, energy,
time, weight, delayed group, surface ID, and particle type
(0=neutron, 1=photon, 2=electron, 3=positron), respectively. Only
present when `run_mode` is 'eigenvalue'.
(PDG number), respectively. Only present when `run_mode` is
'eigenvalue'.
**/tallies/**

View file

@ -318,8 +318,8 @@ should be set to:
they use ``energy`` and ``y``.
:particle:
A list of integers indicating the type of particles to tally ('neutron' = 1,
'photon' = 2, 'electron' = 3, 'positron' = 4).
A list of particle identifiers to tally, specified as strings (e.g.,
``neutron``, ``photon``, ``He4``) or as integer PDG numbers.
------------------
``<mesh>`` Element

View file

@ -4,7 +4,7 @@
Track File Format
=================
The current revision of the particle track file format is 3.0.
The current revision of the particle track file format is 3.1.
**/**
@ -32,6 +32,5 @@ The current revision of the particle track file format is 3.0.
the array for each primary/secondary particle. The
last offset should match the total size of the
array.
- **particles** (*int[]*) -- Particle type for each
primary/secondary particle (0=neutron, 1=photon,
2=electron, 3=positron).
- **particles** (*int32_t[]*) -- Particle type for
each primary/secondary particle (PDG number).

View file

@ -400,7 +400,7 @@ below.
{
openmc::SourceSite particle;
// weight
particle.particle = openmc::ParticleType::neutron;
particle.particle = openmc::ParticleType::neutron();
particle.wgt = 1.0;
// position
double angle = 2.0 * M_PI * openmc::prn(seed);
@ -477,7 +477,7 @@ parameters to the source class when it is created:
{
openmc::SourceSite particle;
// weight
particle.particle = openmc::ParticleType::neutron;
particle.particle = openmc::ParticleType::neutron();
particle.wgt = 1.0;
// position
particle.r.x = 0.0;

View file

@ -1,17 +1,16 @@
#include <cmath> // for M_PI
#include <cmath> // for M_PI
#include <memory> // for unique_ptr
#include "openmc/particle.h"
#include "openmc/random_lcg.h"
#include "openmc/source.h"
#include "openmc/particle.h"
class RingSource : public openmc::Source
{
class RingSource : public openmc::Source {
openmc::SourceSite sample(uint64_t* seed) const
{
openmc::SourceSite particle;
// particle type
particle.particle = openmc::ParticleType::neutron;
particle.particle = openmc::ParticleType::neutron();
// position
double angle = 2.0 * M_PI * openmc::prn(seed);
double radius = 3.0;
@ -25,10 +24,11 @@ class RingSource : public openmc::Source
}
};
// A function to create a unique pointer to an instance of this class when generated
// via a plugin call using dlopen/dlsym.
// You must have external C linkage here otherwise dlopen will not find the file
extern "C" std::unique_ptr<RingSource> openmc_create_source(std::string parameters)
// A function to create a unique pointer to an instance of this class when
// generated via a plugin call using dlopen/dlsym. You must have external C
// linkage here otherwise dlopen will not find the file
extern "C" std::unique_ptr<RingSource> openmc_create_source(
std::string parameters)
{
return std::make_unique<RingSource>();
}

View file

@ -1,63 +1,65 @@
#include <cmath> // for M_PI
#include <cmath> // for M_PI
#include <memory> // for unique_ptr
#include <unordered_map>
#include "openmc/particle.h"
#include "openmc/random_lcg.h"
#include "openmc/source.h"
#include "openmc/particle.h"
class RingSource : public openmc::Source {
public:
RingSource(double radius, double energy) : radius_(radius), energy_(energy) { }
public:
RingSource(double radius, double energy) : radius_(radius), energy_(energy) {}
// Defines a function that can create a unique pointer to a new instance of this class
// by extracting the parameters from the provided string.
static std::unique_ptr<RingSource> from_string(std::string parameters)
{
std::unordered_map<std::string, std::string> parameter_mapping;
// Defines a function that can create a unique pointer to a new instance of
// this class by extracting the parameters from the provided string.
static std::unique_ptr<RingSource> from_string(std::string parameters)
{
std::unordered_map<std::string, std::string> parameter_mapping;
std::stringstream ss(parameters);
std::string parameter;
while (std::getline(ss, parameter, ',')) {
parameter.erase(0, parameter.find_first_not_of(' '));
std::string key = parameter.substr(0, parameter.find_first_of('='));
std::string value = parameter.substr(parameter.find_first_of('=') + 1, parameter.length());
parameter_mapping[key] = value;
}
double radius = std::stod(parameter_mapping["radius"]);
double energy = std::stod(parameter_mapping["energy"]);
return std::make_unique<RingSource>(radius, energy);
std::stringstream ss(parameters);
std::string parameter;
while (std::getline(ss, parameter, ',')) {
parameter.erase(0, parameter.find_first_not_of(' '));
std::string key = parameter.substr(0, parameter.find_first_of('='));
std::string value =
parameter.substr(parameter.find_first_of('=') + 1, parameter.length());
parameter_mapping[key] = value;
}
// Samples from an instance of this class.
openmc::SourceSite sample(uint64_t* seed) const
{
openmc::SourceSite particle;
// particle type
particle.particle = openmc::ParticleType::neutron;
// position
double angle = 2.0 * M_PI * openmc::prn(seed);
double radius = this->radius_;
particle.r.x = radius * std::cos(angle);
particle.r.y = radius * std::sin(angle);
particle.r.z = 0.0;
// angle
particle.u = {1.0, 0.0, 0.0};
particle.E = this->energy_;
double radius = std::stod(parameter_mapping["radius"]);
double energy = std::stod(parameter_mapping["energy"]);
return std::make_unique<RingSource>(radius, energy);
}
return particle;
}
// Samples from an instance of this class.
openmc::SourceSite sample(uint64_t* seed) const
{
openmc::SourceSite particle;
// particle type
particle.particle = openmc::ParticleType::neutron();
// position
double angle = 2.0 * M_PI * openmc::prn(seed);
double radius = this->radius_;
particle.r.x = radius * std::cos(angle);
particle.r.y = radius * std::sin(angle);
particle.r.z = 0.0;
// angle
particle.u = {1.0, 0.0, 0.0};
particle.E = this->energy_;
private:
double radius_;
double energy_;
return particle;
}
private:
double radius_;
double energy_;
};
// A function to create a unique pointer to an instance of this class when generated
// via a plugin call using dlopen/dlsym.
// You must have external C linkage here otherwise dlopen will not find the file
extern "C" std::unique_ptr<RingSource> openmc_create_source(std::string parameters)
// A function to create a unique pointer to an instance of this class when
// generated via a plugin call using dlopen/dlsym. You must have external C
// linkage here otherwise dlopen will not find the file
extern "C" std::unique_ptr<RingSource> openmc_create_source(
std::string parameters)
{
return RingSource::from_string(parameters);
}

View file

@ -201,8 +201,8 @@ int openmc_weight_windows_set_energy_bounds(
int32_t index, double* e_bounds, size_t e_bounds_size);
int openmc_weight_windows_get_energy_bounds(
int32_t index, const double** e_bounds, size_t* e_bounds_size);
int openmc_weight_windows_set_particle(int32_t index, int particle);
int openmc_weight_windows_get_particle(int32_t index, int* particle);
int openmc_weight_windows_set_particle(int32_t index, int32_t particle);
int openmc_weight_windows_get_particle(int32_t index, int32_t* particle);
int openmc_weight_windows_get_bounds(int32_t index, const double** lower_bounds,
const double** upper_bounds, size_t* size);
int openmc_weight_windows_set_bounds(int32_t index, const double* lower_bounds,
@ -227,7 +227,7 @@ int openmc_zernike_filter_set_order(int32_t index, int order);
int openmc_zernike_filter_set_params(
int32_t index, const double* x, const double* y, const double* r);
int openmc_particle_filter_get_bins(int32_t idx, int bins[]);
int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[]);
//! Sets the mesh and energy grid for CMFD reweight
//! \param[in] meshtyally_id id of CMFD Mesh Tally

View file

@ -25,16 +25,16 @@ using double_4dvec = vector<vector<vector<vector<double>>>>;
constexpr int HDF5_VERSION[] {3, 0};
// Version numbers for binary files
constexpr array<int, 2> VERSION_STATEPOINT {18, 1};
constexpr array<int, 2> VERSION_PARTICLE_RESTART {2, 0};
constexpr array<int, 2> VERSION_TRACK {3, 0};
constexpr array<int, 2> VERSION_STATEPOINT {18, 2};
constexpr array<int, 2> VERSION_PARTICLE_RESTART {2, 1};
constexpr array<int, 2> VERSION_TRACK {3, 1};
constexpr array<int, 2> VERSION_SUMMARY {6, 1};
constexpr array<int, 2> VERSION_VOLUME {1, 0};
constexpr array<int, 2> VERSION_VOXEL {2, 0};
constexpr array<int, 2> VERSION_MGXS_LIBRARY {1, 0};
constexpr array<int, 2> VERSION_PROPERTIES {1, 1};
constexpr array<int, 2> VERSION_WEIGHT_WINDOWS {1, 0};
constexpr array<int, 2> VERSION_COLLISION_TRACK {1, 0};
constexpr array<int, 2> VERSION_COLLISION_TRACK {1, 1};
// ============================================================================
// ADJUSTABLE PARAMETERS

View file

@ -163,7 +163,7 @@ bool multipole_in_range(const Nuclide& nuc, double E);
namespace data {
// Minimum/maximum transport energy for each particle type. Order corresponds to
// that of the ParticleType enum
// transport_index() for supported transport particles.
extern array<double, 4> energy_min;
extern array<double, 4> energy_max;

View file

@ -126,10 +126,6 @@ public:
//! Functions
//============================================================================
std::string particle_type_to_str(ParticleType type);
ParticleType str_to_particle_type(std::string str);
void add_surf_source_to_bank(Particle& p, const Surface& surf);
} // namespace openmc

View file

@ -3,6 +3,7 @@
#include "openmc/array.h"
#include "openmc/constants.h"
#include "openmc/particle_type.h"
#include "openmc/position.h"
#include "openmc/random_lcg.h"
#include "openmc/tallies/filter_match.h"
@ -30,9 +31,6 @@ constexpr double CACHE_INVALID {-1.0};
//==========================================================================
// Aliases and type definitions
//! Particle types
enum class ParticleType { neutron, photon, electron, positron };
//! Saved ("banked") state of a particle
//! NOTE: This structure's MPI type is built in initialize_mpi() of
//! initialize.cpp. Any changes made to the struct here must also be
@ -496,7 +494,7 @@ private:
MacroXS macro_xs_;
CacheDataMG mg_xs_cache_;
ParticleType type_ {ParticleType::neutron};
ParticleType type_;
double E_;
double E_last_;

View file

@ -0,0 +1,172 @@
//==============================================================================
// ParticleType class definition
//==============================================================================
#ifndef OPENMC_PARTICLE_TYPE_H
#define OPENMC_PARTICLE_TYPE_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <type_traits>
#include "openmc/constants.h"
namespace openmc {
//------------------------------------------------------------------------------
// PDG constants (canonical particle identity as simple integers)
//------------------------------------------------------------------------------
inline constexpr int32_t PDG_NEUTRON = 2112;
inline constexpr int32_t PDG_PHOTON = 22;
inline constexpr int32_t PDG_ELECTRON = 11;
inline constexpr int32_t PDG_POSITRON = -11;
inline constexpr int32_t PDG_PROTON = 2212;
inline constexpr int32_t PDG_DEUTERON = 1000010020;
inline constexpr int32_t PDG_TRITON = 1000010030;
inline constexpr int32_t PDG_ALPHA = 1000020040;
//------------------------------------------------------------------------------
// ParticleType class (standard-layout, trivially copyable)
//------------------------------------------------------------------------------
class ParticleType {
public:
//----------------------------------------------------------------------------
// Constructors
// Default constructor: defaults to neutron
constexpr ParticleType() : pdg_number_(PDG_NEUTRON) {}
// Constructor from PDG number
constexpr explicit ParticleType(int32_t pdg_number) : pdg_number_(pdg_number)
{}
// Constructor from particle name string (e.g., "neutron", "photon", "Fe56")
explicit ParticleType(std::string_view str);
// Constructor from Z, A, and metastable state for nuclear particles
constexpr ParticleType(int Z, int A, int m = 0)
: pdg_number_(1000000000 + Z * 10000 + A * 10 + m)
{}
//----------------------------------------------------------------------------
// Accessors
// Accessor for the underlying PDG number
constexpr int32_t pdg_number() const { return pdg_number_; }
//----------------------------------------------------------------------------
// Methods
// Convert to string representation
std::string str() const;
// Check if this represents a nucleus (vs elementary particle)
constexpr bool is_nucleus() const
{
// PDG nuclear codes are >= 1000000000 (100ZZZAAAI format)
return pdg_number_ >= 1000000000;
}
// Get transport index (0-3 for transportable particles, C_NONE otherwise)
constexpr int transport_index() const;
// Check if this is a neutron
constexpr bool is_neutron() const { return pdg_number_ == PDG_NEUTRON; }
// Check if this is a photon
constexpr bool is_photon() const { return pdg_number_ == PDG_PHOTON; }
constexpr bool is_transportable() const
{
return this->transport_index() != C_NONE;
}
//----------------------------------------------------------------------------
// Static factory methods
static constexpr ParticleType neutron() { return ParticleType {PDG_NEUTRON}; }
static constexpr ParticleType photon() { return ParticleType {PDG_PHOTON}; }
static constexpr ParticleType electron()
{
return ParticleType {PDG_ELECTRON};
}
static constexpr ParticleType positron()
{
return ParticleType {PDG_POSITRON};
}
static constexpr ParticleType proton() { return ParticleType {PDG_PROTON}; }
static constexpr ParticleType deuteron()
{
return ParticleType {PDG_DEUTERON};
}
static constexpr ParticleType triton() { return ParticleType {PDG_TRITON}; }
static constexpr ParticleType alpha() { return ParticleType {PDG_ALPHA}; }
private:
int32_t pdg_number_;
};
//------------------------------------------------------------------------------
// Static assertions to ensure standard-layout and trivially copyable
//------------------------------------------------------------------------------
static_assert(std::is_standard_layout_v<ParticleType>,
"ParticleType must be standard-layout");
static_assert(std::is_trivially_copyable_v<ParticleType>,
"ParticleType must be trivially copyable");
static_assert(sizeof(ParticleType) == sizeof(int32_t),
"ParticleType must be same size as int32_t");
//------------------------------------------------------------------------------
// Comparison operators (free functions for symmetry)
//------------------------------------------------------------------------------
constexpr bool operator==(ParticleType lhs, ParticleType rhs)
{
return lhs.pdg_number() == rhs.pdg_number();
}
constexpr bool operator!=(ParticleType lhs, ParticleType rhs)
{
return lhs.pdg_number() != rhs.pdg_number();
}
constexpr bool operator<(ParticleType lhs, ParticleType rhs)
{
return lhs.pdg_number() < rhs.pdg_number();
}
//------------------------------------------------------------------------------
// ParticleType member function implementations (inline)
//------------------------------------------------------------------------------
constexpr int ParticleType::transport_index() const
{
switch (pdg_number_) {
case PDG_NEUTRON:
return 0;
case PDG_PHOTON:
return 1;
case PDG_ELECTRON:
return 2;
case PDG_POSITRON:
return 3;
default:
return C_NONE;
}
}
//------------------------------------------------------------------------------
// Legacy conversion helpers
//------------------------------------------------------------------------------
// Legacy enum code (0..3) to ParticleType conversion
ParticleType legacy_particle_index_to_type(int code);
} // namespace openmc
#endif // OPENMC_PARTICLE_TYPE_H

View file

@ -10,7 +10,7 @@
#include "openmc/chain.h"
#include "openmc/endf.h"
#include "openmc/memory.h" // for unique_ptr
#include "openmc/particle.h"
#include "openmc/particle_type.h"
#include "openmc/vector.h" // for vector
namespace openmc {

View file

@ -12,7 +12,7 @@
#include "openmc/distribution_multi.h"
#include "openmc/distribution_spatial.h"
#include "openmc/memory.h"
#include "openmc/particle.h"
#include "openmc/particle_type.h"
#include "openmc/vector.h"
namespace openmc {
@ -148,11 +148,11 @@ protected:
private:
// Data members
ParticleType particle_ {ParticleType::neutron}; //!< Type of particle emitted
UPtrSpace space_; //!< Spatial distribution
UPtrAngle angle_; //!< Angular distribution
UPtrDist energy_; //!< Energy distribution
UPtrDist time_; //!< Time distribution
ParticleType particle_; //!< Type of particle emitted
UPtrSpace space_; //!< Spatial distribution
UPtrAngle angle_; //!< Angular distribution
UPtrDist energy_; //!< Energy distribution
UPtrDist time_; //!< Time distribution
};
//==============================================================================

View file

@ -1,7 +1,7 @@
#ifndef OPENMC_TALLIES_FILTER_PARTICLE_H
#define OPENMC_TALLIES_FILTER_PARTICLE_H
#include "openmc/particle.h"
#include "openmc/particle_type.h"
#include "openmc/span.h"
#include "openmc/tallies/filter.h"
#include "openmc/vector.h"

View file

@ -10,7 +10,7 @@
#include "openmc/constants.h"
#include "openmc/memory.h"
#include "openmc/mesh.h"
#include "openmc/particle.h"
#include "openmc/particle_type.h"
#include "openmc/span.h"
#include "openmc/tallies/tally.h"
#include "openmc/vector.h"
@ -193,10 +193,9 @@ public:
private:
//----------------------------------------------------------------------------
// Data members
int32_t id_; //!< Unique ID
int64_t index_; //!< Index into weight windows vector
ParticleType particle_type_ {
ParticleType::neutron}; //!< Particle type to apply weight windows to
int32_t id_; //!< Unique ID
int64_t index_; //!< Index into weight windows vector
ParticleType particle_type_; //!< Particle type to apply weight windows to
vector<double> energy_bounds_; //!< Energy boundaries [eV]
xt::xtensor<double, 2> lower_ww_; //!< Lower weight window bounds (shape:
//!< energy_bins, mesh_bins (k, j, i))

View file

@ -35,7 +35,6 @@ _CURRENT_NAMES = (
'z-min out', 'z-min in', 'z-max out', 'z-max in'
)
_PARTICLES = {'neutron', 'photon', 'electron', 'positron'}
class FilterMeta(ABCMeta):
@ -735,9 +734,8 @@ class ParticleFilter(Filter):
Parameters
----------
bins : str, or sequence of str
The particles to tally represented as strings ('neutron', 'photon',
'electron', 'positron').
bins : str, int, openmc.ParticleType, or sequence
The particle types to tally represented as names, PDG numbers, or types.
filter_id : int
Unique identifier for the filter
@ -763,11 +761,16 @@ class ParticleFilter(Filter):
@Filter.bins.setter
def bins(self, bins):
cv.check_type('bins', bins, Sequence, str)
if isinstance(bins, (str, Integral, openmc.ParticleType)):
bins = [bins]
else:
cv.check_type('bins', bins, Sequence,
(str, Integral, openmc.ParticleType))
bins = np.atleast_1d(bins)
for edge in bins:
cv.check_value('filter bin', edge, _PARTICLES)
self._bins = bins
normalized = []
for entry in bins:
normalized.append(str(openmc.ParticleType(entry)))
self._bins = np.array(normalized, dtype=str)
@classmethod
def from_hdf5(cls, group, **kwargs):

View file

@ -28,7 +28,7 @@ class _SourceSite(Structure):
('wgt', c_double),
('delayed_group', c_int),
('surf_id', c_int),
('particle', c_int),
('particle', c_int32),
('parent_nuclide', c_int),
('parent_id', c_int64),
('progeny_id', c_int64)]

View file

@ -132,6 +132,9 @@ _dll.openmc_meshsurface_filter_set_translation.errcheck = _error_handler
_dll.openmc_new_filter.argtypes = [c_char_p, POINTER(c_int32)]
_dll.openmc_new_filter.restype = c_int
_dll.openmc_new_filter.errcheck = _error_handler
_dll.openmc_particle_filter_get_bins.argtypes = [c_int32, POINTER(c_int32)]
_dll.openmc_particle_filter_get_bins.restype = c_int
_dll.openmc_particle_filter_get_bins.errcheck = _error_handler
_dll.openmc_spatial_legendre_filter_get_order.argtypes = [c_int32, POINTER(c_int)]
_dll.openmc_spatial_legendre_filter_get_order.restype = c_int
_dll.openmc_spatial_legendre_filter_get_order.errcheck = _error_handler
@ -402,8 +405,8 @@ class MeshFilter(Filter):
translation : Iterable of float
3-D coordinates of the translation vector
rotation : Iterable of float
The rotation matrix or angles of the filter mesh. This can either be
a fully specified 3 x 3 rotation matrix or an Iterable of length 3
The rotation matrix or angles of the filter mesh. This can either be
a fully specified 3 x 3 rotation matrix or an Iterable of length 3
with the angles in degrees about the x, y, and z axes, respectively.
"""
@ -454,7 +457,7 @@ class MeshFilter(Filter):
else:
raise ValueError(
f'Invalid size of rotation matrix: {rot_size}')
@rotation.setter
def rotation(self, rotation_data):
flat_rotation = np.asarray(rotation_data, dtype=float).flatten()
@ -598,9 +601,9 @@ class ParticleFilter(Filter):
@property
def bins(self):
particle_i = np.zeros((self.n_bins,), dtype=c_int)
particle_i = np.zeros((self.n_bins,), dtype=c_int32)
_dll.openmc_particle_filter_get_bins(
self._index, particle_i.ctypes.data_as(POINTER(c_int)))
self._index, particle_i.ctypes.data_as(POINTER(c_int32)))
return [ParticleType(i) for i in particle_i]

View file

@ -53,11 +53,11 @@ _dll.openmc_weight_windows_get_energy_bounds.argtypes = [c_int32, POINTER(POINTE
_dll.openmc_weight_windows_get_energy_bounds.restype = c_int
_dll.openmc_weight_windows_get_energy_bounds.errcheck = _error_handler
_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int]
_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int32]
_dll.openmc_weight_windows_set_particle.restype = c_int
_dll.openmc_weight_windows_set_particle.errcheck = _error_handler
_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int)]
_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int32)]
_dll.openmc_weight_windows_get_particle.restype = c_int
_dll.openmc_weight_windows_get_particle.errcheck = _error_handler
@ -201,16 +201,13 @@ class WeightWindows(_FortranObjectWithID):
@property
def particle(self):
val = c_int()
val = c_int32()
_dll.openmc_weight_windows_get_particle(self._index, val)
return ParticleType(val.value)
@particle.setter
def particle(self, p):
if isinstance(p, str):
p = ParticleType.from_string(p)
else:
p = ParticleType(p)
p = ParticleType(p)
_dll.openmc_weight_windows_set_particle(self._index, int(p))
@property
@ -304,10 +301,10 @@ class WeightWindows(_FortranObjectWithID):
----------
tally : openmc.lib.Tally
The tally used to create the WeightWindows instance.
particle : openmc.ParticleType or str, optional
particle : openmc.ParticleType or str or int, optional
The particle type to use for the WeightWindows instance. Should be
specified as an instance of ParticleType or as a string with a value of
'neutron' or 'photon'.
specified as an instance of ParticleType, a PDG number, or as a
name.
Returns
-------
@ -317,7 +314,8 @@ class WeightWindows(_FortranObjectWithID):
Raises
------
ValueError
If the particle parameter is not an instance of ParticleType or a string.
If the particle parameter is not an instance of ParticleType, a string,
or an integer PDG number.
ValueError
If the particle parameter is not a valid particle type (i.e., not 'neutron'
or 'photon').
@ -328,12 +326,13 @@ class WeightWindows(_FortranObjectWithID):
If the tally does not have a MeshFilter.
"""
# do some checks on particle value
if not isinstance(particle, (ParticleType, str)):
raise ValueError(f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon').")
if not isinstance(particle, (ParticleType, str, int)):
raise ValueError(
f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon')."
)
# convert particle type if needed
if isinstance(particle, str):
particle = ParticleType.from_string(particle)
particle = ParticleType(particle)
if particle not in (ParticleType.NEUTRON, ParticleType.PHOTON):
raise ValueError('Weight windows can only be applied for neutrons or photons')

View file

@ -1,6 +1,7 @@
import h5py
import openmc.checkvalue as cv
from .particle_type import ParticleType
_VERSION_PARTICLE_RESTART = 2
@ -28,8 +29,8 @@ class Particle:
Type of simulation (criticality or fixed source)
id : long
Identifier of the particle
type : int
Particle type (1 = neutron, 2 = photon, 3 = electron, 4 = positron)
type : openmc.ParticleType
Particle type
weight : float
Weight of the particle
energy : float
@ -52,7 +53,7 @@ class Particle:
self.energy = f['energy'][()]
self.generations_per_batch = f['generations_per_batch'][()]
self.id = f['id'][()]
self.type = f['type'][()]
self.type = ParticleType(f['type'][()])
self.n_particles = f['n_particles'][()]
self.run_mode = f['run_mode'][()].decode()
self.uvw = f['uvw'][()]

228
openmc/particle_type.py Normal file
View file

@ -0,0 +1,228 @@
from numbers import Integral
from openmc.data import gnds_name, zam, ATOMIC_SYMBOL
_PDG_NAME = {
2112: 'neutron',
22: 'photon',
11: 'electron',
-11: 'positron',
2212: 'H1',
}
_ALIAS_PDG = {
'neutron': 2112,
'n': 2112,
'photon': 22,
'gamma': 22,
'electron': 11,
'positron': -11,
'proton': 2212,
'p': 2212,
'h1': 2212,
'deuteron': 1000010020,
'd': 1000010020,
'h2': 1000010020,
'triton': 1000010030,
't': 1000010030,
'h3': 1000010030,
'alpha': 1000020040,
'he4': 1000020040,
}
_LEGACY_PARTICLE_INDEX = {
0: 2112,
1: 22,
2: 11,
3: -11,
}
class ParticleType:
"""Particle type defined by a PDG number.
ParticleType uses the Particle Data Group (PDG) Monte Carlo numbering scheme
to uniquely identify particle types. This includes elementary particles
(neutrons, photons, etc.) and nuclear codes for isotopes.
Parameters
----------
value : str, int, or ParticleType
The particle identifier. Can be:
- A string name (e.g., 'neutron', 'photon', 'He4', 'U235')
- An integer PDG number (e.g., 2112 for neutron)
- A string with PDG prefix (e.g., 'pdg:2112')
- An existing ParticleType instance
Attributes
----------
pdg_number : int
The PDG number for this particle type
zam : tuple of int or None
For nuclear particles, the (Z, A, m) tuple where Z is atomic number,
A is mass number, and m is metastable state. None for elementary particles.
is_nucleus : bool
Whether this particle is a nucleus (ion)
Examples
--------
>>> neutron = ParticleType('neutron')
>>> neutron.pdg_number
2112
>>> he4 = ParticleType('He4')
>>> he4.zam
(2, 4, 0)
>>> ParticleType(2112) == ParticleType('neutron')
True
"""
__slots__ = ('_pdg_number',)
def __init__(self, value: 'str | int | ParticleType'):
if isinstance(value, ParticleType):
pdg = value._pdg_number
elif isinstance(value, str):
pdg = self._pdg_number_from_string(value)
elif isinstance(value, Integral):
pdg = int(value)
# Handle legacy particle indices (0, 1, 2, 3)
if pdg in _LEGACY_PARTICLE_INDEX:
pdg = _LEGACY_PARTICLE_INDEX[pdg]
else:
raise TypeError(f"Cannot create ParticleType from {type(value).__name__}")
self._pdg_number = pdg
def __eq__(self, other):
if isinstance(other, ParticleType):
return self._pdg_number == other._pdg_number
if isinstance(other, Integral):
return self._pdg_number == int(other)
if isinstance(other, str):
try:
return self._pdg_number == ParticleType(other)._pdg_number
except (ValueError, TypeError):
return False
return NotImplemented
def __hash__(self) -> int:
return hash(self._pdg_number)
def __int__(self) -> int:
return self._pdg_number
@property
def pdg_number(self) -> int:
return self._pdg_number
@staticmethod
def _pdg_number_from_string(value: str) -> int:
"""Parse a string to get a PDG number.
Parameters
----------
value : str
Particle identifier string
Returns
-------
int
PDG number
Raises
------
ValueError
If string cannot be parsed as a valid particle identifier
"""
s = value.strip()
if not s:
raise ValueError('Particle identifier cannot be empty.')
lower = s.lower()
if lower.startswith('pdg:'):
code_str = lower[4:]
try:
return int(code_str)
except ValueError:
raise ValueError(f'Invalid PDG number: {code_str}')
if lower in _ALIAS_PDG:
return _ALIAS_PDG[lower]
# Assume it is a GNDS nuclide name
Z, A, m = zam(s)
if Z <= 0 or Z > 999 or A <= 0 or A > 999 or m < 0 or m > 9:
raise ValueError('Invalid Z/A/m for nuclear PDG number.')
return 1000000000 + Z * 10000 + A * 10 + m
def __repr__(self) -> str:
return f'<ParticleType: {str(self)} (PDG={self._pdg_number})>'
def __str__(self) -> str:
"""Return a canonical string representation of the particle type.
Returns
-------
str
Canonical name (e.g., 'neutron', 'He4', 'pdg:12345')
"""
if self._pdg_number in _PDG_NAME:
return _PDG_NAME[self._pdg_number]
if (zam_tuple := self.zam) is not None:
Z, A, m = zam_tuple
if Z <= 0 or Z > max(ATOMIC_SYMBOL) or A <= 0 or A > 999:
raise ValueError(f"Invalid nuclear PDG number: {self._pdg_number}")
return gnds_name(Z, A, m)
return f'pdg:{self._pdg_number}'
@property
def zam(self) -> 'tuple[int, int, int] | None':
"""Return the (Z, A, m) tuple for nuclear particles.
Returns
-------
tuple of int or None
For nuclear particles, returns (Z, A, m) where Z is atomic number,
A is mass number, and m is metastable state. Returns None for
elementary particles.
"""
if self._pdg_number < 1000000000:
return None
Z = (self._pdg_number // 10000) % 1000
A = (self._pdg_number // 10) % 1000
m = self._pdg_number % 10
if Z <= 0 or A <= 0:
return None
else:
return (Z, A, m)
@property
def is_nucleus(self) -> bool:
"""Return whether this particle is a nucleus.
Returns
-------
bool
True if the particle is a nucleus (ion), False otherwise
"""
return self.zam is not None
# Define common particle constants
ParticleType.NEUTRON = ParticleType(2112)
ParticleType.PHOTON = ParticleType(22)
ParticleType.ELECTRON = ParticleType(11)
ParticleType.POSITRON = ParticleType(-11)
ParticleType.PROTON = ParticleType(2212)
ParticleType.DEUTERON = ParticleType(1000010020)
ParticleType.TRITON = ParticleType(1000010030)
ParticleType.ALPHA = ParticleType(1000020040)

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable, Sequence
from enum import IntEnum
from numbers import Real
from pathlib import Path
import warnings
@ -19,6 +18,8 @@ from openmc.stats.multivariate import UnitSphere, Spatial
from openmc.stats.univariate import Univariate
from ._xml import get_elem_list, get_text
from .mesh import MeshBase, StructuredMesh, UnstructuredMesh
from .particle_type import ParticleType
from .statepoint import _VERSION_STATEPOINT
from .utility_funcs import input_path
@ -265,8 +266,8 @@ class IndependentSource(SourceBase):
time distribution of source sites
strength : float
Strength of the source
particle : {'neutron', 'photon', 'electron', 'positron'}
Source particle type
particle : str or int or openmc.ParticleType
Source particle type (name, PDG number, or type)
domains : iterable of openmc.Cell, openmc.Material, or openmc.Universe
Domains to reject based on, i.e., if a sampled spatial location is not
within one of these domains, it will be rejected.
@ -302,10 +303,9 @@ class IndependentSource(SourceBase):
type : str
Indicator of source type: 'independent'
.. versionadded:: 0.14.0
particle : {'neutron', 'photon', 'electron', 'positron'}
Source particle type
.. versionadded:: 0.14.0
particle : str or int or openmc.ParticleType
Source particle type (alias, PDG number, or GNDS nuclide name)
constraints : dict
Constraints on sampled source particles. Valid keys include
'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds',
@ -320,7 +320,7 @@ class IndependentSource(SourceBase):
energy: openmc.stats.Univariate | None = None,
time: openmc.stats.Univariate | None = None,
strength: float = 1.0,
particle: str = 'neutron',
particle: str | int | ParticleType = 'neutron',
domains: Sequence[openmc.Cell | openmc.Material |
openmc.Universe] | None = None,
constraints: dict[str, Any] | None = None
@ -405,14 +405,12 @@ class IndependentSource(SourceBase):
self._time = time
@property
def particle(self):
def particle(self) -> ParticleType:
return self._particle
@particle.setter
def particle(self, particle):
cv.check_value('source particle', particle,
['neutron', 'photon', 'electron', 'positron'])
self._particle = particle
self._particle = ParticleType(particle)
def populate_xml_element(self, element):
"""Add necessary source information to an XML element
@ -423,7 +421,7 @@ class IndependentSource(SourceBase):
XML element containing source data
"""
element.set("particle", self.particle)
element.set("particle", str(self.particle))
if self.space is not None:
element.append(self.space.to_xml_element())
if self.angle is not None:
@ -898,76 +896,6 @@ class FileSource(SourceBase):
return cls(**kwargs)
class ParticleType(IntEnum):
"""
IntEnum class representing a particle type. Type
values mirror those found in the C++ class.
"""
NEUTRON = 0
PHOTON = 1
ELECTRON = 2
POSITRON = 3
@classmethod
def from_string(cls, value: str):
"""
Constructs a ParticleType instance from a string.
Parameters
----------
value : str
The string representation of the particle type.
Returns
-------
The corresponding ParticleType instance.
"""
try:
return cls[value.upper()]
except KeyError:
raise ValueError(
f"Invalid string for creation of {cls.__name__}: {value}")
@classmethod
def from_pdg_number(cls, pdg_number: int) -> ParticleType:
"""Constructs a ParticleType instance from a PDG number.
The Particle Data Group at LBNL publishes a Monte Carlo particle
numbering scheme as part of the `Review of Particle Physics
<10.1103/PhysRevD.110.030001>`_. This method maps PDG numbers to the
corresponding :class:`ParticleType`.
Parameters
----------
pdg_number : int
The PDG number of the particle type.
Returns
-------
The corresponding ParticleType instance.
"""
try:
return {
2112: ParticleType.NEUTRON,
22: ParticleType.PHOTON,
11: ParticleType.ELECTRON,
-11: ParticleType.POSITRON,
}[pdg_number]
except KeyError:
raise ValueError(f"Unrecognized PDG number: {pdg_number}")
def __repr__(self) -> str:
"""
Returns a string representation of the ParticleType instance.
Returns:
str: The lowercase name of the ParticleType instance.
"""
return self.name.lower()
# needed for < Python 3.11
def __str__(self) -> str:
return self.__repr__()
class SourceParticle:
@ -992,8 +920,8 @@ class SourceParticle:
Delayed group particle was created in (neutrons only)
surf_id : int
Surface ID where particle is at, if any.
particle : ParticleType
Type of the particle
particle : ParticleType or str or int
Type of the particle (type, name, or PDG number)
"""
@ -1006,7 +934,7 @@ class SourceParticle:
wgt: float = 1.0,
delayed_group: int = 0,
surf_id: int = 0,
particle: ParticleType = ParticleType.NEUTRON
particle: ParticleType | str | int = ParticleType.NEUTRON
):
self.r = tuple(r)
@ -1018,9 +946,16 @@ class SourceParticle:
self.surf_id = surf_id
self.particle = particle
@property
def particle(self) -> ParticleType:
return self._particle
@particle.setter
def particle(self, particle):
self._particle = ParticleType(particle)
def __repr__(self):
name = self.particle.name.lower()
return f'<SourceParticle: {name} at E={self.E:.6e} eV>'
return f'<SourceParticle: {str(self.particle)} at E={self.E:.6e} eV>'
def to_tuple(self) -> tuple:
"""Return source particle attributes as a tuple
@ -1032,7 +967,7 @@ class SourceParticle:
"""
return (self.r, self.u, self.E, self.time, self.wgt,
self.delayed_group, self.surf_id, self.particle.value)
self.delayed_group, self.surf_id, self.particle.pdg_number)
def write_source_file(
@ -1116,12 +1051,7 @@ class ParticleList(list):
particles = []
with mcpl.MCPLFile(filename) as f:
for particle in f.particles:
# Determine particle type based on the PDG number
try:
particle_type = ParticleType.from_pdg_number(
particle.pdgcode)
except ValueError:
particle_type = "UNKNOWN"
particle_type = ParticleType(particle.pdgcode)
# Create a source particle instance. Note that MCPL stores
# energy in MeV and time in ms.
@ -1179,7 +1109,7 @@ class ParticleList(list):
# Extract the attributes of the source particles into a list of tuples
data = [(sp.r[0], sp.r[1], sp.r[2], sp.u[0], sp.u[1], sp.u[2],
sp.E, sp.time, sp.wgt, sp.delayed_group, sp.surf_id,
sp.particle.name.lower()) for sp in self]
str(sp.particle)) for sp in self]
# Define the column names for the DataFrame
columns = ['x', 'y', 'z', 'u_x', 'u_y', 'u_z', 'E', 'time', 'wgt',
@ -1226,6 +1156,7 @@ class ParticleList(list):
kwargs.setdefault('mode', 'w')
with h5py.File(filename, **kwargs) as fh:
fh.attrs['filetype'] = np.bytes_("source")
fh.attrs['version'] = np.array([_VERSION_STATEPOINT, 2])
fh.create_dataset('source_bank', data=arr, dtype=source_dtype)
@ -1337,7 +1268,7 @@ def read_collision_track_mcpl(file_path):
data['material_id'].append(int(values_dict.get('material_id', 0)))
data['universe_id'].append(int(values_dict.get('universe_id', 0)))
data['n_collision'].append(int(values_dict.get('n_collision', 0)))
data['particle'].append(ParticleType.from_pdg_number(p.pdgcode))
data['particle'].append(ParticleType(p.pdgcode))
data['parent_id'].append(int(values_dict.get('parent_id', 0)))
data['progeny_id'].append(int(values_dict.get('progeny_id', 0)))

View file

@ -4,7 +4,8 @@ from collections.abc import Sequence
import h5py
from .checkvalue import check_filetype_version
from .source import SourceParticle, ParticleType
from .particle_type import ParticleType
from .source import SourceParticle
from pathlib import Path
@ -25,7 +26,7 @@ states : numpy.ndarray
"""
def _particle_track_repr(self):
return f"<ParticleTrack: {self.particle}, {len(self.states)} states>"
return f"<ParticleTrack: {str(self.particle)}, {len(self.states)} states>"
ParticleTrack.__repr__ = _particle_track_repr
@ -92,8 +93,8 @@ class Track(Sequence):
Parameters
----------
particle : {'neutron', 'photon', 'electron', 'positron'}
Matching particle type
particle : str or int or openmc.ParticleType
Matching particle type (name, PDG number, or type)
state_filter : function
Function that takes a state (structured datatype) and returns a bool
depending on some criteria.
@ -126,7 +127,7 @@ class Track(Sequence):
for t in self:
# Check for matching particle
if particle is not None:
if t.particle.name.lower() != particle:
if t.particle != ParticleType(particle):
continue
# Apply arbitrary state filter
@ -184,7 +185,7 @@ class Track(Sequence):
def sources(self):
sources = []
for particle_track in self:
particle_type = ParticleType(particle_track.particle)
particle_type = particle_track.particle
state = particle_track.states[0]
sources.append(
SourceParticle(

View file

@ -10,13 +10,12 @@ import numpy as np
import h5py
import openmc
from openmc.filter import _PARTICLES
from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh
import openmc.checkvalue as cv
from openmc.checkvalue import PathLike
from ._xml import get_elem_list, get_text, clean_indentation
from .mixin import IDManagerMixin
from .utility_funcs import change_directory
from .particle_type import ParticleType
class WeightWindows(IDManagerMixin):
@ -51,7 +50,7 @@ class WeightWindows(IDManagerMixin):
A list of values for which each successive pair constitutes a range of
energies in [eV] for a single bin. If no energy bins are provided, the
maximum and minimum energy for the data available at runtime.
particle_type : {'neutron', 'photon'}
particle_type : str or int or openmc.ParticleType
Particle type the weight windows apply to
survival_ratio : float
Ratio of the survival weight to the lower weight window bound for
@ -116,7 +115,7 @@ class WeightWindows(IDManagerMixin):
upper_ww_bounds: Iterable[float] | None = None,
upper_bound_ratio: float | None = None,
energy_bounds: Iterable[Real] | None = None,
particle_type: str = 'neutron',
particle_type: str | int | openmc.ParticleType = 'neutron',
survival_ratio: float = 3.0,
max_lower_bound_ratio: float | None = None,
max_split: int = 10,
@ -213,13 +212,15 @@ class WeightWindows(IDManagerMixin):
self._mesh = mesh
@property
def particle_type(self) -> str:
def particle_type(self) -> ParticleType:
return self._particle_type
@particle_type.setter
def particle_type(self, pt: str):
cv.check_value('Particle type', pt, _PARTICLES)
self._particle_type = pt
def particle_type(self, pt):
ptype = ParticleType(pt)
if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}:
raise ValueError("Weight windows can only be applied for neutrons or photons")
self._particle_type = ptype
@property
def energy_bounds(self) -> Iterable[Real]:
@ -329,7 +330,7 @@ class WeightWindows(IDManagerMixin):
subelement.text = str(self.mesh.id)
subelement = ET.SubElement(element, 'particle_type')
subelement.text = self.particle_type
subelement.text = str(self.particle_type)
if self.energy_bounds is not None:
subelement = ET.SubElement(element, 'energy_bounds')
@ -494,7 +495,7 @@ class WeightWindowGenerator:
A list of values for which each successive pair constitutes a range of
energies in [eV] for a single bin. If no energy bins are provided, the
maximum and minimum energy for the data available at runtime.
particle_type : {'neutron', 'photon'}
particle_type : str or int or openmc.ParticleType
Particle type the weight windows apply to
method : {'magic', 'fw_cadis'}
The weight window generation methodology applied during an update.
@ -513,7 +514,7 @@ class WeightWindowGenerator:
energy_bounds : Iterable of Real
A list of values for which each successive pair constitutes a range of
energies in [eV] for a single bin
particle_type : {'neutron', 'photon'}
particle_type : openmc.ParticleType
Particle type the weight windows apply to
method : {'magic', 'fw_cadis'}
The weight window generation methodology applied during an update.
@ -534,7 +535,7 @@ class WeightWindowGenerator:
self,
mesh: openmc.MeshBase,
energy_bounds: Sequence[float] | None = None,
particle_type: str = 'neutron',
particle_type: str | int | openmc.ParticleType = 'neutron',
method: str = 'magic',
max_realizations: int = 1,
update_interval: int = 1,
@ -555,7 +556,7 @@ class WeightWindowGenerator:
def __repr__(self):
string = type(self).__name__ + '\n'
string += f'\t{"Mesh":<20}=\t{self.mesh.id}\n'
string += f'\t{"Particle:":<20}=\t{self.particle_type}\n'
string += f'\t{"Particle:":<20}=\t{str(self.particle_type)}\n'
string += f'\t{"Energy Bounds:":<20}=\t{self.energy_bounds}\n'
string += f'\t{"Method":<20}=\t{self.method}\n'
string += f'\t{"Max Realizations:":<20}=\t{self.max_realizations}\n'
@ -586,13 +587,15 @@ class WeightWindowGenerator:
self._energy_bounds = eb
@property
def particle_type(self) -> str:
def particle_type(self) -> ParticleType:
return self._particle_type
@particle_type.setter
def particle_type(self, pt: str):
cv.check_value('particle type', pt, ('neutron', 'photon'))
self._particle_type = pt
def particle_type(self, pt):
ptype = ParticleType(pt)
if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}:
raise ValueError("Weight windows can only be applied for neutrons or photons")
self._particle_type = ptype
@property
def method(self) -> str:
@ -695,7 +698,7 @@ class WeightWindowGenerator:
subelement = ET.SubElement(element, 'energy_bounds')
subelement.text = ' '.join(str(e) for e in self.energy_bounds)
particle_elem = ET.SubElement(element, 'particle_type')
particle_elem.text = self.particle_type
particle_elem.text = str(self.particle_type)
realizations_elem = ET.SubElement(element, 'max_realizations')
realizations_elem.text = str(self.max_realizations)
update_interval_elem = ET.SubElement(element, 'update_interval')
@ -730,7 +733,7 @@ class WeightWindowGenerator:
mesh_id = int(get_text(elem, 'mesh'))
mesh = meshes[mesh_id]
energy_bounds = get_elem_list(elem, "energy_bounds, float")
particle_type = get_text(elem, 'particle_type')

View file

@ -31,13 +31,13 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost)
if (p.material() == MATERIAL_VOID)
return;
int photon = static_cast<int>(ParticleType::photon);
int photon = ParticleType::photon().transport_index();
if (p.E() < settings::energy_cutoff[photon])
return;
// Get bremsstrahlung data for this material and particle type
BremsstrahlungData* mat;
if (p.type() == ParticleType::positron) {
if (p.type() == ParticleType::positron()) {
mat = &model::materials[p.material()]->ttb_->positron;
} else {
mat = &model::materials[p.material()]->ttb_->electron;
@ -119,7 +119,7 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost)
}
// Create secondary photon
p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon);
p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon());
*E_lost += w;
}
}

View file

@ -819,9 +819,9 @@ void Material::calculate_xs(Particle& p) const
p.macro_xs().fission = 0.0;
p.macro_xs().nu_fission = 0.0;
if (p.type() == ParticleType::neutron) {
if (p.type().is_neutron()) {
this->calculate_neutron_xs(p);
} else if (p.type() == ParticleType::photon) {
} else if (p.type().is_photon()) {
this->calculate_photon_xs(p);
}
}
@ -829,7 +829,7 @@ void Material::calculate_xs(Particle& p) const
void Material::calculate_neutron_xs(Particle& p) const
{
// Find energy index on energy grid
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
int i_grid =
std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing;

View file

@ -321,25 +321,7 @@ inline void ensure_mcpl_ready_or_fatal()
SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr)
{
SourceSite site;
switch (particle_repr->pdgcode) {
case 2112:
site.particle = ParticleType::neutron;
break;
case 22:
site.particle = ParticleType::photon;
break;
case 11:
site.particle = ParticleType::electron;
break;
case -11:
site.particle = ParticleType::positron;
break;
default:
fatal_error(fmt::format(
"MCPL: Encountered unexpected PDG code {} when converting to SourceSite.",
particle_repr->pdgcode));
break;
}
site.particle = ParticleType {particle_repr->pdgcode};
// Copy position and direction
site.r.x = particle_repr->position[0];
@ -368,7 +350,6 @@ vector<SourceSite> mcpl_source_sites(std::string path)
}
size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
size_t n_skipped = 0;
if (n_particles_in_file > 0) {
sites.reserve(n_particles_in_file);
}
@ -381,31 +362,16 @@ vector<SourceSite> mcpl_source_sites(std::string path)
path, sites.size(), n_particles_in_file));
break;
}
if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 ||
p_repr->pdgcode == 11 || p_repr->pdgcode == -11) {
sites.push_back(mcpl_particle_to_site(p_repr));
} else {
n_skipped++;
}
sites.push_back(mcpl_particle_to_site(p_repr));
}
g_mcpl_api->close_file(mcpl_file);
if (n_skipped > 0 && n_particles_in_file > 0) {
double percent_skipped =
100.0 * static_cast<double>(n_skipped) / n_particles_in_file;
warning(fmt::format(
"MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because "
"their type is not supported by OpenMC.",
n_skipped, n_particles_in_file, percent_skipped, path));
}
if (sites.empty()) {
if (n_particles_in_file > 0) {
fatal_error(fmt::format(
"MCPL file '{}' contained {} particles, but none were of the supported "
"types (neutron, photon, electron, positron). OpenMC cannot proceed "
"without source particles.",
"MCPL file '{}' contained {} particles, but no particles could be "
"read.",
path, n_particles_in_file));
} else {
fatal_error(fmt::format(
@ -461,22 +427,7 @@ void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id,
p_repr.ekin = site.E * 1e-6;
p_repr.time = site.time * 1e3;
p_repr.weight = site.wgt;
switch (site.particle) {
case ParticleType::neutron:
p_repr.pdgcode = 2112;
break;
case ParticleType::photon:
p_repr.pdgcode = 22;
break;
case ParticleType::electron:
p_repr.pdgcode = 11;
break;
case ParticleType::positron:
p_repr.pdgcode = -11;
break;
default:
continue;
}
p_repr.pdgcode = site.particle.pdg_number();
g_mcpl_api->add_particle(file_id, &p_repr);
}
}
@ -633,22 +584,7 @@ void write_mcpl_collision_track_internal(mcpl_outfile_t* file_id,
p_repr.ekin = site.E * 1e-6;
p_repr.time = site.time * 1e3;
p_repr.weight = site.wgt;
switch (site.particle) {
case ParticleType::neutron:
p_repr.pdgcode = 2112;
break;
case ParticleType::photon:
p_repr.pdgcode = 22;
break;
case ParticleType::electron:
p_repr.pdgcode = 11;
break;
case ParticleType::positron:
p_repr.pdgcode = -11;
break;
default:
continue;
}
p_repr.pdgcode = site.particle.pdg_number();
g_mcpl_api->add_particle(file_id, &p_repr);
}
} else {

View file

@ -486,7 +486,7 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size,
SourceSite site;
site.E = 1.0;
site.particle = ParticleType::neutron;
site.particle = ParticleType::neutron();
for (int axis = 0; axis < 3; ++axis) {
// Set starting position and direction

View file

@ -244,7 +244,7 @@ void MgxsInterface::read_header(const std::string& path_cross_sections)
void put_mgxs_header_data_to_globals()
{
// Get the minimum and maximum energies
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
data::energy_min[neutron] = data::mg.energy_bins_.back();
data::energy_max[neutron] = data::mg.energy_bins_.front();

View file

@ -379,7 +379,7 @@ void Nuclide::create_derived(
auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD);
for (const auto& p : rx->products_) {
if (p.particle_ == ParticleType::photon) {
if (p.particle_.is_photon()) {
for (int k = 0; k < n; ++k) {
double E = grid_[t].energy[k + j];
@ -501,7 +501,7 @@ void Nuclide::create_derived(
void Nuclide::init_grid()
{
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
double E_min = data::energy_min[neutron];
double E_max = data::energy_max[neutron];
int M = settings::n_log_bins;
@ -552,7 +552,7 @@ double Nuclide::nu(double E, EmissionMode mode, int group) const
for (int i = 1; i < rx->products_.size(); ++i) {
// Skip any non-neutron products
const auto& product = rx->products_[i];
if (product.particle_ != ParticleType::neutron)
if (!product.particle_.is_neutron())
continue;
// Evaluate yield

View file

@ -155,21 +155,21 @@ std::string time_stamp()
void print_particle(Particle& p)
{
// Display particle type and ID.
switch (p.type()) {
case ParticleType::neutron:
switch (p.type().pdg_number()) {
case PDG_NEUTRON:
fmt::print("Neutron ");
break;
case ParticleType::photon:
case PDG_PHOTON:
fmt::print("Photon ");
break;
case ParticleType::electron:
case PDG_ELECTRON:
fmt::print("Electron ");
break;
case ParticleType::positron:
case PDG_POSITRON:
fmt::print("Positron ");
break;
default:
fmt::print("Unknown Particle ");
fmt::print("Particle {} ", p.type().str());
}
fmt::print("{}\n", p.id());

View file

@ -48,17 +48,19 @@ double Particle::speed() const
if (settings::run_CE) {
// Determine mass in eV/c^2
double mass;
switch (this->type()) {
case ParticleType::neutron:
switch (this->type().pdg_number()) {
case PDG_NEUTRON:
mass = MASS_NEUTRON_EV;
break;
case ParticleType::photon:
case PDG_PHOTON:
mass = 0.0;
break;
case ParticleType::electron:
case ParticleType::positron:
case PDG_ELECTRON:
case PDG_POSITRON:
mass = MASS_ELECTRON_EV;
break;
default:
fatal_error("Unsupported particle for speed calculation.");
}
// Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<<m:
return C_LIGHT * std::sqrt(this->E() * (this->E() + 2 * mass)) /
@ -77,7 +79,11 @@ bool Particle::create_secondary(
{
// If energy is below cutoff for this particle, don't create secondary
// particle
if (E < settings::energy_cutoff[static_cast<int>(type)]) {
int idx = type.transport_index();
if (idx == C_NONE) {
return false;
}
if (E < settings::energy_cutoff[idx]) {
return false;
}
@ -235,7 +241,8 @@ void Particle::event_advance()
boundary() = distance_to_boundary(*this);
// Sample a distance to collision
if (type() == ParticleType::electron || type() == ParticleType::positron) {
if (type() == ParticleType::electron() ||
type() == ParticleType::positron()) {
collision_distance() = material() == MATERIAL_VOID ? INFINITY : 0.0;
} else if (macro_xs().total == 0.0) {
collision_distance() = INFINITY;
@ -244,7 +251,7 @@ void Particle::event_advance()
}
double speed = this->speed();
double time_cutoff = settings::time_cutoff[static_cast<int>(type())];
double time_cutoff = settings::time_cutoff[type().transport_index()];
double distance_cutoff =
(time_cutoff < INFTY) ? (time_cutoff - time()) * speed : INFTY;
@ -269,8 +276,7 @@ void Particle::event_advance()
}
// Score track-length estimate of k-eff
if (settings::run_mode == RunMode::EIGENVALUE &&
type() == ParticleType::neutron) {
if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) {
keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission;
}
@ -331,8 +337,7 @@ void Particle::event_cross_surface()
void Particle::event_collide()
{
// Score collision estimate of keff
if (settings::run_mode == RunMode::EIGENVALUE &&
type() == ParticleType::neutron) {
if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) {
keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total;
}
@ -370,8 +375,7 @@ void Particle::event_collide()
}
}
if (!model::active_pulse_height_tallies.empty() &&
type() == ParticleType::photon) {
if (!model::active_pulse_height_tallies.empty() && type().is_photon()) {
pht_collision_energy();
}
@ -442,7 +446,7 @@ void Particle::event_revive_from_secondary()
// Subtract secondary particle energy from interim pulse-height results
if (!model::active_pulse_height_tallies.empty() &&
this->type() == ParticleType::photon) {
this->type().is_photon()) {
// Since the birth cell of the particle has not been set we
// have to determine it before the energy of the secondary particle can be
// removed from the pulse-height of this cell.
@ -525,7 +529,7 @@ void Particle::pht_collision_energy()
// If the energy of the particle is below the cutoff, it will not be sampled
// so its energy is added to the pulse-height in the cell
int photon = static_cast<int>(ParticleType::photon);
int photon = ParticleType::photon().transport_index();
if (E() < settings::energy_cutoff[photon]) {
pht_storage()[index] += E();
}
@ -824,7 +828,7 @@ void Particle::write_restart() const
break;
}
write_dataset(file_id, "id", id());
write_dataset(file_id, "type", static_cast<int>(type()));
write_dataset(file_id, "type", type().pdg_number());
int64_t i = current_work();
if (settings::run_mode == RunMode::EIGENVALUE) {
@ -878,37 +882,6 @@ void Particle::update_neutron_xs(
//==============================================================================
// Non-method functions
//==============================================================================
std::string particle_type_to_str(ParticleType type)
{
switch (type) {
case ParticleType::neutron:
return "neutron";
case ParticleType::photon:
return "photon";
case ParticleType::electron:
return "electron";
case ParticleType::positron:
return "positron";
}
UNREACHABLE();
}
ParticleType str_to_particle_type(std::string str)
{
if (str == "neutron") {
return ParticleType::neutron;
} else if (str == "photon") {
return ParticleType::photon;
} else if (str == "electron") {
return ParticleType::electron;
} else if (str == "positron") {
return ParticleType::positron;
} else {
throw std::invalid_argument {fmt::format("Invalid particle name: {}", str)};
}
}
void add_surf_source_to_bank(Particle& p, const Surface& surf)
{
if (simulation::current_batch <= settings::n_inactive ||

View file

@ -31,6 +31,16 @@ void read_particle_restart(Particle& p, RunMode& previous_run_mode)
hid_t file_id = file_open(settings::path_particle_restart, 'r');
// Read data from file
bool legacy_particle_codes = true;
if (attribute_exists(file_id, "version")) {
array<int, 2> version;
read_attribute(file_id, "version", version);
if (version[0] > VERSION_PARTICLE_RESTART[0] ||
(version[0] == VERSION_PARTICLE_RESTART[0] && version[1] >= 1)) {
legacy_particle_codes = false;
}
}
read_dataset(file_id, "current_batch", simulation::current_batch);
read_dataset(file_id, "generations_per_batch", settings::gen_per_batch);
read_dataset(file_id, "current_generation", simulation::current_gen);
@ -45,7 +55,8 @@ void read_particle_restart(Particle& p, RunMode& previous_run_mode)
read_dataset(file_id, "id", p.id());
int type;
read_dataset(file_id, "type", type);
p.type() = static_cast<ParticleType>(type);
p.type() = legacy_particle_codes ? legacy_particle_index_to_type(type)
: ParticleType {type};
read_dataset(file_id, "weight", p.wgt());
read_dataset(file_id, "energy", p.E());
read_dataset(file_id, "xyz", p.r());

246
src/particle_type.cpp Normal file
View file

@ -0,0 +1,246 @@
#include "openmc/particle_type.h"
#include <algorithm>
#include <cctype>
#include <stdexcept>
#include "openmc/string_utils.h"
namespace openmc {
namespace {
constexpr const char* ATOMIC_SYMBOL[] = {"", "H", "He", "Li", "Be", "B", "C",
"N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca",
"Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As",
"Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd",
"Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr",
"Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf",
"Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At",
"Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf",
"Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg",
"Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"};
constexpr int MAX_Z =
static_cast<int>(sizeof(ATOMIC_SYMBOL) / sizeof(ATOMIC_SYMBOL[0])) - 1;
bool is_integer_string(const std::string& s)
{
if (s.empty())
return false;
size_t i = 0;
if (s[0] == '-' || s[0] == '+') {
if (s.size() == 1)
return false;
i = 1;
}
for (; i < s.size(); ++i) {
if (!std::isdigit(static_cast<unsigned char>(s[i])))
return false;
}
return true;
}
int atomic_number_from_symbol(std::string_view symbol)
{
for (int z = 1; z <= MAX_Z; ++z) {
if (symbol == ATOMIC_SYMBOL[z]) {
return z;
}
}
return 0;
}
bool parse_gnds_nuclide(std::string_view name, int& Z, int& A, int& m)
{
if (name.empty())
return false;
size_t pos = 0;
if (!std::isupper(static_cast<unsigned char>(name[pos])))
return false;
std::string symbol;
symbol += name[pos++];
if (pos < name.size() &&
std::islower(static_cast<unsigned char>(name[pos]))) {
symbol += name[pos++];
}
if (pos >= name.size() ||
!std::isdigit(static_cast<unsigned char>(name[pos]))) {
return false;
}
size_t a_start = pos;
while (
pos < name.size() && std::isdigit(static_cast<unsigned char>(name[pos]))) {
++pos;
}
A = std::stoi(std::string {name.substr(a_start, pos - a_start)});
if (A <= 0 || A > 999)
return false;
m = 0;
if (pos < name.size()) {
if (name[pos] != '_' || pos + 2 >= name.size() || name[pos + 1] != 'm') {
return false;
}
pos += 2;
size_t m_start = pos;
while (pos < name.size() &&
std::isdigit(static_cast<unsigned char>(name[pos]))) {
++pos;
}
if (m_start == pos)
return false;
m = std::stoi(std::string {name.substr(m_start, pos - m_start)});
if (m < 0 || m > 9)
return false;
}
if (pos != name.size())
return false;
Z = atomic_number_from_symbol(symbol);
return Z != 0;
}
// Helper to convert nuclear PDG number to nuclide name
std::string nuclide_name_from_pdg(int32_t pdg)
{
int32_t code = pdg;
int m = code % 10;
int A = (code / 10) % 1000;
int Z = (code / 10000) % 1000;
if (Z <= 0 || Z > MAX_Z || A <= 0 || A > 999) {
throw std::invalid_argument {
"Invalid nuclear PDG number: " + std::to_string(pdg)};
}
std::string name = ATOMIC_SYMBOL[Z] + std::to_string(A);
if (m > 0) {
name += "_m" + std::to_string(m);
}
return name;
}
} // namespace
//==============================================================================
// ParticleType member function implementations
//==============================================================================
ParticleType::ParticleType(std::string_view str)
{
std::string s {str};
strtrim(s);
if (s.empty()) {
throw std::invalid_argument {"Particle string is empty."};
}
std::string lower = s;
to_lower(lower);
// Check for pdg: prefix
if (starts_with(lower, "pdg:")) {
std::string value_str = lower.substr(4);
if (!is_integer_string(value_str)) {
throw std::invalid_argument {"Invalid PDG number: " + value_str};
}
pdg_number_ = std::stoi(value_str);
return;
}
// Check for known particle names
if (lower == "neutron" || lower == "n") {
pdg_number_ = PDG_NEUTRON;
return;
}
if (lower == "photon" || lower == "gamma") {
pdg_number_ = PDG_PHOTON;
return;
}
if (lower == "electron") {
pdg_number_ = PDG_ELECTRON;
return;
}
if (lower == "positron") {
pdg_number_ = PDG_POSITRON;
return;
}
if (lower == "proton" || lower == "p" || lower == "h1") {
pdg_number_ = PDG_PROTON;
return;
}
if (lower == "deuteron" || lower == "d" || lower == "h2") {
pdg_number_ = PDG_DEUTERON;
return;
}
if (lower == "triton" || lower == "t" || lower == "h3") {
pdg_number_ = PDG_TRITON;
return;
}
if (lower == "alpha" || lower == "he4") {
pdg_number_ = PDG_ALPHA;
return;
}
// Check for integer string
if (is_integer_string(s)) {
pdg_number_ = std::stoi(s);
return;
}
// Try to parse as GNDS nuclide name
int Z = 0;
int A = 0;
int m = 0;
if (!parse_gnds_nuclide(s, Z, A, m)) {
throw std::invalid_argument {"Invalid nuclide name: " + s};
}
pdg_number_ = 1000000000 + Z * 10000 + A * 10 + m;
}
std::string ParticleType::str() const
{
if (pdg_number_ == PDG_NEUTRON)
return "neutron";
if (pdg_number_ == PDG_PHOTON)
return "photon";
if (pdg_number_ == PDG_ELECTRON)
return "electron";
if (pdg_number_ == PDG_POSITRON)
return "positron";
if (pdg_number_ == PDG_PROTON)
return "proton";
if (is_nucleus()) {
return nuclide_name_from_pdg(pdg_number_);
}
return "pdg:" + std::to_string(pdg_number_);
}
//==============================================================================
// Free function implementations
//==============================================================================
ParticleType legacy_particle_index_to_type(int index)
{
switch (index) {
case 0:
return ParticleType {PDG_NEUTRON};
case 1:
return ParticleType {PDG_PHOTON};
case 2:
return ParticleType {PDG_ELECTRON};
case 3:
return ParticleType {PDG_POSITRON};
default:
throw std::invalid_argument {
"Invalid legacy particle index: " + std::to_string(index)};
}
}
} // namespace openmc

View file

@ -293,7 +293,7 @@ PhotonInteraction::PhotonInteraction(hid_t group)
close_group(rgroup);
// Truncate the bremsstrahlung data at the cutoff energy
int photon = static_cast<int>(ParticleType::photon);
int photon = ParticleType::photon().transport_index();
const auto& E {electron_energy};
double cutoff = settings::energy_cutoff[photon];
if (cutoff > E(0)) {
@ -805,7 +805,7 @@ void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
if (shell.transitions.empty()) {
Direction u = isotropic_direction(p.current_seed());
double E = shell.binding_energy;
p.create_secondary(p.wgt(), u, E, ParticleType::photon);
p.create_secondary(p.wgt(), u, E, ParticleType::photon());
continue;
}
@ -833,12 +833,13 @@ void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
holes[n_holes++] = transition.secondary_subshell;
// Create auger electron
p.create_secondary(p.wgt(), u, transition.energy, ParticleType::electron);
p.create_secondary(
p.wgt(), u, transition.energy, ParticleType::electron());
} else {
// Radiative transition -- get X-ray energy
// Create fluorescent photon
p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon);
p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
}
}
}

View file

@ -46,27 +46,29 @@ void collision(Particle& p)
++(p.n_collision());
// Sample reaction for the material the particle is in
switch (p.type()) {
case ParticleType::neutron:
switch (p.type().pdg_number()) {
case PDG_NEUTRON:
sample_neutron_reaction(p);
break;
case ParticleType::photon:
case PDG_PHOTON:
sample_photon_reaction(p);
break;
case ParticleType::electron:
case PDG_ELECTRON:
sample_electron_reaction(p);
break;
case ParticleType::positron:
case PDG_POSITRON:
sample_positron_reaction(p);
break;
default:
fatal_error("Unsupported particle PDG for collision sampling.");
}
if (settings::weight_window_checkpoint_collision)
apply_weight_windows(p);
// Kill particle if energy falls below cutoff
int type = static_cast<int>(p.type());
if (p.E() < settings::energy_cutoff[type]) {
int type = p.type().transport_index();
if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
p.wgt() = 0.0;
}
@ -75,11 +77,11 @@ void collision(Particle& p)
std::string msg;
if (p.event() == TallyEvent::KILL) {
msg = fmt::format(" Killed. Energy = {} eV.", p.E());
} else if (p.type() == ParticleType::neutron) {
} else if (p.type().is_neutron()) {
msg = fmt::format(" {} with {}. Energy = {} eV.",
reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
p.E());
} else if (p.type() == ParticleType::photon) {
} else if (p.type().is_photon()) {
msg = fmt::format(" {} with {}. Energy = {} eV.",
reaction_name(p.event_mt()),
to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
@ -208,7 +210,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
// Initialize fission site object with particle data
SourceSite site;
site.r = p.r();
site.particle = ParticleType::neutron;
site.particle = ParticleType::neutron();
site.time = p.time();
site.wgt = 1. / weight;
site.surf_id = 0;
@ -218,7 +220,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
// Reject site if it exceeds time cutoff
if (site.delayed_group > 0) {
double t_cutoff = settings::time_cutoff[static_cast<int>(site.particle)];
double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
if (site.time > t_cutoff) {
continue;
}
@ -289,7 +291,7 @@ void sample_photon_reaction(Particle& p)
// Kill photon if below energy cutoff -- an extra check is made here because
// photons with energy below the cutoff may have been produced by neutrons
// reactions or atomic relaxation
int photon = static_cast<int>(ParticleType::photon);
int photon = ParticleType::photon().transport_index();
if (p.E() < settings::energy_cutoff[photon]) {
p.E() = 0.0;
p.wgt() = 0.0;
@ -339,13 +341,13 @@ void sample_photon_reaction(Particle& p)
// Create Compton electron
double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
int electron = static_cast<int>(ParticleType::electron);
int electron = ParticleType::electron().transport_index();
if (E_electron >= settings::energy_cutoff[electron]) {
double mu_electron = (alpha - alpha_out * p.mu()) /
std::sqrt(alpha * alpha + alpha_out * alpha_out -
2.0 * alpha * alpha_out * p.mu());
Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
}
// Allow electrons to fill orbital and produce Auger electrons and
@ -418,7 +420,7 @@ void sample_photon_reaction(Particle& p)
u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
// Create secondary electron
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
// Allow electrons to fill orbital and produce auger electrons
// and fluorescent photons
@ -443,12 +445,11 @@ void sample_photon_reaction(Particle& p)
// Create secondary electron
Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
// Create secondary positron
u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron());
p.event() = TallyEvent::ABSORB;
p.event_mt() = PAIR_PROD;
p.wgt() = 0.0;
@ -483,8 +484,8 @@ void sample_positron_reaction(Particle& p)
Direction u = isotropic_direction(p.current_seed());
// Create annihilation photon pair traveling in opposite directions
p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon());
p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon());
p.E() = 0.0;
p.wgt() = 0.0;
@ -609,7 +610,7 @@ void sample_photon_product(
continue;
for (int j = 0; j < rx->products_.size(); ++j) {
if (rx->products_[j].particle_ == ParticleType::photon) {
if (rx->products_[j].particle_.is_photon()) {
// For fission, artificially increase the photon yield to account
// for delayed photons
double f = 1.0;
@ -1098,7 +1099,7 @@ void sample_fission_neutron(
rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
// resample if energy is greater than maximum neutron energy
constexpr int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
if (site->E < data::energy_max[neutron])
break;
@ -1158,7 +1159,7 @@ void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
if (std::floor(yield) == yield && yield > 0) {
// If yield is integral, create exactly that many secondary particles
for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
}
} else {
// Otherwise, change weight of particle based on yield
@ -1214,7 +1215,7 @@ void sample_secondary_photons(Particle& p, int i_nuclide)
}
// Create the secondary photon
bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
// Tag secondary particle with parent nuclide
if (created_photon && settings::use_decay_photons) {

View file

@ -136,7 +136,7 @@ void create_fission_sites(Particle& p)
// Initialize fission site object with particle data
SourceSite site;
site.r = p.r();
site.particle = ParticleType::neutron;
site.particle = ParticleType::neutron();
site.time = p.time();
site.wgt = 1. / weight;
@ -171,7 +171,7 @@ void create_fission_sites(Particle& p)
site.time -= std::log(prn(p.current_seed())) / decay_rate;
// Reject site if it exceeds time cutoff
double t_cutoff = settings::time_cutoff[static_cast<int>(site.particle)];
double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
if (site.time > t_cutoff) {
continue;
}

View file

@ -70,9 +70,8 @@ Reaction::Reaction(
if (settings::use_decay_photons) {
// Remove photon products for D1S method
products_.erase(
std::remove_if(products_.begin(), products_.end(),
[](const auto& p) { return p.particle_ == ParticleType::photon; }),
products_.erase(std::remove_if(products_.begin(), products_.end(),
[](const auto& p) { return p.particle_.is_photon(); }),
products_.end());
// Determine product for D1S method

View file

@ -26,7 +26,7 @@ ReactionProduct::ReactionProduct(hid_t group)
// Read particle type
std::string temp;
read_attribute(group, "particle", temp);
particle_ = str_to_particle_type(temp);
particle_ = ParticleType {temp};
// Read emission mode and decay rate
read_attribute(group, "emission_mode", temp);
@ -42,7 +42,7 @@ ReactionProduct::ReactionProduct(hid_t group)
if (emission_mode_ == EmissionMode::delayed) {
if (attribute_exists(group, "decay_rate")) {
read_attribute(group, "decay_rate", decay_rate_);
} else if (particle_ == ParticleType::neutron) {
} else if (particle_.is_neutron()) {
warning(fmt::format("Decay rate doesn't exist for delayed neutron "
"emission ({}).",
object_name(group)));
@ -85,7 +85,7 @@ ReactionProduct::ReactionProduct(hid_t group)
ReactionProduct::ReactionProduct(const ChainNuclide::Product& product)
{
particle_ = ParticleType::photon;
particle_ = ParticleType::photon();
emission_mode_ = EmissionMode::delayed;
// Get chain nuclide object for radionuclide

View file

@ -687,7 +687,7 @@ void initialize_data()
for (const auto& nuc : data::nuclides) {
if (nuc->grid_.size() >= 1) {
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
data::energy_min[neutron] =
std::max(data::energy_min[neutron], nuc->grid_[0].energy.front());
data::energy_max[neutron] =
@ -698,7 +698,7 @@ void initialize_data()
if (settings::photon_transport) {
for (const auto& elem : data::elements) {
if (elem->energy_.size() >= 1) {
int photon = static_cast<int>(ParticleType::photon);
int photon = ParticleType::photon().transport_index();
int n = elem->energy_.size();
data::energy_min[photon] =
std::max(data::energy_min[photon], std::exp(elem->energy_(1)));
@ -711,9 +711,9 @@ void initialize_data()
// Determine if minimum/maximum energy for bremsstrahlung is greater/less
// than the current minimum/maximum
if (data::ttb_e_grid.size() >= 1) {
int photon = static_cast<int>(ParticleType::photon);
int electron = static_cast<int>(ParticleType::electron);
int positron = static_cast<int>(ParticleType::positron);
int photon = ParticleType::photon().transport_index();
int electron = ParticleType::electron().transport_index();
int positron = ParticleType::positron().transport_index();
int n_e = data::ttb_e_grid.size();
const std::vector<int> charged = {electron, positron};
@ -737,7 +737,7 @@ void initialize_data()
// grid has not been allocated
if (nuc->grid_.size() > 0) {
double max_E = nuc->grid_[0].energy.back();
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
if (max_E == data::energy_max[neutron]) {
write_message(7, "Maximum neutron transport energy: {} eV for {}",
data::energy_max[neutron], nuc->name_);
@ -754,7 +754,7 @@ void initialize_data()
for (auto& nuc : data::nuclides) {
nuc->init_grid();
}
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
simulation::log_spacing =
std::log(data::energy_max[neutron] / data::energy_min[neutron]) /
settings::n_log_bins;

View file

@ -37,6 +37,20 @@
namespace openmc {
namespace {
void validate_particle_type(ParticleType type, const std::string& context)
{
if (type.is_transportable())
return;
fatal_error(
fmt::format("Unsupported source particle type '{}' (PDG {}) in {}.",
type.str(), type.pdg_number(), context));
}
} // namespace
//==============================================================================
// Global variables
//==============================================================================
@ -284,22 +298,15 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node)
{
// Check for particle type
if (check_for_node(node, "particle")) {
auto temp_str = get_node_value(node, "particle", true, true);
if (temp_str == "neutron") {
particle_ = ParticleType::neutron;
} else if (temp_str == "photon") {
particle_ = ParticleType::photon;
auto temp_str = get_node_value(node, "particle", false, true);
particle_ = ParticleType(temp_str);
if (particle_ == ParticleType::photon() ||
particle_ == ParticleType::electron() ||
particle_ == ParticleType::positron()) {
settings::photon_transport = true;
} else if (temp_str == "electron") {
particle_ = ParticleType::electron;
settings::photon_transport = true;
} else if (temp_str == "positron") {
particle_ = ParticleType::positron;
settings::photon_transport = true;
} else {
fatal_error(std::string("Unknown source particle type: ") + temp_str);
}
}
validate_particle_type(particle_, "IndependentSource");
// Check for external source file
if (check_for_node(node, "file")) {
@ -390,7 +397,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
// Sample energy and time for neutron and photon sources
if (settings::solver_type != SolverType::RANDOM_RAY) {
// Check for monoenergetic source above maximum particle energy
auto p = static_cast<int>(particle_);
auto p = particle_.transport_index();
auto energy_ptr = dynamic_cast<Discrete*>(energy_.get());
if (energy_ptr) {
auto energies = xt::adapt(energy_ptr->x());
@ -472,6 +479,11 @@ void FileSource::load_sites_from_file(const std::string& path)
// Close file
file_close(file_id);
}
// Make sure particles in source file have valid types
for (const auto& site : this->sites_) {
validate_particle_type(site.particle, "FileSource");
}
}
SourceSite FileSource::sample(uint64_t* seed) const
@ -585,6 +597,11 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node)
std::make_unique<MeshElementSpatial>(mesh_idx, elem_index));
}
// Make sure sources use valid particle types
for (const auto& src : sources_) {
validate_particle_type(src->particle_type(), "MeshSource");
}
// the number of source distributions should either be one or equal to the
// number of mesh elements
if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) {

View file

@ -22,6 +22,7 @@
#include "openmc/mgxs_interface.h"
#include "openmc/nuclide.h"
#include "openmc/output.h"
#include "openmc/particle_type.h"
#include "openmc/settings.h"
#include "openmc/simulation.h"
#include "openmc/tallies/derivative.h"
@ -632,6 +633,7 @@ void write_h5_source_point(const char* filename, span<SourceSite> source_bank,
if (mpi::master || parallel) {
file_id = file_open(filename_.c_str(), 'w', true);
write_attribute(file_id, "filetype", "source");
write_attribute(file_id, "version", VERSION_STATEPOINT);
}
// Get pointer to source bank and write to file
@ -677,6 +679,16 @@ std::string dtype_member_names(hid_t dtype_id)
void read_source_bank(
hid_t group_id, vector<SourceSite>& sites, bool distribute)
{
bool legacy_particle_codes = true;
if (attribute_exists(group_id, "version")) {
array<int, 2> version;
read_attribute(group_id, "version", version);
if (version[0] > VERSION_STATEPOINT[0] ||
(version[0] == VERSION_STATEPOINT[0] && version[1] >= 2)) {
legacy_particle_codes = false;
}
}
hid_t banktype = h5banktype(true);
// Open the dataset
@ -738,6 +750,12 @@ void read_source_bank(
H5Sclose(memspace);
H5Dclose(dset);
H5Tclose(banktype);
if (legacy_particle_codes) {
for (auto& site : sites) {
site.particle = legacy_particle_index_to_type(site.particle.pdg_number());
}
}
}
void write_unstructured_mesh_results()

View file

@ -13,7 +13,7 @@ void ParticleFilter::from_xml(pugi::xml_node node)
// Convert to vector of ParticleType
vector<ParticleType> types;
for (auto& p : particles) {
types.push_back(str_to_particle_type(p));
types.emplace_back(p);
}
this->set_particles(types);
}
@ -47,7 +47,7 @@ void ParticleFilter::to_statepoint(hid_t filter_group) const
Filter::to_statepoint(filter_group);
vector<std::string> particles;
for (auto p : particles_) {
particles.push_back(particle_type_to_str(p));
particles.push_back(p.str());
}
write_dataset(filter_group, "bins", particles);
}
@ -55,10 +55,10 @@ void ParticleFilter::to_statepoint(hid_t filter_group) const
std::string ParticleFilter::text_label(int bin) const
{
const auto& p = particles_.at(bin);
return fmt::format("Particle: {}", particle_type_to_str(p));
return fmt::format("Particle: {}", p.str());
}
extern "C" int openmc_particle_filter_get_bins(int32_t idx, int bins[])
extern "C" int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[])
{
if (int err = verify_filter(idx))
return err;
@ -68,7 +68,7 @@ extern "C" int openmc_particle_filter_get_bins(int32_t idx, int bins[])
if (pf) {
const auto& particles = pf->particles();
for (int i = 0; i < particles.size(); i++) {
bins[i] = static_cast<int>(particles[i]);
bins[i] = particles[i].pdg_number();
}
} else {
set_errmsg("The filter at the specified index is not a ParticleFilter");

View file

@ -289,12 +289,12 @@ Tally::Tally(pugi::xml_node node)
const auto& f = model::tally_filters[particle_filter_index].get();
auto pf = dynamic_cast<ParticleFilter*>(f);
for (auto p : pf->particles()) {
if (p != ParticleType::neutron) {
if (!p.is_neutron()) {
warning(fmt::format(
"Particle filter other than NEUTRON used with "
"photon transport turned off. All tallies for particle type {}"
" will have no scores",
static_cast<int>(p)));
p.str()));
}
}
}

View file

@ -328,10 +328,10 @@ double score_neutron_heating(const Particle& p, const Tally& tally, double flux,
//! Helper function to obtain reaction Q value for photons and charged particles
double get_reaction_q_value(const Particle& p)
{
if (p.type() == ParticleType::photon && p.event_mt() == PAIR_PROD) {
if (p.type().is_photon() && p.event_mt() == PAIR_PROD) {
// pair production
return -2 * MASS_ELECTRON_EV;
} else if (p.type() == ParticleType::positron) {
} else if (p.type() == ParticleType::positron()) {
// positron annihilation
return 2 * MASS_ELECTRON_EV;
} else {
@ -344,7 +344,7 @@ double get_reaction_q_value(const Particle& p)
double score_particle_heating(const Particle& p, const Tally& tally,
double flux, int rxn_bin, int i_nuclide, double atom_density)
{
if (p.type() == ParticleType::neutron)
if (p.type().is_neutron())
return score_neutron_heating(
p, tally, flux, rxn_bin, i_nuclide, atom_density);
if (i_nuclide == -1 || i_nuclide == p.event_nuclide() ||
@ -584,8 +584,6 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
// Get the pre-collision energy of the particle.
auto E = p.E_last();
using Type = ParticleType;
for (auto i = 0; i < tally.scores_.size(); ++i) {
auto score_bin = tally.scores_[i];
auto score_index = start_index + i;
@ -598,9 +596,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
case SCORE_TOTAL:
if (i_nuclide >= 0) {
if (p.type() == Type::neutron) {
if (p.type().is_neutron()) {
score = p.neutron_xs(i_nuclide).total * atom_density * flux;
} else if (p.type() == Type::photon) {
} else if (p.type().is_photon()) {
score = p.photon_xs(i_nuclide).total * atom_density * flux;
}
} else {
@ -609,7 +607,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
break;
case SCORE_INVERSE_VELOCITY:
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// Score inverse velocity in units of s/cm.
@ -617,11 +615,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
break;
case SCORE_SCATTER:
if (p.type() != Type::neutron && p.type() != Type::photon)
if (!p.type().is_neutron() && !p.type().is_photon())
continue;
if (i_nuclide >= 0) {
if (p.type() == Type::neutron) {
if (p.type().is_neutron()) {
const auto& micro = p.neutron_xs(i_nuclide);
score = (micro.total - micro.absorption) * atom_density * flux;
} else {
@ -629,7 +627,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
score = (micro.coherent + micro.incoherent) * atom_density * flux;
}
} else {
if (p.type() == Type::neutron) {
if (p.type().is_neutron()) {
score = (p.macro_xs().total - p.macro_xs().absorption) * flux;
} else {
score = (p.macro_xs().coherent + p.macro_xs().incoherent) * flux;
@ -638,11 +636,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
break;
case SCORE_ABSORPTION:
if (p.type() != Type::neutron && p.type() != Type::photon)
if (!p.type().is_neutron() && !p.type().is_photon())
continue;
if (i_nuclide >= 0) {
if (p.type() == Type::neutron) {
if (p.type().is_neutron()) {
score = p.neutron_xs(i_nuclide).absorption * atom_density * flux;
} else {
const auto& xs = p.photon_xs(i_nuclide);
@ -650,7 +648,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
(xs.total - xs.coherent - xs.incoherent) * atom_density * flux;
}
} else {
if (p.type() == Type::neutron) {
if (p.type().is_neutron()) {
score = p.macro_xs().absorption * flux;
} else {
score =
@ -806,7 +804,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
// this loop.
for (auto d = 1; d < rxn.products_.size(); ++d) {
const auto& product = rxn.products_[d];
if (product.particle_ != Type::neutron)
if (!product.particle_.is_neutron())
continue;
auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
@ -860,7 +858,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
// this loop.
for (auto d = 1; d < rxn.products_.size(); ++d) {
const auto& product = rxn.products_[d];
if (product.particle_ != Type::neutron)
if (!product.particle_.is_neutron())
continue;
auto yield =
@ -911,7 +909,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
continue;
case ELASTIC:
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
if (i_nuclide >= 0) {
@ -943,7 +941,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
case SCORE_IFP_TIME_NUM:
if (settings::ifp_on) {
if ((p.type() == Type::neutron) && (p.fission())) {
if (p.type().is_neutron() && p.fission()) {
if (is_generation_time_or_both()) {
const auto& lifetimes =
simulation::ifp_source_lifetime_bank[p.current_work() - 1];
@ -957,7 +955,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
case SCORE_IFP_BETA_NUM:
if (settings::ifp_on) {
if ((p.type() == Type::neutron) && (p.fission())) {
if (p.type().is_neutron() && p.fission()) {
if (is_beta_effective_or_both()) {
const auto& delayed_groups =
simulation::ifp_source_delayed_group_bank[p.current_work() - 1];
@ -982,7 +980,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
case SCORE_IFP_DENOM:
if (settings::ifp_on) {
if ((p.type() == Type::neutron) && (p.fission())) {
if (p.type().is_neutron() && p.fission()) {
int ifp_data_size;
if (is_beta_effective_or_both()) {
ifp_data_size = static_cast<int>(
@ -1012,7 +1010,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
if (!simulation::need_depletion_rx)
goto default_case;
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
int m;
@ -1045,7 +1043,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
case INCOHERENT:
case PHOTOELECTRIC:
case PAIR_PROD:
if (p.type() != Type::photon)
if (!p.type().is_photon())
continue;
if (i_nuclide >= 0) {
@ -1075,7 +1073,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
// The default block is really only meant for redundant neutron reactions
// (e.g. 444, 901)
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// Any other cross section has to be calculated on-the-fly
@ -1129,8 +1127,6 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
p.neutron_xs(p.event_nuclide()).total
: 0.0;
using Type = ParticleType;
for (auto i = 0; i < tally.scores_.size(); ++i) {
auto score_bin = tally.scores_[i];
auto score_index = start_index + i;
@ -1141,7 +1137,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
// All events score to a flux bin. We actually use a collision estimator
// in place of an analog one since there is no way to count 'events'
// exactly for the flux
if (p.type() == Type::neutron || p.type() == Type::photon) {
if (p.type().is_neutron() || p.type().is_photon()) {
score = flux * p.wgt_last() / p.macro_xs().total;
} else {
score = 0.;
@ -1155,7 +1151,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
break;
case SCORE_INVERSE_VELOCITY:
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// All events score to an inverse velocity bin. We actually use a
@ -1165,7 +1161,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
break;
case SCORE_SCATTER:
if (p.type() != Type::neutron && p.type() != Type::photon)
if (!p.type().is_neutron() && !p.type().is_photon())
continue;
// Skip any event where the particle didn't scatter
@ -1177,7 +1173,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
break;
case SCORE_NU_SCATTER:
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// Only analog estimators are available.
@ -1202,7 +1198,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
break;
case SCORE_ABSORPTION:
if (p.type() != Type::neutron && p.type() != Type::photon)
if (!p.type().is_neutron() && !p.type().is_photon())
continue;
if (settings::survival_biasing) {
@ -1431,7 +1427,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
// this loop.
for (auto d = 1; d < rxn.products_.size(); ++d) {
const auto& product = rxn.products_[d];
if (product.particle_ != Type::neutron)
if (!product.particle_.is_neutron())
continue;
auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
@ -1523,7 +1519,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
continue;
case ELASTIC:
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// Check if event MT matches
@ -1552,7 +1548,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
if (!simulation::need_depletion_rx)
goto default_case;
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// Check if the event MT matches
@ -1565,7 +1561,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
case INCOHERENT:
case PHOTOELECTRIC:
case PAIR_PROD:
if (p.type() != Type::photon)
if (!p.type().is_photon())
continue;
if (score_bin == PHOTOELECTRIC) {
@ -1592,7 +1588,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
// The default block is really only meant for redundant neutron reactions
// (e.g. 444, 901)
if (p.type() != Type::neutron)
if (!p.type().is_neutron())
continue;
// Any other score is assumed to be a MT number. Thus, we just need
@ -2316,10 +2312,7 @@ void score_analog_tally_ce(Particle& p)
// Since electrons/positrons are not transported, we assign a flux of zero.
// Note that the heating score does NOT use the flux and will be non-zero for
// electrons/positrons.
double flux =
(p.type() == ParticleType::neutron || p.type() == ParticleType::photon)
? 1.0
: 0.0;
double flux = (p.type().is_neutron() || p.type().is_photon()) ? 1.0 : 0.0;
for (auto i_tally : model::active_analog_tallies) {
const Tally& tally {*model::tallies[i_tally]};
@ -2447,7 +2440,7 @@ void score_tracklength_tally_general(
if (j == C_NONE) {
// Determine log union grid index
if (i_log_union == C_NONE) {
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
i_log_union = std::log(p.E() / data::energy_min[neutron]) /
simulation::log_spacing;
}
@ -2544,7 +2537,7 @@ void score_collision_tally(Particle& p)
{
// Determine the collision estimate of the flux
double flux = 0.0;
if (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) {
if (p.type().is_neutron() || p.type().is_photon()) {
flux = p.wgt_last() / p.macro_xs().total;
}
@ -2578,7 +2571,7 @@ void score_collision_tally(Particle& p)
if (j == C_NONE) {
// Determine log union grid index
if (i_log_union == C_NONE) {
int neutron = static_cast<int>(ParticleType::neutron);
int neutron = ParticleType::neutron().transport_index();
i_log_union = std::log(p.E() / data::energy_min[neutron]) /
simulation::log_spacing;
}

View file

@ -122,7 +122,7 @@ void finalize_particle_track(Particle& p)
int offset = 0;
for (auto& track_i : p.tracks()) {
offsets.push_back(offset);
particles.push_back(static_cast<int>(track_i.particle));
particles.push_back(track_i.particle.pdg_number());
offset += track_i.states.size();
tracks.insert(tracks.end(), track_i.states.begin(), track_i.states.end());
}

View file

@ -59,7 +59,7 @@ void apply_weight_windows(Particle& p)
return;
// WW on photon and neutron only
if (p.type() != ParticleType::neutron && p.type() != ParticleType::photon)
if (!p.type().is_neutron() && !p.type().is_photon())
return;
// skip dead or no energy
@ -179,7 +179,7 @@ WeightWindows::WeightWindows(pugi::xml_node node)
// get the particle type
auto particle_type_str = std::string(get_node_value(node, "particle_type"));
particle_type_ = openmc::str_to_particle_type(particle_type_str);
particle_type_ = ParticleType {particle_type_str};
// Determine associated mesh
int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
@ -252,7 +252,7 @@ WeightWindows* WeightWindows::from_hdf5(
std::string particle_type;
read_dataset(ww_group, "particle_type", particle_type);
wws->particle_type_ = openmc::str_to_particle_type(particle_type);
wws->particle_type_ = ParticleType {particle_type};
read_dataset<double>(ww_group, "energy_bounds", wws->energy_bounds_);
@ -284,7 +284,10 @@ void WeightWindows::set_defaults()
{
// set energy bounds to the min/max energy supported by the data
if (energy_bounds_.size() == 0) {
int p_type = static_cast<int>(particle_type_);
int p_type = particle_type_.transport_index();
if (p_type == C_NONE) {
fatal_error("Weight windows particle is not supported for transport.");
}
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
@ -345,10 +348,9 @@ void WeightWindows::set_energy_bounds(span<const double> bounds)
void WeightWindows::set_particle_type(ParticleType p_type)
{
if (p_type != ParticleType::neutron && p_type != ParticleType::photon)
fatal_error(
fmt::format("Particle type '{}' cannot be applied to weight windows.",
particle_type_to_str(p_type)));
if (!p_type.is_neutron() && !p_type.is_photon())
fatal_error(fmt::format(
"Particle type '{}' cannot be applied to weight windows.", p_type.str()));
particle_type_ = p_type;
}
@ -608,8 +610,7 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value,
if (p_it == particles.end()) {
auto msg = fmt::format("Particle type '{}' not present on Filter {} for "
"Tally {} used to update WeightWindows {}",
particle_type_to_str(this->particle_type_), pf->id(), tally->id(),
this->id());
this->particle_type_.str(), pf->id(), tally->id(), this->id());
fatal_error(msg);
}
@ -818,8 +819,7 @@ void WeightWindows::to_hdf5(hid_t group) const
hid_t ww_group = create_group(group, fmt::format("weight_windows_{}", id()));
write_dataset(ww_group, "mesh", this->mesh()->id());
write_dataset(
ww_group, "particle_type", openmc::particle_type_to_str(particle_type_));
write_dataset(ww_group, "particle_type", particle_type_.str());
write_dataset(ww_group, "energy_bounds", energy_bounds_);
write_dataset(ww_group, "lower_ww_bounds", lower_ww_);
write_dataset(ww_group, "upper_ww_bounds", upper_ww_);
@ -846,8 +846,8 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node)
max_realizations_, active_batches);
warning(msg);
}
auto tmp_str = get_node_value(node, "particle_type", true, true);
auto particle_type = str_to_particle_type(tmp_str);
auto tmp_str = get_node_value(node, "particle_type", false, true);
auto particle_type = ParticleType {tmp_str};
update_interval_ = std::stoi(get_node_value(node, "update_interval"));
on_the_fly_ = get_node_value_bool(node, "on_the_fly");
@ -856,7 +856,10 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node)
if (check_for_node(node, "energy_bounds")) {
e_bounds = get_node_array<double>(node, "energy_bounds");
} else {
int p_type = static_cast<int>(particle_type);
int p_type = particle_type.transport_index();
if (p_type == C_NONE) {
fatal_error("Weight windows particle is not supported for transport.");
}
e_bounds.push_back(data::energy_min[p_type]);
e_bounds.push_back(data::energy_max[p_type]);
}
@ -1110,23 +1113,25 @@ extern "C" int openmc_weight_windows_get_energy_bounds(
return 0;
}
extern "C" int openmc_weight_windows_set_particle(int32_t index, int particle)
extern "C" int openmc_weight_windows_set_particle(
int32_t index, int32_t particle)
{
if (int err = verify_ww_index(index))
return err;
const auto& wws = variance_reduction::weight_windows.at(index);
wws->set_particle_type(static_cast<ParticleType>(particle));
wws->set_particle_type(ParticleType {particle});
return 0;
}
extern "C" int openmc_weight_windows_get_particle(int32_t index, int* particle)
extern "C" int openmc_weight_windows_get_particle(
int32_t index, int32_t* particle)
{
if (int err = verify_ww_index(index))
return err;
const auto& wws = variance_reduction::weight_windows.at(index);
*particle = static_cast<int>(wws->particle_type());
*particle = wws->particle_type().pdg_number();
return 0;
}

View file

@ -25,7 +25,7 @@ TEST_CASE("MCPL stat:sum field")
// Initialize test particles
for (int i = 0; i < 100; ++i) {
source_bank[i].particle = openmc::ParticleType::neutron;
source_bank[i].particle = openmc::ParticleType::neutron();
source_bank[i].r = {i * 0.1, i * 0.2, i * 0.3};
source_bank[i].u = {0.0, 0.0, 1.0};
source_bank[i].E = 2.0e6; // 2 MeV
@ -63,7 +63,7 @@ TEST_CASE("MCPL stat:sum field")
// Initialize particles
for (int i = 0; i < count; ++i) {
source_bank[i].particle = openmc::ParticleType::neutron;
source_bank[i].particle = openmc::ParticleType::neutron();
source_bank[i].r = {0.0, 0.0, 0.0};
source_bank[i].u = {0.0, 0.0, 1.0};
source_bank[i].E = 1.0e6;

View file

@ -10,7 +10,7 @@ class CustomSource : public openmc::Source {
{
openmc::SourceSite particle;
// wgt
particle.particle = openmc::ParticleType::neutron;
particle.particle = openmc::ParticleType::neutron();
particle.wgt = 1.0;
// position

View file

@ -2,36 +2,37 @@
#include "openmc/source.h"
class CustomSource : public openmc::Source {
public:
CustomSource(double energy) : energy_(energy) { }
public:
CustomSource(double energy) : energy_(energy) {}
// Samples from an instance of this class.
openmc::SourceSite sample(uint64_t* seed) const
{
openmc::SourceSite particle;
// wgt
particle.particle = openmc::ParticleType::neutron;
particle.wgt = 1.0;
// position
particle.r.x = 0.0;
particle.r.y = 0.0;
particle.r.z = 0.0;
// angle
particle.u = {1.0, 0.0, 0.0};
particle.E = this->energy_;
particle.delayed_group = 0;
// Samples from an instance of this class.
openmc::SourceSite sample(uint64_t* seed) const
{
openmc::SourceSite particle;
// wgt
particle.particle = openmc::ParticleType::neutron();
particle.wgt = 1.0;
// position
particle.r.x = 0.0;
particle.r.y = 0.0;
particle.r.z = 0.0;
// angle
particle.u = {1.0, 0.0, 0.0};
particle.E = this->energy_;
particle.delayed_group = 0;
return particle;
}
return particle;
}
private:
double energy_;
private:
double energy_;
};
// A function to create a unique pointer to an instance of this class when generated
// via a plugin call using dlopen/dlsym.
// You must have external C linkage here otherwise dlopen will not find the file
extern "C" std::unique_ptr<CustomSource> openmc_create_source(std::string parameter)
// A function to create a unique pointer to an instance of this class when
// generated via a plugin call using dlopen/dlsym. You must have external C
// linkage here otherwise dlopen will not find the file
extern "C" std::unique_ptr<CustomSource> openmc_create_source(
std::string parameter)
{
double energy = std::stod(parameter);
return std::make_unique<CustomSource>(energy);

View file

@ -634,7 +634,7 @@ def return_surface_source_data(filepath):
wgt = point.wgt
delayed_group = point.delayed_group
surf_id = point.surf_id
particle = point.particle
particle = point.particle.pdg_number
key = (
f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}"
f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}"

View file

@ -0,0 +1,298 @@
"""Unit tests for ParticleType class."""
import pytest
from openmc import ParticleType
# Tests for creating ParticleType instances
def test_create_from_int():
"""Test creation from PDG number."""
p = ParticleType(2112)
assert p.pdg_number == 2112
assert int(p) == 2112
def test_create_from_string_name():
"""Test creation from particle name."""
p = ParticleType('neutron')
assert p.pdg_number == 2112
p = ParticleType('photon')
assert p.pdg_number == 22
def test_create_from_string_aliases():
"""Test creation from particle aliases."""
assert ParticleType('n').pdg_number == 2112
assert ParticleType('gamma').pdg_number == 22
assert ParticleType('p').pdg_number == 2212
assert ParticleType('proton').pdg_number == 2212
assert ParticleType('d').pdg_number == 1000010020
assert ParticleType('t').pdg_number == 1000010030
def test_create_from_string_nuclide():
"""Test creation from GNDS nuclide name."""
p = ParticleType('He4')
assert p.pdg_number == 1000020040
p = ParticleType('U235')
assert p.pdg_number == 1000922350
p = ParticleType('Am242_m1')
assert p.pdg_number == 1000952421
def test_create_from_string_pdg_prefix():
"""Test creation with pdg: prefix."""
p = ParticleType('pdg:2112')
assert p.pdg_number == 2112
p = ParticleType('PDG:22')
assert p.pdg_number == 22
def test_create_from_particle_type():
"""Test creation from existing ParticleType."""
p1 = ParticleType(2112)
p2 = ParticleType(p1)
assert p1 == p2
assert p1 is not p2 # Different instances
def test_legacy_particle_indices():
"""Test backward compatibility with legacy indices 0-3."""
assert ParticleType(0) == ParticleType.NEUTRON
assert ParticleType(1) == ParticleType.PHOTON
assert ParticleType(2) == ParticleType.ELECTRON
assert ParticleType(3) == ParticleType.POSITRON
def test_create_invalid_type():
"""Test creation with invalid type raises TypeError."""
with pytest.raises(TypeError):
ParticleType([2112])
with pytest.raises(TypeError):
ParticleType({'pdg': 2112})
def test_create_invalid_string():
"""Test creation with invalid string raises ValueError."""
with pytest.raises(ValueError):
ParticleType('')
with pytest.raises(ValueError):
ParticleType('pdg:invalid')
def test_create_case_insensitive():
"""Test that string parsing is case insensitive for aliases."""
assert ParticleType('NEUTRON').pdg_number == 2112
assert ParticleType('Neutron').pdg_number == 2112
assert ParticleType('PHOTON').pdg_number == 22
# Tests for equality and comparison
def test_equality_same_pdg():
"""Test that instances with same PDG are equal."""
p1 = ParticleType(2112)
p2 = ParticleType(2112)
assert p1 == p2
def test_equality_int():
"""Test equality comparison with int."""
p = ParticleType(2112)
assert p == 2112
assert 2112 == p
def test_equality_string():
"""Test equality comparison with string."""
p = ParticleType(2112)
assert p == 'neutron'
assert p == 'pdg:2112'
assert p == 'n'
def test_inequality():
"""Test inequality comparisons."""
p1 = ParticleType(2112)
p2 = ParticleType(22)
assert p1 != p2
assert p1 != 22
assert p1 != 'photon'
def test_equality_with_constants():
"""Test equality with class constants."""
p = ParticleType(2112)
assert p == ParticleType.NEUTRON
assert ParticleType.NEUTRON == p
def test_equality_invalid_string():
"""Test equality with invalid string returns False."""
p = ParticleType(2112)
assert not (p == 'invalid_particle')
assert p != 'invalid_particle'
# Tests for hashing behavior
def test_hash_consistency():
"""Test that equal instances hash to the same value."""
p1 = ParticleType(2112)
p2 = ParticleType(2112)
assert hash(p1) == hash(p2)
def test_set_deduplication():
"""Test that equal instances deduplicate in sets."""
p1 = ParticleType(2112)
p2 = ParticleType(2112)
s = {p1, p2}
assert len(s) == 1
def test_dict_key():
"""Test use as dictionary key."""
p1 = ParticleType(2112)
p2 = ParticleType(2112)
d = {p1: 'neutron'}
assert d[p2] == 'neutron'
def test_hash_different_particles():
"""Test that different particles have different hashes (usually)."""
p1 = ParticleType(2112)
p2 = ParticleType(22)
# Different PDG numbers should (almost always) have different hashes
assert hash(p1) != hash(p2)
# Tests for properties and computed attributes
def test_pdg_number_property():
"""Test pdg_number property."""
p = ParticleType(2112)
assert p.pdg_number == 2112
assert isinstance(p.pdg_number, int)
def test_int_conversion():
"""Test __int__ conversion."""
p = ParticleType(1000020040)
assert int(p) == 1000020040
def test_zam_elementary():
"""Test zam property for elementary particles."""
assert ParticleType.NEUTRON.zam is None
assert ParticleType.PHOTON.zam is None
assert ParticleType.ELECTRON.zam is None
assert ParticleType.POSITRON.zam is None
def test_zam_nucleus():
"""Test zam property for nuclear particles."""
he4 = ParticleType('He4')
assert he4.zam == (2, 4, 0)
u235 = ParticleType('U235')
Z, A, m = u235.zam
assert Z == 92
assert A == 235
assert m == 0
def test_zam_metastable():
"""Test zam property for metastable nuclei."""
# Am242m has m=1
am242m = ParticleType('Am242_m1')
Z, A, m = am242m.zam
assert Z == 95
assert A == 242
assert m == 1
def test_is_nucleus_false():
"""Test is_nucleus for elementary particles."""
assert not ParticleType.NEUTRON.is_nucleus
assert not ParticleType.PHOTON.is_nucleus
assert not ParticleType.ELECTRON.is_nucleus
assert not ParticleType.POSITRON.is_nucleus
def test_is_nucleus_true():
"""Test is_nucleus for nuclear particles."""
assert ParticleType.ALPHA.is_nucleus
assert ParticleType('He4').is_nucleus
assert ParticleType('U235').is_nucleus
assert ParticleType.DEUTERON.is_nucleus
assert ParticleType.TRITON.is_nucleus
# Tests for __str__ and __repr__
def test_str_elementary():
"""Test string representation of elementary particles."""
assert str(ParticleType.NEUTRON) == 'neutron'
assert str(ParticleType.PHOTON) == 'photon'
assert str(ParticleType.ELECTRON) == 'electron'
assert str(ParticleType.POSITRON) == 'positron'
assert str(ParticleType.PROTON) == 'H1'
def test_str_nucleus():
"""Test string representation of nuclei."""
assert str(ParticleType.ALPHA) == 'He4'
assert str(ParticleType('U235')) == 'U235'
assert str(ParticleType.DEUTERON) == 'H2'
assert str(ParticleType.TRITON) == 'H3'
def test_str_arbitrary_pdg():
"""Test string representation of arbitrary PDG number."""
# PDG number that doesn't match any known particle
p = ParticleType(12345)
assert str(p) == 'pdg:12345'
def test_repr():
"""Test repr includes PDG number."""
p = ParticleType.NEUTRON
repr_str = repr(p)
assert 'ParticleType' in repr_str
assert 'PDG=2112' in repr_str
assert 'neutron' in repr_str
def test_repr_nucleus():
"""Test repr for nuclear particles."""
p = ParticleType.ALPHA
repr_str = repr(p)
assert 'ParticleType' in repr_str
assert 'PDG=1000020040' in repr_str
assert 'He4' in repr_str
def test_create_from_numpy_int():
"""Test creation from numpy integer types."""
import numpy as np
p = ParticleType(np.int32(2112))
assert p.pdg_number == 2112
p = ParticleType(np.int64(22))
assert p.pdg_number == 22
def test_equality_numpy_int():
"""Test equality comparison with numpy integer types."""
import numpy as np
p = ParticleType(2112)
assert p == np.int32(2112)
assert p == np.int64(2112)

View file

@ -42,7 +42,7 @@ def test_source_file(run_in_tmpdir):
assert np.all(arr['E'] == n - np.arange(n))
assert np.all(arr['wgt'] == 1.0)
assert np.all(arr['delayed_group'] == 0)
assert np.all(arr['particle'] == 0)
assert np.all(arr['particle'] == 2112) # PDG number for neutron
# Ensure sites read in are consistent
sites = openmc.ParticleList.from_hdf5('test_source.h5')
@ -64,7 +64,7 @@ def test_source_file(run_in_tmpdir):
dgs = np.array([s.delayed_group for s in sites])
assert np.all(dgs == 0)
p_types = np.array([s.particle for s in sites])
assert np.all(p_types == 0)
assert np.all(p_types == 2112) # PDG number for neutron
# Ensure a ParticleList item is a SourceParticle
site = sites[0]

View file

@ -70,7 +70,7 @@ def test_tracks(sphere_model, particle, run_in_tmpdir):
particle_track = track.particle_tracks[0]
assert isinstance(particle_track, openmc.ParticleTrack)
assert particle_track.particle.name.lower() == particle
assert str(particle_track.particle).lower() == particle
assert isinstance(particle_track.states, np.ndarray)
# Sanity checks on actual data
@ -140,8 +140,10 @@ def test_filter(sphere_model, run_in_tmpdir):
assert matches == tracks
matches = tracks.filter(state_filter=lambda s: s['E'] > 0.0)
assert matches == tracks
matches = tracks.filter(particle='bunnytron')
matches = tracks.filter(particle='proton')
assert matches == []
with pytest.raises(ValueError):
tracks.filter(particle='bunnytron')
def test_write_to_vtk(sphere_model):

View file

@ -52,7 +52,7 @@ n_mesh.z_grid = np.array([-100.0,
n_e_bounds = (np.array([0.0,
100000.0,
146780.0]),)
n_particles = ('neutron',)
n_particles = [openmc.ParticleType.NEUTRON]
# expected results - neutron and photon data
np_mesh = openmc.RectilinearMesh()
@ -63,7 +63,7 @@ np_mesh.z_grid = n_mesh.z_grid
np_e_bounds = (np.array([0.0, 100000.0, 146780.0, 215440.0]),
np.array([0.0, 1.0E8]))
np_particles = ('neutron', 'photon')
np_particles = [openmc.ParticleType.NEUTRON, openmc.ParticleType.PHOTON]
# expected results - photon data only
p_mesh = openmc.RectilinearMesh()
@ -74,7 +74,7 @@ p_mesh.y_grid = np_mesh.y_grid
p_mesh.z_grid = np.array([-50.0, 50.0])
p_e_bounds = (np.array([0.0, 100000.0, 146780.0, 215440.0, 316230.0]),)
p_particles = ('photon',)
p_particles = [openmc.ParticleType.PHOTON]
expected_results = [('wwinp_n', n_mesh, n_particles, n_e_bounds),
('wwinp_np', np_mesh, np_particles, np_e_bounds),