diff --git a/CMakeLists.txt b/CMakeLists.txt index d3119fb8758..138bf19bb04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -426,6 +426,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 diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 0ba0e061fd5..dc57e577511 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -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 + + + photon neutron + 0.0 1.0e5 1.0e6 20.0e6 + + ------------------ ```` Element ------------------ diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index 6bc9ff6b152..3e410d9fdc6 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -73,36 +73,5 @@ class EnergyoutFilter : public EnergyFilter { 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 diff --git a/include/openmc/tallies/filter_particle_production.h b/include/openmc/tallies/filter_particle_production.h new file mode 100644 index 00000000000..00fac9f7ccf --- /dev/null +++ b/include/openmc/tallies/filter_particle_production.h @@ -0,0 +1,53 @@ +#ifndef OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H +#define OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H + +#include + +#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 secondary_types_; //!< Types of secondary particles + + //! Map from PDG number to index in secondary_types_ for O(1) lookup + std::unordered_map type_to_index_; + + vector energy_bins_; //!< Energy bin boundaries (optional) +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H diff --git a/openmc/filter.py b/openmc/filter.py index 7561f6be6f2..9a2836e57c8 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -24,9 +24,9 @@ '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 + + @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 - @particle.setter - def particle(self, particle): - self._particle = openmc.ParticleType(particle) + @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): diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 16e4c055021..b0e9624c9e2 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -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" diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 0b039440a0c..0b954cce3a3 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -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 //============================================================================== diff --git a/src/tallies/filter_particle_production.cpp b/src/tallies/filter_particle_production.cpp new file mode 100644 index 00000000000..f8d82fdd926 --- /dev/null +++ b/src/tallies/filter_particle_production.cpp @@ -0,0 +1,108 @@ +#include "openmc/tallies/filter_particle_production.h" + +#include + +#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(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(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(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(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 names; + for (const auto& pt : secondary_types_) { + names.push_back(pt.str()); + } + write_dataset(filter_group, "particles", names); +} + +} // namespace openmc diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 51b5d9ffcc5..ccc70fc8f14 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -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; diff --git a/tests/regression_tests/mg_tallies/results_true.dat b/tests/regression_tests/mg_tallies/results_true.dat index 07fe22ce503..c1591cca288 100644 --- a/tests/regression_tests/mg_tallies/results_true.dat +++ b/tests/regression_tests/mg_tallies/results_true.dat @@ -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: diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index cc618eaff13..0ae0079f412 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -48,8 +48,8 @@ 0.0 20000000.0 - 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 - photon + neutron photon + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 1 2 diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index cc618eaff13..0ae0079f412 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -48,8 +48,8 @@ 0.0 20000000.0 - 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 - photon + neutron photon + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 1 2 diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index ec319aa0eda..c4f5fafa252 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -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 diff --git a/tests/regression_tests/photon_production/test.py b/tests/regression_tests/photon_production/test.py index 65d0688e98d..5ed5c5ecf02 100644 --- a/tests/regression_tests/photon_production/test.py +++ b/tests/regression_tests/photon_production/test.py @@ -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() diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index ba3226e3af3..2e7481c8767 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -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():