Extend ParticleProductionFilter to support multiple particle types (#3780)

This commit is contained in:
Paul Romano 2026-02-13 22:18:29 -06:00 committed by GitHub
parent 7fc5b94877
commit a35927aad3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 512 additions and 185 deletions

View file

@ -427,6 +427,7 @@ list(APPEND libopenmc_SOURCES
src/tallies/filter_musurface.cpp
src/tallies/filter_parent_nuclide.cpp
src/tallies/filter_particle.cpp
src/tallies/filter_particle_production.cpp
src/tallies/filter_polar.cpp
src/tallies/filter_sph_harm.cpp
src/tallies/filter_sptl_legendre.cpp

View file

@ -142,9 +142,9 @@ attributes/sub-elements:
:type:
The type of the filter. Accepted options are "cell", "cellfrom",
"cellborn", "surface", "material", "universe", "energy", "energyout", "mu",
"polar", "azimuthal", "mesh", "distribcell", "delayedgroup",
"energyfunction", and "particle".
"cellborn", "surface", "material", "universe", "energy", "energyout",
"mu", "polar", "azimuthal", "mesh", "distribcell", "delayedgroup",
"energyfunction", "particle", and "particleproduction".
:bins:
A description of the bins for each type of filter can be found in
@ -321,6 +321,32 @@ should be set to:
A list of particle identifiers to tally, specified as strings (e.g.,
``neutron``, ``photon``, ``He4``) or as integer PDG numbers.
:particleproduction:
This filter tallies secondary particles produced in reactions, binned by
particle type and, optionally, by energy. Unlike other energy filters, the
weight applied is the weight of the secondary particle. To obtain secondary
particle production rates, use this filter with the ``events`` score.
The filter uses the following sub-elements instead of ``bins``:
:particles:
A space-separated list of secondary particle types to tally (e.g.,
``photon``, ``neutron``, ``electron``).
:energies:
An optional monotonically increasing list of energy boundaries in [eV]
for binning the secondary particle energies. If omitted, total production
is tallied without energy binning.
For example, to tally photon and neutron production in three energy groups:
.. code-block:: xml
<filter id="1" type="particleproduction">
<particles>photon neutron</particles>
<energies>0.0 1.0e5 1.0e6 20.0e6</energies>
</filter>
------------------
``<mesh>`` Element
------------------

View file

@ -73,36 +73,5 @@ public:
std::string text_label(int bin) const override;
};
//==============================================================================
//! Bins the outgoing energy of secondary particles
//!
//! This filter can be used to get the photon production matrix for multigroup
//! photon transport, the energy distribution of secondary neutrons, etc. Unlike
//! other energy filters, the weight that is applied is equal to the weight of
//! the secondary particle. Thus, to get secondary production it should be used
//! in conjunction with the "events" score.
//==============================================================================
class ParticleProductionFilter : public EnergyFilter {
public:
//----------------------------------------------------------------------------
// Methods
std::string type_str() const override { return "particleproduction"; }
FilterType type() const override { return FilterType::PARTICLE_PRODUCTION; }
void get_all_bins(const Particle& p, TallyEstimator estimator,
FilterMatch& match) const override;
std::string text_label(int bin) const override;
void from_xml(pugi::xml_node node) override;
void to_statepoint(hid_t filter_group) const override;
protected:
ParticleType secondary_type_; //!< Type of secondary particle to filter
};
} // namespace openmc
#endif // OPENMC_TALLIES_FILTER_ENERGY_H

View file

@ -0,0 +1,53 @@
#ifndef OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H
#define OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H
#include <unordered_map>
#include "openmc/particle.h"
#include "openmc/tallies/filter.h"
#include "openmc/vector.h"
namespace openmc {
//==============================================================================
//! Bins the outgoing energy of secondary particles
//!
//! This filter bins secondary particles by type and, optionally, by energy. It
//! can be used to get the photon production matrix for multigroup photon
//! transport, the energy distribution of secondary neutrons, etc. The weight
//! that is applied is equal to the weight of the secondary particle. Thus, to
//! get secondary production it should be used in conjunction with the "events"
//! score.
//==============================================================================
class ParticleProductionFilter : public Filter {
public:
//----------------------------------------------------------------------------
// Methods
std::string type_str() const override { return "particleproduction"; }
FilterType type() const override { return FilterType::PARTICLE_PRODUCTION; }
void get_all_bins(const Particle& p, TallyEstimator estimator,
FilterMatch& match) const override;
std::string text_label(int bin) const override;
void from_xml(pugi::xml_node node) override;
void to_statepoint(hid_t filter_group) const override;
private:
//----------------------------------------------------------------------------
// Data members
vector<ParticleType> secondary_types_; //!< Types of secondary particles
//! Map from PDG number to index in secondary_types_ for O(1) lookup
std::unordered_map<int32_t, int> type_to_index_;
vector<double> energy_bins_; //!< Energy bin boundaries (optional)
};
} // namespace openmc
#endif // OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H

View file

@ -24,9 +24,9 @@ _FILTER_TYPES = (
'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy',
'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup',
'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre',
'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance',
'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface',
'meshmaterial',
'sphericalharmonics', 'zernike', 'zernikeradial', 'particle',
'particleproduction', 'cellinstance', 'collision', 'time', 'parentnuclide',
'weight', 'meshborn', 'meshsurface', 'meshmaterial',
)
_CURRENT_NAMES = (
@ -1748,15 +1748,16 @@ class EnergyoutFilter(EnergyFilter):
"""
class ParticleProductionFilter(EnergyFilter):
"""Bins tally events based on energy of secondary particles.
class ParticleProductionFilter(Filter):
"""Bins tally events based on secondary particle type and energy.
This filter bins the energies of secondary particles (e.g., photons,
electrons, or recoils) produced in a reaction. This is useful for
constructing production matrices or analyzing secondary particle spectra.
Note that unlike other energy filters, the weight that is applied is equal
to the weight of the secondary particle. Thus, to obtain secondary particle
production, it should be used in conjunction with the "events" score.
This filter bins secondary particles (e.g., photons, electrons, or recoils)
produced in a reaction by particle type and, optionally, by energy. This is
useful for constructing production matrices or analyzing secondary particle
spectra. Note that unlike other energy filters, the weight that is applied
is equal to the weight of the secondary particle. Thus, to obtain secondary
particle production, it should be used in conjunction with the "events"
score.
The incident particle type can be filtered using :class:`ParticleFilter`.
@ -1764,63 +1765,206 @@ class ParticleProductionFilter(EnergyFilter):
Parameters
----------
particle : str, int, openmc.ParticleType
Type of secondary particle to tally ('photon', 'neutron', etc.)
values : Iterable of Real
particles : str, int, openmc.ParticleType, or iterable thereof
Type(s) of secondary particle(s) to tally ('photon', 'neutron', etc.)
energies : Iterable of Real or str, optional
A list of energy boundaries in [eV]; each successive pair defines a bin.
Alternatively, the name of the group structure can be given as a string
(must be a key in :data:`openmc.mgxs.GROUP_STRUCTURES`). If not
provided, the filter tallies total secondary particle production without
energy binning.
filter_id : int, optional
Unique identifier for the filter
Attributes
----------
values : numpy.ndarray
Energy boundaries in [eV]
bins : numpy.ndarray
Array of (low, high) energy bin pairs
particles : list of openmc.ParticleType
The secondary particle types this filter applies to
energies : numpy.ndarray or None
Energy boundaries in [eV], or None if no energy binning
bins : list
A list of bins; each element fully describes one bin. When energies are
specified, each element is a tuple ``(particle, energy_low,
energy_high)``. When no energies are specified, each element is a
particle name string.
num_bins : int
Number of filter bins
particle : str
The secondary particle type this filter applies to
Total number of filter bins
num_energy_bins : int
Number of energy bins (1 if no energies specified)
shape : tuple of int
Shape of the filter as (n_particles, n_energy_bins)
"""
def __init__(self, particle, values, filter_id=None):
super().__init__(values, filter_id)
self.particle = particle
def __init__(self, particles, energies=None, filter_id=None):
self.particles = particles
self.energies = energies
self.id = filter_id
def __repr__(self):
string = type(self).__name__ + '\n'
string += '{: <16}=\t{}\n'.format('\tParticle', self.particle)
string += '{: <16}=\t{}\n'.format('\tValues', self.values)
string += '{: <16}=\t{}\n'.format('\tParticles',
[str(p) for p in self.particles])
if self.energies is not None:
string += '{: <16}=\t{}\n'.format('\tEnergies', self.energies)
string += '{: <16}=\t{}\n'.format('\tID', self.id)
return string
@property
def particle(self) -> openmc.ParticleType:
return self._particle
def particles(self):
return self._particles
@particle.setter
def particle(self, particle):
self._particle = openmc.ParticleType(particle)
@particles.setter
def particles(self, particles):
if isinstance(particles, (str, int, openmc.ParticleType)):
self._particles = [openmc.ParticleType(particles)]
else:
self._particles = [openmc.ParticleType(p) for p in particles]
@property
def energies(self):
return self._energies
@energies.setter
def energies(self, energies):
if energies is None:
self._energies = None
elif isinstance(energies, str):
cv.check_value('energies', energies,
openmc.mgxs.GROUP_STRUCTURES.keys())
self._energies = np.array(
openmc.mgxs.GROUP_STRUCTURES[energies.upper()])
else:
energies = np.asarray(energies, dtype=float)
cv.check_length('energies', energies, 2)
for i in range(len(energies) - 1):
if energies[i + 1] <= energies[i]:
raise ValueError("Energy bins must be monotonically "
"increasing.")
self._energies = energies
@property
def bins(self):
if self.energies is None:
return [str(p) for p in self.particles]
else:
result = []
energy_pairs = np.vstack(
(self.energies[:-1], self.energies[1:])).T
for particle in self.particles:
for e_low, e_high in energy_pairs:
result.append((str(particle), e_low, e_high))
return result
@bins.setter
def bins(self, bins):
# bins is set indirectly through particles/energies
pass
def check_bins(self, bins):
pass
@property
def num_energy_bins(self):
if self.energies is None:
return 1
else:
return len(self.energies) - 1
@property
def num_bins(self):
return len(self.particles) * self.num_energy_bins
@property
def shape(self):
return (len(self.particles), self.num_energy_bins)
def to_xml_element(self):
element = super().to_xml_element()
subelement = ET.SubElement(element, 'particle')
subelement.text = str(self.particle)
element = ET.Element('filter')
element.set('id', str(self.id))
element.set('type', self.short_name.lower())
subelement = ET.SubElement(element, 'particles')
subelement.text = ' '.join(str(p) for p in self.particles)
if self.energies is not None:
subelement = ET.SubElement(element, 'energies')
subelement.text = ' '.join(str(e) for e in self.energies)
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
values = [float(x) for x in get_text(elem, 'bins').split()]
particle = get_text(elem, 'particle')
return cls(particle, values, filter_id=filter_id)
particles = get_text(elem, 'particles').split()
bins_elem = elem.find('energies')
if bins_elem is not None:
energies = [float(x) for x in bins_elem.text.split()]
else:
energies = None
return cls(particles, energies=energies, filter_id=filter_id)
@classmethod
def from_hdf5(cls, group, **kwargs):
filter_id = int(group.name.split('/')[-1].lstrip('filter '))
bins = group['bins'][()]
particle = group['particle'][()].decode()
return cls(particle, bins, filter_id=filter_id)
# Read particle types
particles = [b.decode() if isinstance(b, bytes) else b
for b in group['particles'][()]]
# Read energy bins if present
if 'energies' in group:
energies = group['energies'][()]
else:
energies = None
return cls(particles, energies=energies, filter_id=filter_id)
def get_pandas_dataframe(self, data_size, stride, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : int
The total number of bins in the tally corresponding to this filter
stride : int
Stride in memory for the filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with columns for particle type and, if energy
bins are specified, energy bin boundaries.
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
filter_dict = {}
key = self.short_name.lower()
n_ebins = self.num_energy_bins
# Particle column — outer dimension (changes slowest)
particle_names = [str(p) for p in self.particles]
filter_dict[key, 'particle'] = _repeat_and_tile(
np.array(particle_names), n_ebins * stride, data_size)
# Energy columns only if energies were specified
if self.energies is not None:
energy_pairs = np.vstack(
(self.energies[:-1], self.energies[1:])).T
filter_dict[key, 'energy low [eV]'] = _repeat_and_tile(
energy_pairs[:, 0], stride, data_size)
filter_dict[key, 'energy high [eV]'] = _repeat_and_tile(
energy_pairs[:, 1], stride, data_size)
return pd.DataFrame(filter_dict)
class TimeFilter(RealFilter):

View file

@ -31,6 +31,7 @@
#include "openmc/tallies/filter_musurface.h"
#include "openmc/tallies/filter_parent_nuclide.h"
#include "openmc/tallies/filter_particle.h"
#include "openmc/tallies/filter_particle_production.h"
#include "openmc/tallies/filter_polar.h"
#include "openmc/tallies/filter_sph_harm.h"
#include "openmc/tallies/filter_sptl_legendre.h"

View file

@ -117,50 +117,6 @@ std::string EnergyoutFilter::text_label(int bin) const
"Outgoing Energy [{}, {})", bins_.at(bin), bins_.at(bin + 1));
}
//==============================================================================
// ParticleProductionFilter implementation
//==============================================================================
void ParticleProductionFilter::get_all_bins(
const Particle& p, TallyEstimator estimator, FilterMatch& match) const
{
int start_idx = p.secondary_bank_index();
int end_idx = start_idx + p.n_secondaries();
// Loop over secondary bank entries
for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) {
// Check if this is the correct type of secondary, then match its energy if
// it's the right type
const auto& site = p.secondary_bank(bank_idx);
if (site.particle == secondary_type_) {
if (site.E >= bins_.front() && site.E <= bins_.back()) {
auto bin = lower_bound_index(bins_.begin(), bins_.end(), site.E);
match.bins_.push_back(bin);
match.weights_.push_back(site.wgt);
}
}
}
}
std::string ParticleProductionFilter::text_label(int bin) const
{
return fmt::format("Secondary {}, Energy [{}, {})", secondary_type_.str(),
bins_.at(bin), bins_.at(bin + 1));
}
void ParticleProductionFilter::from_xml(pugi::xml_node node)
{
EnergyFilter::from_xml(node);
std::string p = get_node_value(node, "particle");
secondary_type_ = ParticleType {p};
}
void ParticleProductionFilter::to_statepoint(hid_t filter_group) const
{
EnergyFilter::to_statepoint(filter_group);
write_dataset(filter_group, "particle", secondary_type_.str());
}
//==============================================================================
// C-API functions
//==============================================================================

View file

@ -0,0 +1,108 @@
#include "openmc/tallies/filter_particle_production.h"
#include <fmt/core.h>
#include "openmc/search.h"
#include "openmc/xml_interface.h"
namespace openmc {
//==============================================================================
// ParticleProductionFilter implementation
//==============================================================================
void ParticleProductionFilter::get_all_bins(
const Particle& p, TallyEstimator estimator, FilterMatch& match) const
{
int start_idx = p.secondary_bank_index();
int end_idx = start_idx + p.n_secondaries();
// Loop over secondary bank entries
for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) {
const auto& site = p.secondary_bank(bank_idx);
// Find which particle-type slot this secondary belongs to
auto it = type_to_index_.find(site.particle.pdg_number());
if (it == type_to_index_.end())
continue;
int particle_idx = it->second;
if (energy_bins_.empty()) {
// No energy binning, just particle type
match.bins_.push_back(particle_idx);
match.weights_.push_back(site.wgt);
} else {
// Bin the energy
if (site.E >= energy_bins_.front() && site.E <= energy_bins_.back()) {
int n_energies = static_cast<int>(energy_bins_.size()) - 1;
auto energy_idx =
lower_bound_index(energy_bins_.begin(), energy_bins_.end(), site.E);
match.bins_.push_back(particle_idx * n_energies + energy_idx);
match.weights_.push_back(site.wgt);
}
}
}
}
std::string ParticleProductionFilter::text_label(int bin) const
{
if (energy_bins_.empty()) {
return fmt::format("Secondary {}", secondary_types_.at(bin).str());
} else {
int n_energies = static_cast<int>(energy_bins_.size()) - 1;
int particle_idx = bin / n_energies;
int energy_idx = bin % n_energies;
return fmt::format("Secondary {}, Energy [{}, {})",
secondary_types_.at(particle_idx).str(), energy_bins_.at(energy_idx),
energy_bins_.at(energy_idx + 1));
}
}
void ParticleProductionFilter::from_xml(pugi::xml_node node)
{
// Read energy bins if present (optional)
if (check_for_node(node, "energies")) {
auto bins = get_node_array<double>(node, "energies");
for (int64_t i = 1; i < bins.size(); ++i) {
if (bins[i] <= bins[i - 1]) {
throw std::runtime_error {
"Energy bins must be monotonically increasing."};
}
}
energy_bins_.assign(bins.begin(), bins.end());
}
// Read particle types (required)
auto names = get_node_array<std::string>(node, "particles");
for (const auto& name : names) {
int idx = secondary_types_.size();
secondary_types_.emplace_back(name);
type_to_index_[secondary_types_.back().pdg_number()] = idx;
}
// Compute total bins
if (energy_bins_.empty()) {
n_bins_ = secondary_types_.size();
} else {
n_bins_ = secondary_types_.size() * (energy_bins_.size() - 1);
}
}
void ParticleProductionFilter::to_statepoint(hid_t filter_group) const
{
Filter::to_statepoint(filter_group);
// Write energy bins if present
if (!energy_bins_.empty()) {
write_dataset(filter_group, "energies", energy_bins_);
}
// Write particle types
vector<std::string> names;
for (const auto& pt : secondary_types_) {
names.push_back(pt.str());
}
write_dataset(filter_group, "particles", names);
}
} // namespace openmc

View file

@ -903,10 +903,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
break;
case SCORE_EVENTS:
// Simply count the number of scoring events
#pragma omp atomic
tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
continue;
// Simply count the number of scoring events
score = 1.0;
break;
case ELASTIC:
if (!p.type().is_neutron())
@ -1513,10 +1512,9 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index,
break;
case SCORE_EVENTS:
// Simply count the number of scoring events
#pragma omp atomic
tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
continue;
// Simply count the number of scoring events
score = 1.0;
break;
case ELASTIC:
if (!p.type().is_neutron())
@ -2291,10 +2289,9 @@ void score_general_mg(Particle& p, int i_tally, int start_index,
break;
case SCORE_EVENTS:
// Simply count the number of scoring events
#pragma omp atomic
tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
continue;
// Simply count the number of scoring events
score = 1.0;
break;
default:
continue;

View file

@ -238,8 +238,8 @@ tally 2:
3.994127E-08
4.815195E+06
6.359497E+12
1.373000E+00
4.448330E-01
1.340228E+00
4.256704E-01
1.788012E-04
8.768717E-09
3.941968E+00
@ -260,8 +260,8 @@ tally 2:
1.493588E-07
1.064740E+07
2.378109E+13
2.753000E+00
1.542179E+00
2.698926E+00
1.484990E+00
3.953669E-04
3.279028E-08
8.155605E+00
@ -282,8 +282,8 @@ tally 2:
1.319523E-07
1.005866E+07
2.100960E+13
2.829000E+00
1.702143E+00
2.795871E+00
1.662994E+00
3.735055E-04
2.896884E-08
8.365907E+00
@ -304,8 +304,8 @@ tally 2:
2.314228E-07
1.328300E+07
3.684741E+13
3.246000E+00
2.132054E+00
3.162533E+00
2.024101E+00
4.932336E-04
5.080661E-08
9.080077E+00
@ -326,8 +326,8 @@ tally 2:
1.984988E-07
1.206905E+07
3.160522E+13
3.759000E+00
2.944479E+00
3.670108E+00
2.815133E+00
4.481564E-04
4.357848E-08
1.076370E+01
@ -348,8 +348,8 @@ tally 2:
1.474939E-07
1.003129E+07
2.348415E+13
2.577000E+00
1.509201E+00
2.523640E+00
1.451617E+00
3.724888E-04
3.238084E-08
7.654439E+00
@ -370,8 +370,8 @@ tally 2:
1.993067E-07
1.242911E+07
3.173385E+13
3.266000E+00
2.172442E+00
3.198836E+00
2.085466E+00
4.615266E-04
4.375584E-08
9.265396E+00
@ -392,8 +392,8 @@ tally 2:
2.277785E-07
1.329858E+07
3.626717E+13
3.485000E+00
2.510947E+00
3.467276E+00
2.484377E+00
4.938123E-04
5.000655E-08
9.871966E+00
@ -414,8 +414,8 @@ tally 2:
1.293793E-09
7.073280E+05
2.059993E+11
3.000000E-01
4.514200E-02
2.978788E-01
4.498561E-02
2.626500E-05
2.840396E-10
9.197485E-01
@ -436,8 +436,8 @@ tally 2:
2.205650E-10
2.942995E+05
3.511863E+10
1.300000E-01
5.822000E-03
1.270281E-01
5.517180E-03
1.092814E-05
4.842290E-11
3.746117E-01
@ -908,8 +908,8 @@ tally 12:
3.994127E-08
4.815195E+06
6.359497E+12
1.373000E+00
4.448330E-01
1.340228E+00
4.256704E-01
1.788012E-04
8.768717E-09
2.806910E+00
@ -928,8 +928,8 @@ tally 12:
1.493588E-07
1.064740E+07
2.378109E+13
2.753000E+00
1.542179E+00
2.698926E+00
1.484990E+00
3.953669E-04
3.279028E-08
2.869647E+00
@ -948,8 +948,8 @@ tally 12:
1.319523E-07
1.005866E+07
2.100960E+13
2.829000E+00
1.702143E+00
2.795871E+00
1.662994E+00
3.735055E-04
2.896884E-08
3.141043E+00
@ -968,8 +968,8 @@ tally 12:
2.314228E-07
1.328300E+07
3.684741E+13
3.246000E+00
2.132054E+00
3.162533E+00
2.024101E+00
4.932336E-04
5.080661E-08
3.682383E+00
@ -988,8 +988,8 @@ tally 12:
1.984988E-07
1.206905E+07
3.160522E+13
3.759000E+00
2.944479E+00
3.670108E+00
2.815133E+00
4.481564E-04
4.357848E-08
2.634850E+00
@ -1008,8 +1008,8 @@ tally 12:
1.474939E-07
1.003129E+07
2.348415E+13
2.577000E+00
1.509201E+00
2.523640E+00
1.451617E+00
3.724888E-04
3.238084E-08
3.192584E+00
@ -1028,8 +1028,8 @@ tally 12:
1.993067E-07
1.242911E+07
3.173385E+13
3.266000E+00
2.172442E+00
3.198836E+00
2.085466E+00
4.615266E-04
4.375584E-08
3.402213E+00
@ -1048,8 +1048,8 @@ tally 12:
2.277785E-07
1.329858E+07
3.626717E+13
3.485000E+00
2.510947E+00
3.467276E+00
2.484377E+00
4.938123E-04
5.000655E-08
3.110378E-01
@ -1068,8 +1068,8 @@ tally 12:
1.293793E-09
7.073280E+05
2.059993E+11
3.000000E-01
4.514200E-02
2.978788E-01
4.498561E-02
2.626500E-05
2.840396E-10
1.267544E-01
@ -1088,8 +1088,8 @@ tally 12:
2.205650E-10
2.942995E+05
3.511863E+10
1.300000E-01
5.822000E-03
1.270281E-01
5.517180E-03
1.092814E-05
4.842290E-11
tally 13:

View file

@ -48,8 +48,8 @@
<bins>0.0 20000000.0</bins>
</filter>
<filter id="4" type="particleproduction">
<bins>0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0</bins>
<particle>photon</particle>
<particles>neutron photon</particles>
<energies>0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0</energies>
</filter>
<tally id="1">
<filters>1 2</filters>

View file

@ -48,8 +48,8 @@
<bins>0.0 20000000.0</bins>
</filter>
<filter id="4" type="particleproduction">
<bins>0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0</bins>
<particle>photon</particle>
<particles>neutron photon</particles>
<energies>0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0</energies>
</filter>
<tally id="1">
<filters>1 2</filters>

View file

@ -139,6 +139,16 @@ tally 4:
0.000000E+00
0.000000E+00
tally 5:
5.000000E-04
2.500000E-07
1.300000E-03
1.690000E-06
8.000000E-04
6.400000E-07
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
2.000000E-02
4.000000E-04
8.200000E-03

View file

@ -72,7 +72,7 @@ def model():
# Track source energies of secondary gammas
ene2_filter = openmc.ParticleProductionFilter(
'photon', [0.0, 100e3, 300e3, 500e3, 2e6, 20e6])
['neutron', 'photon'], [0.0, 100e3, 300e3, 500e3, 2e6, 20e6])
neutron_only = openmc.ParticleFilter(['neutron'])
tally_gam_ene = openmc.Tally()

View file

@ -321,12 +321,22 @@ def test_energy_filter():
def test_particle_production_filter():
energy_bins = [1e3, 1e4, 1e5, 1e6]
# --- Single particle with energy bins ---
f = openmc.ParticleProductionFilter('photon', energy_bins)
assert f.particle == openmc.ParticleType.PHOTON
# particles getter always returns a list
assert isinstance(f.particles, list)
assert len(f.particles) == 1
assert f.particles[0] == openmc.ParticleType.PHOTON
assert f.num_energy_bins == 3
assert f.num_bins == 3
assert f.bins.shape == (3, 2)
assert np.allclose(f.values, energy_bins)
assert f.shape == (1, 3)
assert len(f.bins) == 3
# Each bin is (particle_name, e_low, e_high)
assert f.bins[0] == ('photon', 1e3, 1e4)
assert f.bins[2] == ('photon', 1e5, 1e6)
# __repr__ check
repr(f)
@ -335,20 +345,72 @@ def test_particle_production_filter():
elem = f.to_xml_element()
assert elem.tag == 'filter'
assert elem.attrib['type'] == 'particleproduction'
assert elem.find('particle').text == 'photon'
assert elem.find('bins').text.split()[0] == str(energy_bins[0])
assert elem.find('particles').text == 'photon'
assert elem.find('energies').text.split()[0] == str(energy_bins[0])
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert new_f.particle == f.particle
assert np.allclose(new_f.bins, f.bins)
assert new_f.particles == f.particles
assert np.allclose(new_f.energies, f.energies)
# pandas output
# pandas output (with energy bins -> 3 MultiIndex columns)
df = f.get_pandas_dataframe(data_size=3, stride=1)
assert df.shape[0] == 3
assert "particleproduction low [eV]" in df.columns
assert "particleproduction high [eV]" in df.columns
assert ('particleproduction', 'particle') in df.columns
assert ('particleproduction', 'energy low [eV]') in df.columns
assert ('particleproduction', 'energy high [eV]') in df.columns
# --- Multiple particles with energy bins ---
f2 = openmc.ParticleProductionFilter(['photon', 'neutron'], energy_bins)
assert len(f2.particles) == 2
assert f2.num_bins == 6 # 2 particles * 3 energy bins
assert f2.shape == (2, 3)
assert len(f2.bins) == 6
# First 3 bins are photon, next 3 are neutron
assert f2.bins[0] == ('photon', 1e3, 1e4)
assert f2.bins[3] == ('neutron', 1e3, 1e4)
df2 = f2.get_pandas_dataframe(data_size=6, stride=1)
assert df2.shape[0] == 6
assert list(df2[('particleproduction', 'particle')]) == \
['photon'] * 3 + ['neutron'] * 3
# XML round-trip
elem2 = f2.to_xml_element()
new_f2 = openmc.Filter.from_xml_element(elem2)
assert len(new_f2.particles) == 2
assert np.allclose(new_f2.energies, energy_bins)
# --- Multiple particles without energy bins ---
f3 = openmc.ParticleProductionFilter(['photon', 'neutron', 'electron'])
assert f3.energies is None
assert f3.num_bins == 3
assert f3.num_energy_bins == 1
assert f3.shape == (3, 1)
assert f3.bins == ['photon', 'neutron', 'electron']
repr(f3)
df3 = f3.get_pandas_dataframe(data_size=3, stride=1)
assert df3.shape[0] == 3
assert ('particleproduction', 'particle') in df3.columns
# Should not have energy columns
assert ('particleproduction', 'energy low [eV]') not in df3.columns
# XML round-trip without energies
elem3 = f3.to_xml_element()
assert elem3.find('energies') is None
new_f3 = openmc.Filter.from_xml_element(elem3)
assert new_f3.energies is None
assert len(new_f3.particles) == 3
# --- Energies from group structure name ---
f4 = openmc.ParticleProductionFilter('photon', energies='CCFE-709')
expected = openmc.mgxs.GROUP_STRUCTURES['CCFE-709']
assert np.allclose(f4.energies, expected)
assert f4.num_energy_bins == len(expected) - 1
assert f4.num_bins == len(expected) - 1
def test_weight():