From 1441cf6a528c37b620b4f8862c0648dab0a12ce8 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Tue, 17 Mar 2026 11:32:10 -0600 Subject: [PATCH 01/21] Add SpatialExpansionFilter, SpatialLegendreFilter to Python API --- openmc/filter.py | 3 +- openmc/filter_expansion.py | 144 +++++++++++++++++++++++++++++-------- 2 files changed, 115 insertions(+), 32 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index 87aeb70c3a7..4626a10c789 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -25,7 +25,8 @@ 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', - 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', + 'spatialfourier', 'spatiallegendre', 'sphericalharmonics', + 'zernike', 'zernikeradial', 'particle', 'particleproduction', 'cellinstance', 'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface', 'meshmaterial', 'reaction', ) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index b79c8fc79e7..90fbc5ec282 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -137,17 +137,18 @@ def from_hdf5(cls, group, **kwargs): return out -class SpatialLegendreFilter(ExpansionFilter): - r"""Score Legendre expansion moments in space up to specified order. - - This filter allows scores to be multiplied by Legendre polynomials of the - the particle's position along a particular axis, normalized to a given - range, up to a user-specified order. - +class SpatialExpansionFilter(ExpansionFilter): + """Abstract base class for spatial functional expansion filters. + + This class provides common functionality for filters that expand + tally data along a spatial axis (x, y, or z) within a bounded region. + Subclasses must implement the order setter to define their specific + bin structure. + Parameters ---------- order : int - Maximum Legendre polynomial order + Maximum expansion order axis : {'x', 'y', 'z'} Axis along which to take the expansion minimum : float @@ -160,7 +161,7 @@ class SpatialLegendreFilter(ExpansionFilter): Attributes ---------- order : int - Maximum Legendre polynomial order + Maximum expansion order axis : {'x', 'y', 'z'} Axis along which to take the expansion minimum : float @@ -171,7 +172,7 @@ class SpatialLegendreFilter(ExpansionFilter): Unique identifier for the filter num_bins : int The number of filter bins - + """ def __init__(self, order, axis, minimum, maximum, filter_id=None): @@ -197,11 +198,6 @@ def __repr__(self): string += '{: <16}=\t{}\n'.format('\tID', self.id) return string - @ExpansionFilter.order.setter - def order(self, order): - ExpansionFilter.order.__set__(self, order) - self.bins = [f'P{i}' for i in range(order + 1)] - @property def axis(self): return self._axis @@ -229,27 +225,13 @@ def maximum(self, maximum): cv.check_type('maximum', maximum, Real) self._maximum = maximum - @classmethod - def from_hdf5(cls, group, **kwargs): - if group['type'][()].decode() != cls.short_name.lower(): - raise ValueError("Expected HDF5 data for filter type '" - + cls.short_name.lower() + "' but got '" - + group['type'][()].decode() + " instead") - - filter_id = int(group.name.split('/')[-1].lstrip('filter ')) - order = group['order'][()] - axis = group['axis'][()].decode() - min_, max_ = group['min'][()], group['max'][()] - - return cls(order, axis, min_, max_, filter_id) - def to_xml_element(self): """Return XML Element representing the filter. Returns ------- element : lxml.etree._Element - XML element containing Legendre filter data + XML element containing spatial expansion filter data """ element = super().to_xml_element() @@ -259,7 +241,6 @@ def to_xml_element(self): subelement.text = str(self.minimum) subelement = ET.SubElement(element, 'max') subelement.text = str(self.maximum) - return element @classmethod @@ -271,6 +252,107 @@ def from_xml_element(cls, elem, **kwargs): maximum = float(get_text(elem, "max")) return cls(order, axis, minimum, maximum, filter_id=filter_id) + @classmethod + def from_hdf5(cls, group, **kwargs): + if group['type'][()].decode() != cls.short_name.lower(): + raise ValueError("Expected HDF5 data for filter type '" + + cls.short_name.lower() + "' but got '" + + group['type'][()].decode() + " instead") + + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + order = group['order'][()] + axis = group['axis'][()].decode() + min_, max_ = group['min'][()], group['max'][()] + + return cls(order, axis, min_, max_, filter_id) + + + +class SpatialFourierFilter(ExpansionFilter): + r"""Score Fourier expansion moments in space up to specified order. + + This filter allows scores to be multiplied by Fourier basis functions of + the particle's position along a particular axis, normalized to a given + range, up to a user-specified order. + + Parameters + ---------- + order : int + Maximum Fourier expansion order + axis : {'x', 'y', 'z'} + Axis along which to take the expansion + minimum : float + Minimum value along selected axis + maximum : float + Maximum value along selected axis + filter_id : int or None + Unique identifier for the filter + + Attributes + ---------- + order : int + Maximum Fourier expansion order + axis : {'x', 'y', 'z'} + Axis along which to take the expansion + minimum : float + Minimum value along selected axis + maximum : float + Maximum value along selected axis + id : int + Unique identifier for the filter + num_bins : int + The number of filter bins (2*order + 1) + + """ + + @ExpansionFilter.order.setter + def order(self, order): + ExpansionFilter.order.__set__(self, order) + self.bins = [f'F{i}' for i in range(2 * order + 1)] + + +class SpatialLegendreFilter(ExpansionFilter): + r"""Score Legendre expansion moments in space up to specified order. + + This filter allows scores to be multiplied by Legendre polynomials of the + the particle's position along a particular axis, normalized to a given + range, up to a user-specified order. + + Parameters + ---------- + order : int + Maximum Legendre polynomial order + axis : {'x', 'y', 'z'} + Axis along which to take the expansion + minimum : float + Minimum value along selected axis + maximum : float + Maximum value along selected axis + filter_id : int or None + Unique identifier for the filter + + Attributes + ---------- + order : int + Maximum Legendre polynomial order + axis : {'x', 'y', 'z'} + Axis along which to take the expansion + minimum : float + Minimum value along selected axis + maximum : float + Maximum value along selected axis + id : int + Unique identifier for the filter + num_bins : int + The number of filter bins + + """ + + @ExpansionFilter.order.setter + def order(self, order): + ExpansionFilter.order.__set__(self, order) + self.bins = [f'P{i}' for i in range(order + 1)] + class SphericalHarmonicsFilter(ExpansionFilter): r"""Score spherical harmonic expansion moments up to specified order. From 1c56e0a9be5b27da4f5595934b1b6af3c82409f4 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Tue, 17 Mar 2026 12:08:41 -0600 Subject: [PATCH 02/21] SpatialFourierFilter header Copy-paste SpatialLegendreFilter. No other changes needed. Could be combined to a SpatialExpansionFilter header? --- include/openmc/tallies/filter_sptl_fourier.h | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 include/openmc/tallies/filter_sptl_fourier.h diff --git a/include/openmc/tallies/filter_sptl_fourier.h b/include/openmc/tallies/filter_sptl_fourier.h new file mode 100644 index 00000000000..b6c380e9b8d --- /dev/null +++ b/include/openmc/tallies/filter_sptl_fourier.h @@ -0,0 +1,68 @@ +#ifndef OPENMC_TALLIES_FILTER_SPTL_LEGENDRE_H +#define OPENMC_TALLIES_FILTER_SPTL_LEGENDRE_H + +#include + +#include "openmc/tallies/filter.h" + +namespace openmc { + +enum class LegendreAxis { x, y, z }; + +//============================================================================== +//! Gives Legendre moments of the particle's normalized position along an axis +//============================================================================== + +class SpatialLegendreFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~SpatialLegendreFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "spatiallegendre"; } + FilterType type() const override { return FilterType::SPATIAL_LEGENDRE; } + + void from_xml(pugi::xml_node node) override; + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + void to_statepoint(hid_t filter_group) const override; + + std::string text_label(int bin) const override; + + //---------------------------------------------------------------------------- + // Accessors + + int order() const { return order_; } + void set_order(int order); + + LegendreAxis axis() const { return axis_; } + void set_axis(LegendreAxis axis); + + double min() const { return min_; } + double max() const { return max_; } + void set_minmax(double min, double max); + +private: + //---------------------------------------------------------------------------- + // Data members + + int order_; + + //! The Cartesian coordinate axis that the Legendre expansion is applied to. + LegendreAxis axis_; + + //! The minimum coordinate along the reference axis that the expansion covers. + double min_; + + //! The maximum coordinate along the reference axis that the expansion covers. + double max_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_SPTL_LEGENDRE_H From cd23c7347fc3424ae19f775fee95d58ebf7935cf Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Tue, 17 Mar 2026 12:10:09 -0600 Subject: [PATCH 03/21] openmc.lib.SpatialFourierFilter boilerplate --- include/openmc/tallies/filter_sptl_fourier.h | 26 ++++++++++---------- openmc/lib/filter.py | 21 +++++++++++++++- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/include/openmc/tallies/filter_sptl_fourier.h b/include/openmc/tallies/filter_sptl_fourier.h index b6c380e9b8d..7c8baf8834e 100644 --- a/include/openmc/tallies/filter_sptl_fourier.h +++ b/include/openmc/tallies/filter_sptl_fourier.h @@ -1,5 +1,5 @@ -#ifndef OPENMC_TALLIES_FILTER_SPTL_LEGENDRE_H -#define OPENMC_TALLIES_FILTER_SPTL_LEGENDRE_H +#ifndef OPENMC_TALLIES_FILTER_SPTL_FOURIER_H +#define OPENMC_TALLIES_FILTER_SPTL_FOURIER_H #include @@ -7,24 +7,24 @@ namespace openmc { -enum class LegendreAxis { x, y, z }; +enum class FourierAxis { x, y, z }; //============================================================================== -//! Gives Legendre moments of the particle's normalized position along an axis +//! Gives Fourier moments of the particle's normalized position along an axis //============================================================================== -class SpatialLegendreFilter : public Filter { +class SpatialFourierFilter : public Filter { public: //---------------------------------------------------------------------------- // Constructors, destructors - ~SpatialLegendreFilter() = default; + ~SpatialFourierFilter() = default; //---------------------------------------------------------------------------- // Methods - std::string type_str() const override { return "spatiallegendre"; } - FilterType type() const override { return FilterType::SPATIAL_LEGENDRE; } + std::string type_str() const override { return "spatialfourier"; } + FilterType type() const override { return FilterType::SPATIAL_FOURIER; } void from_xml(pugi::xml_node node) override; @@ -41,8 +41,8 @@ class SpatialLegendreFilter : public Filter { int order() const { return order_; } void set_order(int order); - LegendreAxis axis() const { return axis_; } - void set_axis(LegendreAxis axis); + FourierAxis axis() const { return axis_; } + void set_axis(FourierAxis axis); double min() const { return min_; } double max() const { return max_; } @@ -54,8 +54,8 @@ class SpatialLegendreFilter : public Filter { int order_; - //! The Cartesian coordinate axis that the Legendre expansion is applied to. - LegendreAxis axis_; + //! The Cartesian coordinate axis that the Fourier expansion is applied to. + FourierAxis axis_; //! The minimum coordinate along the reference axis that the expansion covers. double min_; @@ -65,4 +65,4 @@ class SpatialLegendreFilter : public Filter { }; } // namespace openmc -#endif // OPENMC_TALLIES_FILTER_SPTL_LEGENDRE_H +#endif // OPENMC_TALLIES_FILTER_SPTL_FOURIER_H diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 574a37443ae..175caa5a333 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -23,7 +23,7 @@ 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', 'MeshMaterialFilter', 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParentNuclideFilter', 'ParticleFilter', 'ParticleProductionFilter', 'PolarFilter', - 'ReactionFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', + 'ReactionFilter', 'SphericalHarmonicsFilter', 'SpatialFourierFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', 'WeightFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] @@ -639,6 +639,25 @@ def order(self, order): _dll.openmc_sphharm_filter_set_order(self._index, order) +class SpatialFourierFilter(Filter): + filter_type = 'spatialfourier' + + def __init__(self, order=None, uid=None, new=True, index=None): + super().__init__(uid, new, index) + if order is not None: + self.order = order + + @property + def order(self): + temp_order = c_int() + _dll.openmc_spatial_fourier_filter_get_order(self._index, temp_order) + return temp_order.value + + @order.setter + def order(self, order): + _dll.openmc_spatial_fourier_filter_set_order(self._index, order) + + class SpatialLegendreFilter(Filter): filter_type = 'spatiallegendre' From 34c04a6dcc325c72f336b451b4417fd4e54f84e7 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Tue, 17 Mar 2026 12:17:42 -0600 Subject: [PATCH 04/21] Start SpatialFourier C++ --- src/tallies/filter_sptl_fourier.cpp | 205 ++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/tallies/filter_sptl_fourier.cpp diff --git a/src/tallies/filter_sptl_fourier.cpp b/src/tallies/filter_sptl_fourier.cpp new file mode 100644 index 00000000000..a13aa905b92 --- /dev/null +++ b/src/tallies/filter_sptl_fourier.cpp @@ -0,0 +1,205 @@ +#include "openmc/tallies/filter_sptl_fourier.h" + +#include // For pair + +#include + +#include "openmc/capi.h" +#include "openmc/error.h" +#include "openmc/math_functions.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +void SpatialFourierFilter::from_xml(pugi::xml_node node) +{ + this->set_order(std::stoi(get_node_value(node, "order"))); + + auto axis = get_node_value(node, "axis"); + switch (axis[0]) { + case 'x': + this->set_axis(FourierAxis::x); + break; + case 'y': + this->set_axis(FourierAxis::y); + break; + case 'z': + this->set_axis(FourierAxis::z); + break; + default: + throw std::runtime_error { + "Axis for SpatialFourierFilter must be 'x', 'y', or 'z'"}; + } + + double min = std::stod(get_node_value(node, "min")); + double max = std::stod(get_node_value(node, "max")); + this->set_minmax(min, max); +} + +void SpatialFourierFilter::set_order(int order) +{ + if (order < 0) { + throw std::invalid_argument {"Fourier order must be non-negative."}; + } + order_ = order; + n_bins_ = order_ + 1; +} + +void SpatialFourierFilter::set_axis(FourierAxis axis) +{ + axis_ = axis; +} + +void SpatialFourierFilter::set_minmax(double min, double max) +{ + if (max <= min) { + throw std::invalid_argument { + "Maximum value must be greater than minimum value"}; + } + min_ = min; + max_ = max; +} + +void SpatialFourierFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get the coordinate along the axis of interest. + double x; + if (axis_ == FourierAxis::x) { + x = p.r().x; + } else if (axis_ == FourierAxis::y) { + x = p.r().y; + } else { + x = p.r().z; + } + + if (x >= min_ && x <= max_) { + // Compute the normalized coordinate value. + double x_norm = 2.0 * (x - min_) / (max_ - min_) - 1.0; + + // Compute and return the Fourier weights. + vector wgt(order_ + 1); + calc_pn_c(order_, x_norm, wgt.data()); + for (int i = 0; i < order_ + 1; i++) { + match.bins_.push_back(i); + match.weights_.push_back(wgt[i]); + } + } +} + +void SpatialFourierFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + write_dataset(filter_group, "order", order_); + if (axis_ == FourierAxis::x) { + write_dataset(filter_group, "axis", "x"); + } else if (axis_ == FourierAxis::y) { + write_dataset(filter_group, "axis", "y"); + } else { + write_dataset(filter_group, "axis", "z"); + } + write_dataset(filter_group, "min", min_); + write_dataset(filter_group, "max", max_); +} + +std::string SpatialFourierFilter::text_label(int bin) const +{ + if (axis_ == FourierAxis::x) { + return fmt::format("Fourier expansion, x axis, P{}", bin); + } else if (axis_ == FourierAxis::y) { + return fmt::format("Fourier expansion, y axis, P{}", bin); + } else { + return fmt::format("Fourier expansion, z axis, P{}", bin); + } +} + +//============================================================================== +// C-API functions +//============================================================================== + +std::pair check_sptl_fourier_filter(int32_t index) +{ + // Make sure this is a valid index to an allocated filter. + int err = verify_filter(index); + if (err) { + return {err, nullptr}; + } + + // Get a pointer to the filter and downcast. + const auto& filt_base = model::tally_filters[index].get(); + auto* filt = dynamic_cast(filt_base); + + // Check the filter type. + if (!filt) { + set_errmsg("Not a spatial Fourier filter."); + err = OPENMC_E_INVALID_TYPE; + } + return {err, filt}; +} + +extern "C" int openmc_spatial_fourier_filter_get_order( + int32_t index, int* order) +{ + // Check the filter. + auto check_result = check_sptl_fourier_filter(index); + auto err = check_result.first; + auto filt = check_result.second; + if (err) + return err; + + // Output the order. + *order = filt->order(); + return 0; +} + +extern "C" int openmc_spatial_fourier_filter_get_params( + int32_t index, int* axis, double* min, double* max) +{ + // Check the filter. + auto check_result = check_sptl_fourier_filter(index); + auto err = check_result.first; + auto filt = check_result.second; + if (err) + return err; + + // Output the params. + *axis = static_cast(filt->axis()); + *min = filt->min(); + *max = filt->max(); + return 0; +} + +extern "C" int openmc_spatial_fourier_filter_set_order( + int32_t index, int order) +{ + // Check the filter. + auto check_result = check_sptl_fourier_filter(index); + auto err = check_result.first; + auto filt = check_result.second; + if (err) + return err; + + // Update the filter. + filt->set_order(order); + return 0; +} + +extern "C" int openmc_spatial_fourier_filter_set_params( + int32_t index, const int* axis, const double* min, const double* max) +{ + // Check the filter. + auto check_result = check_sptl_fourier_filter(index); + auto err = check_result.first; + auto filt = check_result.second; + if (err) + return err; + + // Update the filter. + if (axis) + filt->set_axis(static_cast(*axis)); + if (min && max) + filt->set_minmax(*min, *max); + return 0; +} + +} // namespace openmc From 0fc321c0b9aaf58d017fc9ba77e9393ec9e3dd42 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Tue, 17 Mar 2026 14:33:30 -0600 Subject: [PATCH 05/21] Implement SpatialFourierFilter --- src/tallies/filter_sptl_fourier.cpp | 38 +++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/tallies/filter_sptl_fourier.cpp b/src/tallies/filter_sptl_fourier.cpp index a13aa905b92..3a1382379f2 100644 --- a/src/tallies/filter_sptl_fourier.cpp +++ b/src/tallies/filter_sptl_fourier.cpp @@ -5,8 +5,8 @@ #include #include "openmc/capi.h" +#include "openmc/constants.h" #include "openmc/error.h" -#include "openmc/math_functions.h" #include "openmc/xml_interface.h" namespace openmc { @@ -42,7 +42,7 @@ void SpatialFourierFilter::set_order(int order) throw std::invalid_argument {"Fourier order must be non-negative."}; } order_ = order; - n_bins_ = order_ + 1; + n_bins_ = 2 * order_ + 1; } void SpatialFourierFilter::set_axis(FourierAxis axis) @@ -74,13 +74,18 @@ void SpatialFourierFilter::get_all_bins( } if (x >= min_ && x <= max_) { - // Compute the normalized coordinate value. - double x_norm = 2.0 * (x - min_) / (max_ - min_) - 1.0; + // Compute the normalized coordinate value on [0, 1] + double x_norm = (x - min_) / (max_ - min_); // Compute and return the Fourier weights. - vector wgt(order_ + 1); - calc_pn_c(order_, x_norm, wgt.data()); - for (int i = 0; i < order_ + 1; i++) { + vector wgt(n_bins_); + wgt[0] = 1.0; // a_0: constant term + for (int n = 1; n <= order_; ++n) { + double arg = 2.0 * PI * n * x_norm; + wgt[2*n - 1] = std::cos(arg); + wgt[2*n] = std::sin(arg); + } + for (int i = 0; i < n_bins_; ++i) { match.bins_.push_back(i); match.weights_.push_back(wgt[i]); } @@ -104,13 +109,26 @@ void SpatialFourierFilter::to_statepoint(hid_t filter_group) const std::string SpatialFourierFilter::text_label(int bin) const { + std::string axis_str; + std::string func_str; if (axis_ == FourierAxis::x) { - return fmt::format("Fourier expansion, x axis, P{}", bin); + axis_str = "x"; } else if (axis_ == FourierAxis::y) { - return fmt::format("Fourier expansion, y axis, P{}", bin); + axis_str = "y"; + } else { + axis_str = "z"; + } + + if (bin == 0) { + func_str = "a0 (constant)"; + } else if (bin % 2 == 1) { + int n = (bin + 1) / 2; + func_str = fmt::format("a{} (cos)", n); } else { - return fmt::format("Fourier expansion, z axis, P{}", bin); + int n = bin / 2; + func_str = fmt::format("b{} (sin)", n); } + return fmt::format("Fourier expansion, {} axis, {}", axis_str, func_str); } //============================================================================== From 27d824c2b4bbe6668acdc679085b200f30cb26ff Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Tue, 17 Mar 2026 15:02:15 -0600 Subject: [PATCH 06/21] clang-format --- src/tallies/filter_sptl_fourier.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tallies/filter_sptl_fourier.cpp b/src/tallies/filter_sptl_fourier.cpp index 3a1382379f2..ce603ffa365 100644 --- a/src/tallies/filter_sptl_fourier.cpp +++ b/src/tallies/filter_sptl_fourier.cpp @@ -79,11 +79,11 @@ void SpatialFourierFilter::get_all_bins( // Compute and return the Fourier weights. vector wgt(n_bins_); - wgt[0] = 1.0; // a_0: constant term + wgt[0] = 1.0; // a_0: constant term for (int n = 1; n <= order_; ++n) { double arg = 2.0 * PI * n * x_norm; - wgt[2*n - 1] = std::cos(arg); - wgt[2*n] = std::sin(arg); + wgt[2 * n - 1] = std::cos(arg); + wgt[2 * n] = std::sin(arg); } for (int i = 0; i < n_bins_; ++i) { match.bins_.push_back(i); @@ -187,8 +187,7 @@ extern "C" int openmc_spatial_fourier_filter_get_params( return 0; } -extern "C" int openmc_spatial_fourier_filter_set_order( - int32_t index, int order) +extern "C" int openmc_spatial_fourier_filter_set_order(int32_t index, int order) { // Check the filter. auto check_result = check_sptl_fourier_filter(index); From cbd7f78cf07517b1dcb61675beb0df5c5392f245 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 08:10:32 -0600 Subject: [PATCH 07/21] Fourier filter bin labels --- openmc/filter_expansion.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index 90fbc5ec282..d19207c27f6 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -308,7 +308,12 @@ class SpatialFourierFilter(ExpansionFilter): @ExpansionFilter.order.setter def order(self, order): ExpansionFilter.order.__set__(self, order) - self.bins = [f'F{i}' for i in range(2 * order + 1)] + self.bins = ['a0 (constant)'] + [None]*2*order + for i in range(1, order + 1): + a = 2*i - 1 + b = 2*i + self.bins[a] = f'a{i} (cos)' + self.bins[b] = f'b{i} (sin)' class SpatialLegendreFilter(ExpansionFilter): From 3b8d77b4cba6f8d07f0acf880a146683913a36e2 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 08:11:18 -0600 Subject: [PATCH 08/21] Unit test SpatialFourierFilter --- tests/unit_tests/test_filters.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 2e7481c8767..d16f328f239 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -125,6 +125,36 @@ def test_spatial_legendre(): assert new_f.axis == f.axis +def test_spatial_fourier(): + n = 5 + axis = 'x' + f = openmc.SpatialFourierFilter(n, axis, -10., 10.) + assert f.order == n + assert f.axis == axis + assert f.minimum == -10. + assert f.maximum == 10. + assert f.bins[0] == 'a0 (constant)' + assert f.bins[-1] == 'b5 (sin)' + assert f.bins[-2] == 'a5 (cos)' + assert len(f.bins) == 2*n + 1 + + # Make sure __repr__ works + repr(f) + + # to_xml_element() + elem = f.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'spatialfourier' + assert elem.find('order').text == str(n) + assert elem.find('axis').text == str(axis) + + # from_xml_element() + new_f = openmc.Filter.from_xml_element(elem) + assert new_f.id == f.id + assert new_f.order == f.order + assert new_f.axis == f.axis + + def test_spherical_harmonics(): n = 3 f = openmc.SphericalHarmonicsFilter(n) From 857f8baef9d82cf5f37d2fd1f87fe35397c7e7ec Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 10:10:56 -0600 Subject: [PATCH 09/21] Need to inherit from SpatialExpansionFilter --- openmc/filter_expansion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index d19207c27f6..df30d4b778b 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -268,7 +268,7 @@ def from_hdf5(cls, group, **kwargs): -class SpatialFourierFilter(ExpansionFilter): +class SpatialFourierFilter(SpatialExpansionFilter): r"""Score Fourier expansion moments in space up to specified order. This filter allows scores to be multiplied by Fourier basis functions of @@ -316,7 +316,7 @@ def order(self, order): self.bins[b] = f'b{i} (sin)' -class SpatialLegendreFilter(ExpansionFilter): +class SpatialLegendreFilter(SpatialExpansionFilter): r"""Score Legendre expansion moments in space up to specified order. This filter allows scores to be multiplied by Legendre polynomials of the From b2f4df537ee80a15b6c238ca3eb7b5b0f02347a2 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 10:31:24 -0600 Subject: [PATCH 10/21] Add SpatialLegendre and SpatialFourier to regression test --- .../regression_tests/tallies/inputs_true.dat | 78 ++++++++++++------- tests/regression_tests/tallies/test.py | 17 +++- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/tests/regression_tests/tallies/inputs_true.dat b/tests/regression_tests/tallies/inputs_true.dat index 40829f865a1..89dcc270955 100644 --- a/tests/regression_tests/tallies/inputs_true.dat +++ b/tests/regression_tests/tallies/inputs_true.dat @@ -342,19 +342,31 @@ 4 - + 4 + x + -182.07 + 182.07 - + + 5 + x + -182.07 + 182.07 + + + 4 + + 1 2 3 4 6 8 - + 1 2 5 3 6 - + 10 21 22 23 60 - + 21 22 23 27 28 29 60 @@ -394,109 +406,119 @@ scatter nu-fission - 7 + 5 total - 8 + 7 scatter nu-scatter - 8 2 + 7 2 scatter nu-scatter - 9 + 8 flux tracklength - 9 + 8 flux analog - 9 2 + 8 2 flux tracklength - 10 + 9 scatter nu-scatter analog - 11 - scatter nu-scatter flux total + 10 + flux analog 11 + flux + analog + + + 12 + scatter nu-scatter flux total + analog + + + 12 flux total collision - - 11 + + 12 flux total tracklength - + 12 total - + 15 scatter - + 13 absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate tracklength - + 13 U235 O16 total absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate tracklength - + 13 absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate analog - + 13 U235 O16 total absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate analog - + 13 absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate collision - + 13 U235 O16 total absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate collision - + 14 flux tracklength - + 14 flux analog - + 14 flux collision - + H1-production H2-production H3-production He3-production He4-production heating damage-energy diff --git a/tests/regression_tests/tallies/test.py b/tests/regression_tests/tallies/test.py index d20067ed33f..936339e9276 100644 --- a/tests/regression_tests/tallies/test.py +++ b/tests/regression_tests/tallies/test.py @@ -105,6 +105,20 @@ def test_tallies(): legendre_tally.scores = ['scatter', 'nu-scatter'] legendre_tally.estimator = 'analog' + spatial_legendre_filter = SpatialLegendreFilter( + order=4, axis='x', minimum=-182.07, maximum=182.07) + spatial_legendre_tally = Tally() + spatial_legendre_tally.filters = [spatial_legendre_filter] + spatial_legendre_tally.scores = ['flux'] + spatial_legendre_tally.estimator = 'analog' + + spatial_fourier_filter = SpatialFourierFilter( + order=5, axis='x', minimum=-182.07, maximum=182.07) + spatial_fourier_tally = Tally() + spatial_fourier_tally.filters = [spatial_fourier_filter] + spatial_fourier_tally.scores = ['flux'] + spatial_fourier_tally.estimator = 'analog' + harmonics_filter = SphericalHarmonicsFilter(order=4) harmonics_tally = Tally() harmonics_tally.filters = [harmonics_filter] @@ -168,7 +182,8 @@ def test_tallies(): azimuthal_tally1, azimuthal_tally2, azimuthal_tally3, cellborn_tally, dg_tally, energy_tally, energyout_tally, transfer_tally, material_tally, mu_tally1, mu_tally2, - polar_tally1, polar_tally2, polar_tally3, legendre_tally, + polar_tally1, polar_tally2, polar_tally3, + legendre_tally, spatial_legendre_tally, spatial_fourier_tally, harmonics_tally, harmonics_tally2, harmonics_tally3, universe_tally, collision_tally] model.tallies += score_tallies From 2f21b7f74f4fdfa6fcc785599f418bb7ac49b683 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 11:22:49 -0600 Subject: [PATCH 11/21] Clean up spacing --- openmc/filter_expansion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index df30d4b778b..23fad8dfa11 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -144,7 +144,7 @@ class SpatialExpansionFilter(ExpansionFilter): tally data along a spatial axis (x, y, or z) within a bounded region. Subclasses must implement the order setter to define their specific bin structure. - + Parameters ---------- order : int @@ -172,7 +172,7 @@ class SpatialExpansionFilter(ExpansionFilter): Unique identifier for the filter num_bins : int The number of filter bins - + """ def __init__(self, order, axis, minimum, maximum, filter_id=None): @@ -267,7 +267,6 @@ def from_hdf5(cls, group, **kwargs): return cls(order, axis, min_, max_, filter_id) - class SpatialFourierFilter(SpatialExpansionFilter): r"""Score Fourier expansion moments in space up to specified order. From c99df6e2df4e493f11926322890285edca66c84d Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 11:38:21 -0600 Subject: [PATCH 12/21] Register "spatialfourier" --- src/tallies/filter.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index badb9107733..956925754d7 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -35,6 +35,7 @@ #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_reaction.h" #include "openmc/tallies/filter_sph_harm.h" +#include "openmc/tallies/filter_sptl_fourier.h" #include "openmc/tallies/filter_sptl_legendre.h" #include "openmc/tallies/filter_surface.h" #include "openmc/tallies/filter_time.h" @@ -156,6 +157,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "surface") { return Filter::create(id); + } else if (type == "spatialfourier") { + return Filter::create(id); } else if (type == "spatiallegendre") { return Filter::create(id); } else if (type == "sphericalharmonics") { From 4ba558dcff0ec1c4f9a3eb0fbf8a519e58d273e4 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 11:45:45 -0600 Subject: [PATCH 13/21] Add to a few more locations --- include/openmc/capi.h | 6 ++++++ include/openmc/tallies/filter.h | 1 + src/tallies/tally.cpp | 3 ++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 911654d318f..f72519fc22d 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -192,6 +192,12 @@ int openmc_set_n_batches( int openmc_simulation_finalize(); int openmc_simulation_init(); int openmc_source_bank(void** ptr, int64_t* n); +int openmc_spatial_fourier_filter_get_order(int32_t index, int* order); +int openmc_spatial_fourier_filter_get_params( + int32_t index, int* axis, double* min, double* max); +int openmc_spatial_fourier_filter_set_order(int32_t index, int order); +int openmc_spatial_fourier_filter_set_params( + int32_t index, const int* axis, const double* min, const double* max); int openmc_spatial_legendre_filter_get_order(int32_t index, int* order); int openmc_spatial_legendre_filter_get_params( int32_t index, int* axis, double* min, double* max); diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 77b0d9f420d..ab8db12da90 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -43,6 +43,7 @@ enum class FilterType { POLAR, REACTION, SPHERICAL_HARMONICS, + SPATIAL_FOURIER, SPATIAL_LEGENDRE, SURFACE, TIME, diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 3fe48c1b021..a315a4444de 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -166,7 +166,8 @@ Tally::Tally(pugi::xml_node node) if (sf->cosine() == SphericalHarmonicsCosine::scatter) { estimator_ = TallyEstimator::ANALOG; } - } else if (filt_type == FilterType::SPATIAL_LEGENDRE || + } else if (filt_type == FilterType::SPATIAL_FOURIER || + filt_type == FilterType::SPATIAL_LEGENDRE || filt_type == FilterType::ZERNIKE || filt_type == FilterType::ZERNIKE_RADIAL) { estimator_ = TallyEstimator::COLLISION; From de2cbb7fae69e398974aa494a9a506ad9908b862 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Thu, 19 Mar 2026 12:04:11 -0600 Subject: [PATCH 14/21] Add spatial fourier to CMakeLists.txt --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fe133a22e3..b5256590662 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -455,6 +455,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_polar.cpp src/tallies/filter_reaction.cpp src/tallies/filter_sph_harm.cpp + src/tallies/filter_sptl_fourier.cpp src/tallies/filter_sptl_legendre.cpp src/tallies/filter_surface.cpp src/tallies/filter_time.cpp From 0633eeaeddf313173631563c2770844af216fb5e Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Wed, 25 Mar 2026 11:12:41 -0600 Subject: [PATCH 15/21] Unbotch inputs_true.dat --- .../regression_tests/tallies/inputs_true.dat | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/regression_tests/tallies/inputs_true.dat b/tests/regression_tests/tallies/inputs_true.dat index 89dcc270955..463f1d2f5e0 100644 --- a/tests/regression_tests/tallies/inputs_true.dat +++ b/tests/regression_tests/tallies/inputs_true.dat @@ -406,115 +406,115 @@ scatter nu-fission - 5 + 7 total - 7 + 8 scatter nu-scatter - 7 2 + 8 2 scatter nu-scatter - 8 + 9 flux tracklength - 8 + 9 flux analog - 8 2 + 9 2 flux tracklength - 9 + 10 scatter nu-scatter analog - 10 + 11 flux analog - 11 + 12 flux analog - 12 + 13 scatter nu-scatter flux total analog - 12 + 13 flux total collision - 12 + 13 flux total tracklength - 12 + 14 total - 15 + 17 scatter - 13 + 15 absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate tracklength - 13 + 15 U235 O16 total absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate tracklength - 13 + 15 absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate analog - 13 + 15 U235 O16 total absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate analog - 13 + 15 absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate collision - 13 + 15 U235 O16 total absorption delayed-nu-fission events fission inverse-velocity kappa-fission (n,2n) (n,n1) (n,gamma) nu-fission scatter elastic total prompt-nu-fission fission-q-prompt fission-q-recoverable decay-rate collision - 14 + 16 flux tracklength - 14 + 16 flux analog - 14 + 16 flux collision From df6ec06d6974ba5a01caefdf31b7b1429b9f69c8 Mon Sep 17 00:00:00 2001 From: "Labossiere-Hickman, Travis James" Date: Wed, 25 Mar 2026 12:03:15 -0600 Subject: [PATCH 16/21] pytest update --- tests/regression_tests/tallies/results_true.dat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression_tests/tallies/results_true.dat b/tests/regression_tests/tallies/results_true.dat index 1d3aca4b079..bf15a5303c7 100644 --- a/tests/regression_tests/tallies/results_true.dat +++ b/tests/regression_tests/tallies/results_true.dat @@ -1 +1 @@ -d01c3accd5b4de2aa166a77df28cfe42f5738a44c2480752fcfae7564a507362fff006b6dffb7b1dfe248e14bacef0070cabacea5d75c5996653e5605f7c7384 \ No newline at end of file +7cb0047ce4c66c9dfc563dafc95c88781fbfc7135a41ce22ea5028cfe0b1853d949e87d9b1698309dea8562495dd16334e1ea8b32f12524259fdd2d9219aff97 \ No newline at end of file From 57673b52e37a2e404abf882e28035954850a0917 Mon Sep 17 00:00:00 2001 From: "Travis J. Labossiere-Hickman" Date: Thu, 11 Jun 2026 14:50:40 -0600 Subject: [PATCH 17/21] Register spatialfourier in _FILTER_TYPE_MAP --- openmc/lib/filter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 175caa5a333..4f296f9c5cd 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -743,6 +743,7 @@ class ZernikeRadialFilter(ZernikeFilter): 'polar': PolarFilter, 'reaction': ReactionFilter, 'sphericalharmonics': SphericalHarmonicsFilter, + 'spatialfourier': SpatialFourierFilter, 'spatiallegendre': SpatialLegendreFilter, 'surface': SurfaceFilter, 'time': TimeFilter, From 05f5d2249509b3b33898ec5a8cfa81bb649642a0 Mon Sep 17 00:00:00 2001 From: "Travis J. Labossiere-Hickman" Date: Thu, 11 Jun 2026 14:52:02 -0600 Subject: [PATCH 18/21] Include SpatialFourier in docs/source/pythonapi --- docs/source/pythonapi/base.rst | 1 + docs/source/pythonapi/capi.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index b3911c8b3ba..a9a2dc88b5c 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -142,6 +142,7 @@ Constructing Tallies openmc.EnergyFunctionFilter openmc.LegendreFilter openmc.SpatialLegendreFilter + openmc.SpatialFourierFilter openmc.SphericalHarmonicsFilter openmc.TimeFilter openmc.WeightFilter diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index dab45481dd9..a51796121c9 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -87,6 +87,7 @@ Classes ReactionFilter RectilinearMesh RegularMesh + SpatialFourierFilter SpatialLegendreFilter SphericalHarmonicsFilter SphericalMesh From c198b23c8f63cf7b24f80a2e316cf987fb95cfd1 Mon Sep 17 00:00:00 2001 From: "Travis J. Labossiere-Hickman" Date: Thu, 11 Jun 2026 15:01:58 -0600 Subject: [PATCH 19/21] Implement SpatialExpansionFilter in C++ too --- CMakeLists.txt | 1 + .../openmc/tallies/filter_sptl_expansion.h | 70 ++++++++++++++++ include/openmc/tallies/filter_sptl_fourier.h | 35 +------- include/openmc/tallies/filter_sptl_legendre.h | 35 +------- src/tallies/filter_sptl_expansion.cpp | 78 ++++++++++++++++++ src/tallies/filter_sptl_fourier.cpp | 79 +------------------ src/tallies/filter_sptl_legendre.cpp | 76 +----------------- 7 files changed, 163 insertions(+), 211 deletions(-) create mode 100644 include/openmc/tallies/filter_sptl_expansion.h create mode 100644 src/tallies/filter_sptl_expansion.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b5256590662..75c0224b106 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -455,6 +455,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_polar.cpp src/tallies/filter_reaction.cpp src/tallies/filter_sph_harm.cpp + src/tallies/filter_sptl_expansion.cpp src/tallies/filter_sptl_fourier.cpp src/tallies/filter_sptl_legendre.cpp src/tallies/filter_surface.cpp diff --git a/include/openmc/tallies/filter_sptl_expansion.h b/include/openmc/tallies/filter_sptl_expansion.h new file mode 100644 index 00000000000..03e7f78ebc1 --- /dev/null +++ b/include/openmc/tallies/filter_sptl_expansion.h @@ -0,0 +1,70 @@ +#ifndef OPENMC_TALLIES_FILTER_SPTL_EXPANSION_H +#define OPENMC_TALLIES_FILTER_SPTL_EXPANSION_H + +#include "openmc/tallies/filter.h" + +namespace openmc { + +//============================================================================== +//! Abstract filter for functional expansions of the particle's normalized +//! position along a Cartesian axis +//============================================================================== + +class SpatialExpansionFilter : public Filter { +public: + enum class Axis { x, y, z }; + + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~SpatialExpansionFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + void from_xml(pugi::xml_node node) override; + + void to_statepoint(hid_t filter_group) const override; + + //---------------------------------------------------------------------------- + // Accessors + + int order() const { return order_; } + + //! Set the expansion order and the corresponding number of bins + virtual void set_order(int order) = 0; + + Axis axis() const { return axis_; } + void set_axis(Axis axis); + + double min() const { return min_; } + double max() const { return max_; } + void set_minmax(double min, double max); + +protected: + //---------------------------------------------------------------------------- + // Methods + + //! Get the particle's coordinate along the expansion axis + double position(const Particle& p) const; + + //! Get the name of the expansion axis ("x", "y", or "z") + const char* axis_label() const; + + //---------------------------------------------------------------------------- + // Data members + + int order_; + + //! The Cartesian coordinate axis that the expansion is applied to. + Axis axis_; + + //! The minimum coordinate along the reference axis that the expansion covers. + double min_; + + //! The maximum coordinate along the reference axis that the expansion covers. + double max_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_SPTL_EXPANSION_H diff --git a/include/openmc/tallies/filter_sptl_fourier.h b/include/openmc/tallies/filter_sptl_fourier.h index 7c8baf8834e..164799d00c3 100644 --- a/include/openmc/tallies/filter_sptl_fourier.h +++ b/include/openmc/tallies/filter_sptl_fourier.h @@ -3,17 +3,15 @@ #include -#include "openmc/tallies/filter.h" +#include "openmc/tallies/filter_sptl_expansion.h" namespace openmc { -enum class FourierAxis { x, y, z }; - //============================================================================== //! Gives Fourier moments of the particle's normalized position along an axis //============================================================================== -class SpatialFourierFilter : public Filter { +class SpatialFourierFilter : public SpatialExpansionFilter { public: //---------------------------------------------------------------------------- // Constructors, destructors @@ -26,42 +24,15 @@ class SpatialFourierFilter : public Filter { std::string type_str() const override { return "spatialfourier"; } FilterType type() const override { return FilterType::SPATIAL_FOURIER; } - void from_xml(pugi::xml_node node) override; - void get_all_bins(const Particle& p, TallyEstimator estimator, FilterMatch& match) const override; - void to_statepoint(hid_t filter_group) const override; - std::string text_label(int bin) const override; //---------------------------------------------------------------------------- // Accessors - int order() const { return order_; } - void set_order(int order); - - FourierAxis axis() const { return axis_; } - void set_axis(FourierAxis axis); - - double min() const { return min_; } - double max() const { return max_; } - void set_minmax(double min, double max); - -private: - //---------------------------------------------------------------------------- - // Data members - - int order_; - - //! The Cartesian coordinate axis that the Fourier expansion is applied to. - FourierAxis axis_; - - //! The minimum coordinate along the reference axis that the expansion covers. - double min_; - - //! The maximum coordinate along the reference axis that the expansion covers. - double max_; + void set_order(int order) override; }; } // namespace openmc diff --git a/include/openmc/tallies/filter_sptl_legendre.h b/include/openmc/tallies/filter_sptl_legendre.h index b6c380e9b8d..5b7f2fd5aff 100644 --- a/include/openmc/tallies/filter_sptl_legendre.h +++ b/include/openmc/tallies/filter_sptl_legendre.h @@ -3,17 +3,15 @@ #include -#include "openmc/tallies/filter.h" +#include "openmc/tallies/filter_sptl_expansion.h" namespace openmc { -enum class LegendreAxis { x, y, z }; - //============================================================================== //! Gives Legendre moments of the particle's normalized position along an axis //============================================================================== -class SpatialLegendreFilter : public Filter { +class SpatialLegendreFilter : public SpatialExpansionFilter { public: //---------------------------------------------------------------------------- // Constructors, destructors @@ -26,42 +24,15 @@ class SpatialLegendreFilter : public Filter { std::string type_str() const override { return "spatiallegendre"; } FilterType type() const override { return FilterType::SPATIAL_LEGENDRE; } - void from_xml(pugi::xml_node node) override; - void get_all_bins(const Particle& p, TallyEstimator estimator, FilterMatch& match) const override; - void to_statepoint(hid_t filter_group) const override; - std::string text_label(int bin) const override; //---------------------------------------------------------------------------- // Accessors - int order() const { return order_; } - void set_order(int order); - - LegendreAxis axis() const { return axis_; } - void set_axis(LegendreAxis axis); - - double min() const { return min_; } - double max() const { return max_; } - void set_minmax(double min, double max); - -private: - //---------------------------------------------------------------------------- - // Data members - - int order_; - - //! The Cartesian coordinate axis that the Legendre expansion is applied to. - LegendreAxis axis_; - - //! The minimum coordinate along the reference axis that the expansion covers. - double min_; - - //! The maximum coordinate along the reference axis that the expansion covers. - double max_; + void set_order(int order) override; }; } // namespace openmc diff --git a/src/tallies/filter_sptl_expansion.cpp b/src/tallies/filter_sptl_expansion.cpp new file mode 100644 index 00000000000..44d83ee3ef2 --- /dev/null +++ b/src/tallies/filter_sptl_expansion.cpp @@ -0,0 +1,78 @@ +#include "openmc/tallies/filter_sptl_expansion.h" + +#include "openmc/xml_interface.h" + +namespace openmc { + +void SpatialExpansionFilter::from_xml(pugi::xml_node node) +{ + this->set_order(std::stoi(get_node_value(node, "order"))); + + auto axis = get_node_value(node, "axis"); + switch (axis[0]) { + case 'x': + this->set_axis(Axis::x); + break; + case 'y': + this->set_axis(Axis::y); + break; + case 'z': + this->set_axis(Axis::z); + break; + default: + throw std::runtime_error { + "Axis for spatial expansion filters must be 'x', 'y', or 'z'"}; + } + + double min = std::stod(get_node_value(node, "min")); + double max = std::stod(get_node_value(node, "max")); + this->set_minmax(min, max); +} + +void SpatialExpansionFilter::set_axis(Axis axis) +{ + axis_ = axis; +} + +void SpatialExpansionFilter::set_minmax(double min, double max) +{ + if (max <= min) { + throw std::invalid_argument { + "Maximum value must be greater than minimum value"}; + } + min_ = min; + max_ = max; +} + +double SpatialExpansionFilter::position(const Particle& p) const +{ + if (axis_ == Axis::x) { + return p.r().x; + } else if (axis_ == Axis::y) { + return p.r().y; + } else { + return p.r().z; + } +} + +const char* SpatialExpansionFilter::axis_label() const +{ + if (axis_ == Axis::x) { + return "x"; + } else if (axis_ == Axis::y) { + return "y"; + } else { + return "z"; + } +} + +void SpatialExpansionFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + write_dataset(filter_group, "order", order_); + write_dataset(filter_group, "axis", axis_label()); + write_dataset(filter_group, "min", min_); + write_dataset(filter_group, "max", max_); +} + +} // namespace openmc diff --git a/src/tallies/filter_sptl_fourier.cpp b/src/tallies/filter_sptl_fourier.cpp index ce603ffa365..c1529e0b157 100644 --- a/src/tallies/filter_sptl_fourier.cpp +++ b/src/tallies/filter_sptl_fourier.cpp @@ -7,35 +7,9 @@ #include "openmc/capi.h" #include "openmc/constants.h" #include "openmc/error.h" -#include "openmc/xml_interface.h" namespace openmc { -void SpatialFourierFilter::from_xml(pugi::xml_node node) -{ - this->set_order(std::stoi(get_node_value(node, "order"))); - - auto axis = get_node_value(node, "axis"); - switch (axis[0]) { - case 'x': - this->set_axis(FourierAxis::x); - break; - case 'y': - this->set_axis(FourierAxis::y); - break; - case 'z': - this->set_axis(FourierAxis::z); - break; - default: - throw std::runtime_error { - "Axis for SpatialFourierFilter must be 'x', 'y', or 'z'"}; - } - - double min = std::stod(get_node_value(node, "min")); - double max = std::stod(get_node_value(node, "max")); - this->set_minmax(min, max); -} - void SpatialFourierFilter::set_order(int order) { if (order < 0) { @@ -45,33 +19,11 @@ void SpatialFourierFilter::set_order(int order) n_bins_ = 2 * order_ + 1; } -void SpatialFourierFilter::set_axis(FourierAxis axis) -{ - axis_ = axis; -} - -void SpatialFourierFilter::set_minmax(double min, double max) -{ - if (max <= min) { - throw std::invalid_argument { - "Maximum value must be greater than minimum value"}; - } - min_ = min; - max_ = max; -} - void SpatialFourierFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { // Get the coordinate along the axis of interest. - double x; - if (axis_ == FourierAxis::x) { - x = p.r().x; - } else if (axis_ == FourierAxis::y) { - x = p.r().y; - } else { - x = p.r().z; - } + double x = this->position(p); if (x >= min_ && x <= max_) { // Compute the normalized coordinate value on [0, 1] @@ -92,33 +44,9 @@ void SpatialFourierFilter::get_all_bins( } } -void SpatialFourierFilter::to_statepoint(hid_t filter_group) const -{ - Filter::to_statepoint(filter_group); - write_dataset(filter_group, "order", order_); - if (axis_ == FourierAxis::x) { - write_dataset(filter_group, "axis", "x"); - } else if (axis_ == FourierAxis::y) { - write_dataset(filter_group, "axis", "y"); - } else { - write_dataset(filter_group, "axis", "z"); - } - write_dataset(filter_group, "min", min_); - write_dataset(filter_group, "max", max_); -} - std::string SpatialFourierFilter::text_label(int bin) const { - std::string axis_str; std::string func_str; - if (axis_ == FourierAxis::x) { - axis_str = "x"; - } else if (axis_ == FourierAxis::y) { - axis_str = "y"; - } else { - axis_str = "z"; - } - if (bin == 0) { func_str = "a0 (constant)"; } else if (bin % 2 == 1) { @@ -128,7 +56,8 @@ std::string SpatialFourierFilter::text_label(int bin) const int n = bin / 2; func_str = fmt::format("b{} (sin)", n); } - return fmt::format("Fourier expansion, {} axis, {}", axis_str, func_str); + return fmt::format( + "Fourier expansion, {} axis, {}", this->axis_label(), func_str); } //============================================================================== @@ -213,7 +142,7 @@ extern "C" int openmc_spatial_fourier_filter_set_params( // Update the filter. if (axis) - filt->set_axis(static_cast(*axis)); + filt->set_axis(static_cast(*axis)); if (min && max) filt->set_minmax(*min, *max); return 0; diff --git a/src/tallies/filter_sptl_legendre.cpp b/src/tallies/filter_sptl_legendre.cpp index cf5ef2aed2d..ed0bcf972ec 100644 --- a/src/tallies/filter_sptl_legendre.cpp +++ b/src/tallies/filter_sptl_legendre.cpp @@ -7,35 +7,9 @@ #include "openmc/capi.h" #include "openmc/error.h" #include "openmc/math_functions.h" -#include "openmc/xml_interface.h" namespace openmc { -void SpatialLegendreFilter::from_xml(pugi::xml_node node) -{ - this->set_order(std::stoi(get_node_value(node, "order"))); - - auto axis = get_node_value(node, "axis"); - switch (axis[0]) { - case 'x': - this->set_axis(LegendreAxis::x); - break; - case 'y': - this->set_axis(LegendreAxis::y); - break; - case 'z': - this->set_axis(LegendreAxis::z); - break; - default: - throw std::runtime_error { - "Axis for SpatialLegendreFilter must be 'x', 'y', or 'z'"}; - } - - double min = std::stod(get_node_value(node, "min")); - double max = std::stod(get_node_value(node, "max")); - this->set_minmax(min, max); -} - void SpatialLegendreFilter::set_order(int order) { if (order < 0) { @@ -45,33 +19,11 @@ void SpatialLegendreFilter::set_order(int order) n_bins_ = order_ + 1; } -void SpatialLegendreFilter::set_axis(LegendreAxis axis) -{ - axis_ = axis; -} - -void SpatialLegendreFilter::set_minmax(double min, double max) -{ - if (max <= min) { - throw std::invalid_argument { - "Maximum value must be greater than minimum value"}; - } - min_ = min; - max_ = max; -} - void SpatialLegendreFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { // Get the coordinate along the axis of interest. - double x; - if (axis_ == LegendreAxis::x) { - x = p.r().x; - } else if (axis_ == LegendreAxis::y) { - x = p.r().y; - } else { - x = p.r().z; - } + double x = this->position(p); if (x >= min_ && x <= max_) { // Compute the normalized coordinate value. @@ -87,30 +39,10 @@ void SpatialLegendreFilter::get_all_bins( } } -void SpatialLegendreFilter::to_statepoint(hid_t filter_group) const -{ - Filter::to_statepoint(filter_group); - write_dataset(filter_group, "order", order_); - if (axis_ == LegendreAxis::x) { - write_dataset(filter_group, "axis", "x"); - } else if (axis_ == LegendreAxis::y) { - write_dataset(filter_group, "axis", "y"); - } else { - write_dataset(filter_group, "axis", "z"); - } - write_dataset(filter_group, "min", min_); - write_dataset(filter_group, "max", max_); -} - std::string SpatialLegendreFilter::text_label(int bin) const { - if (axis_ == LegendreAxis::x) { - return fmt::format("Legendre expansion, x axis, P{}", bin); - } else if (axis_ == LegendreAxis::y) { - return fmt::format("Legendre expansion, y axis, P{}", bin); - } else { - return fmt::format("Legendre expansion, z axis, P{}", bin); - } + return fmt::format( + "Legendre expansion, {} axis, P{}", this->axis_label(), bin); } //============================================================================== @@ -196,7 +128,7 @@ extern "C" int openmc_spatial_legendre_filter_set_params( // Update the filter. if (axis) - filt->set_axis(static_cast(*axis)); + filt->set_axis(static_cast(*axis)); if (min && max) filt->set_minmax(*min, *max); return 0; From e940f7ed8c38daf4c49f1f306c0644afb939ccf2 Mon Sep 17 00:00:00 2001 From: "Travis J. Labossiere-Hickman" Date: Thu, 11 Jun 2026 15:27:53 -0600 Subject: [PATCH 20/21] _dll get and set order --- openmc/lib/filter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 4f296f9c5cd..50b8d80d8a0 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -136,6 +136,12 @@ _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_fourier_filter_get_order.argtypes = [c_int32, POINTER(c_int)] +_dll.openmc_spatial_fourier_filter_get_order.restype = c_int +_dll.openmc_spatial_fourier_filter_get_order.errcheck = _error_handler +_dll.openmc_spatial_fourier_filter_set_order.argtypes = [c_int32, c_int] +_dll.openmc_spatial_fourier_filter_set_order.restype = c_int +_dll.openmc_spatial_fourier_filter_set_order.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 From 0dcc91a2c6495b54a7e839f752d745508f8ff655 Mon Sep 17 00:00:00 2001 From: "Travis J. Labossiere-Hickman" Date: Thu, 11 Jun 2026 15:28:09 -0600 Subject: [PATCH 21/21] whitespace cleanup --- openmc/filter_expansion.py | 2 +- tests/regression_tests/tallies/test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index 23fad8dfa11..550533a1583 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -139,7 +139,7 @@ def from_hdf5(cls, group, **kwargs): class SpatialExpansionFilter(ExpansionFilter): """Abstract base class for spatial functional expansion filters. - + This class provides common functionality for filters that expand tally data along a spatial axis (x, y, or z) within a bounded region. Subclasses must implement the order setter to define their specific diff --git a/tests/regression_tests/tallies/test.py b/tests/regression_tests/tallies/test.py index 936339e9276..6e7b58d991a 100644 --- a/tests/regression_tests/tallies/test.py +++ b/tests/regression_tests/tallies/test.py @@ -182,7 +182,7 @@ def test_tallies(): azimuthal_tally1, azimuthal_tally2, azimuthal_tally3, cellborn_tally, dg_tally, energy_tally, energyout_tally, transfer_tally, material_tally, mu_tally1, mu_tally2, - polar_tally1, polar_tally2, polar_tally3, + polar_tally1, polar_tally2, polar_tally3, legendre_tally, spatial_legendre_tally, spatial_fourier_tally, harmonics_tally, harmonics_tally2, harmonics_tally3, universe_tally, collision_tally]