diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fe133a22e3..44809d22424 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,6 +451,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_musurface.cpp src/tallies/filter_parent_nuclide.cpp src/tallies/filter_particle.cpp + src/tallies/filter_point.cpp src/tallies/filter_particle_production.cpp src/tallies/filter_polar.cpp src/tallies/filter_reaction.cpp diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 0b425a673dc..85d6e1ebe93 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -296,9 +296,9 @@ enum class MgxsType { enum class TallyResult { VALUE, SUM, SUM_SQ, SUM_THIRD, SUM_FOURTH }; -enum class TallyType { VOLUME, MESH_SURFACE, SURFACE, PULSE_HEIGHT }; +enum class TallyType { VOLUME, MESH_SURFACE, SURFACE, PULSE_HEIGHT, POINT }; -enum class TallyEstimator { ANALOG, TRACKLENGTH, COLLISION }; +enum class TallyEstimator { ANALOG, TRACKLENGTH, COLLISION, NEXT_EVENT }; enum class TallyEvent { SURFACE, LATTICE, KILL, SCATTER, ABSORB }; diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 5b632bbce53..52aba848a10 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -484,7 +484,7 @@ class GeometryState { * Algorithms.” Annals of Nuclear Energy 113 (March 2018): 506–18. * https://doi.org/10.1016/j.anucene.2017.11.032. */ -class ParticleData : public GeometryState { +class ParticleData : virtual public GeometryState { private: //========================================================================== // Data members -- see public: below for descriptions diff --git a/include/openmc/physics.h b/include/openmc/physics.h index 2472d979939..9cc6d95dc02 100644 --- a/include/openmc/physics.h +++ b/include/openmc/physics.h @@ -85,7 +85,7 @@ void sample_fission_neutron( //! handles all reactions with a single secondary neutron (other than fission), //! i.e. level scattering, (n,np), (n,na), etc. -void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p); +void inelastic_scatter(int i_nuclide, const Reaction& rx, Particle& p); void sample_secondary_photons(Particle& p, int i_nuclide); diff --git a/include/openmc/position.h b/include/openmc/position.h index 5d291d26b95..92346e27ad4 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -204,6 +204,17 @@ inline Position operator/(double a, Position b) return b /= a; } +inline bool operator<(Position a, Position b) +{ + if (a[0] != b[0]) + return (a[0] < b[0]); + if (a[1] != b[1]) + return (a[1] < b[1]); + if (a[2] != b[2]) + return (a[2] < b[2]); + return false; +} + inline Position Position::reflect(Position n) const { const double projection = n.dot(*this); diff --git a/include/openmc/ray.h b/include/openmc/ray.h index 62e86b0d90f..d4eed886850 100644 --- a/include/openmc/ray.h +++ b/include/openmc/ray.h @@ -1,14 +1,14 @@ #ifndef OPENMC_RAY_H #define OPENMC_RAY_H -#include "openmc/particle_data.h" +#include "openmc/particle.h" #include "openmc/position.h" namespace openmc { // Base class that implements ray tracing logic, not necessarily through // defined regions of the geometry but also outside of it. -class Ray : public GeometryState { +class Ray : virtual public GeometryState { public: // Initialize from location and direction @@ -32,6 +32,8 @@ class Ray : public GeometryState { // Sets the dist_ variable void compute_distance(); + virtual void update_distance(); + protected: // Records how far the ray has traveled double traversal_distance_ {0.0}; @@ -46,5 +48,30 @@ class Ray : public GeometryState { unsigned event_counter_ {0}; }; +class ParticleRay : public Ray, public Particle { + +public: + ParticleRay( + Position r, Direction u, ParticleType type_, double time_, double E_) + : Ray(r, u) + { + type() = type_; + time() = time_; + E() = E_; + } + + void on_intersection() override; + + // Sets the dist_ variable + void update_distance() override; + + const double& traversal_distance() const { return traversal_distance_; } + const double& traversal_mfp() const { return traversal_mfp_; } + +protected: + // Records how much mean free paths the ray traveled + double traversal_mfp_ {0.0}; +}; + } // namespace openmc #endif // OPENMC_RAY_H diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 9a6cf1b2131..3484ab707d4 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -36,12 +36,14 @@ extern "C" double extern double log_spacing; //!< lethargy spacing for energy grid searches extern "C" int n_lost_particles; //!< cumulative number of lost particles extern "C" bool need_depletion_rx; //!< need to calculate depletion rx? -extern "C" int restart_batch; //!< batch at which a restart job resumed -extern "C" bool satisfy_triggers; //!< have tally triggers been satisfied? -extern int ssw_current_file; //!< current surface source file -extern "C" int total_gen; //!< total number of generations simulated -extern double total_weight; //!< Total source weight in a batch -extern int64_t work_per_rank; //!< number of particles per MPI rank +extern bool + nonvacuum_boundary_present; //!< Does the geometry contain non-vacuum b.c. +extern "C" int restart_batch; //!< batch at which a restart job resumed +extern "C" bool satisfy_triggers; //!< have tally triggers been satisfied? +extern int ssw_current_file; //!< current surface source file +extern "C" int total_gen; //!< total number of generations simulated +extern double total_weight; //!< Total source weight in a batch +extern int64_t work_per_rank; //!< number of particles per MPI rank extern const RegularMesh* entropy_mesh; extern const RegularMesh* ufs_mesh; diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 77b0d9f420d..34553f5dc1d 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -40,6 +40,7 @@ enum class FilterType { PARENT_NUCLIDE, PARTICLE, PARTICLE_PRODUCTION, + POINT, POLAR, REACTION, SPHERICAL_HARMONICS, diff --git a/include/openmc/tallies/filter_point.h b/include/openmc/tallies/filter_point.h new file mode 100644 index 00000000000..909f0baa27d --- /dev/null +++ b/include/openmc/tallies/filter_point.h @@ -0,0 +1,55 @@ +#ifndef OPENMC_TALLIES_FILTER_POINT_H +#define OPENMC_TALLIES_FILTER_POINT_H + +#include "openmc/position.h" +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins tally by point detectors +//============================================================================== + +class PointFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~PointFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "point"; } + FilterType type() const override { return FilterType::POINT; } + + 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 + + const vector>& detectors() const + { + return detectors_; + } + + void set_detectors(span> detectors); + +private: + //---------------------------------------------------------------------------- + // Data members + + vector> detectors_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_PARTICLE_H diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 66e6ff764f3..de8495b5196 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -3,6 +3,7 @@ #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr +#include "openmc/position.h" #include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/trigger.h" @@ -11,6 +12,7 @@ #include "openmc/tensor.h" #include "pugixml.hpp" +#include #include #include @@ -209,11 +211,13 @@ extern vector active_analog_tallies; extern vector active_tracklength_tallies; extern vector active_timed_tracklength_tallies; extern vector active_collision_tallies; +extern vector active_point_tallies; extern vector active_meshsurf_tallies; extern vector active_surface_tallies; extern vector active_pulse_height_tallies; extern vector pulse_height_cells; extern vector time_grid; +extern std::set active_point_detectors; } // namespace model diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index d1aed28318c..6e4c646a8f9 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -1,9 +1,12 @@ #ifndef OPENMC_TALLIES_TALLY_SCORING_H #define OPENMC_TALLIES_TALLY_SCORING_H +#include "openmc/nuclide.h" #include "openmc/particle.h" +#include "openmc/ray.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/tally.h" +#include "openmc/thermal.h" namespace openmc { @@ -81,6 +84,9 @@ void score_analog_tally_ce(Particle& p); //! \param p The particle being tracked void score_analog_tally_mg(Particle& p); +void score_tracklength_tally_general( + Particle& p, double flux, const vector& tallies); + //! Score tallies using a tracklength estimate of the flux. // //! This is triggered at every event (surface crossing, lattice crossing, or @@ -122,6 +128,52 @@ void score_surface_tally( //! \param tallies A vector of the indices of the tallies to score to void score_pulse_height_tally(Particle& p, const vector& tallies); +void score_point_tally(Particle& p, int i_nuclide, const Reaction& rx, + int i_product, Direction* v_t); + +void score_point_tally(Particle& p, int i_nuclide, const ThermalData& sab, + const NuclideMicroXS& micro); + +void score_point_tally(SourceSite& site, int source_index); + +template +void score_point_tally_impl( + const Position r, const ParticleType type, const double time, PDF pdffunc) +{ + for (auto& det : model::active_point_detectors) { + auto u = (det - r); + double total_distance = u.norm(); + u /= total_distance; + double E; + double pdf = pdffunc(u, E); + if (pdf < 0.0) + fatal_error("negative pdf"); + auto p = ParticleRay(r, u, type, time, E); + p.Ray::trace(); + double distance = p.traversal_distance(); + if (distance < total_distance) + continue; + double mfp = p.traversal_mfp(); + double attenuation = std::exp(-mfp); + + // Save the attenuation for point filter handling + p.wgt_last() = p.wgt(); + p.wgt() *= attenuation; + + // Set position state so PointFilter::get_all_bins can match: + // r() = detector position (for bin matching), + // r_last() = source/scatter position (for 1/r^2 weight) + p.r_last() = r; + p.r() = det; + + // Set E_last so EnergyFilter::get_all_bins bins correctly + p.E_last() = E; + + double flux = p.wgt_last() * pdf; + score_tracklength_tally_general(p, flux, model::active_point_tallies); + } +} + } // namespace openmc #endif // OPENMC_TALLIES_TALLY_SCORING_H diff --git a/openmc/filter.py b/openmc/filter.py index 87aeb70c3a7..23f1d6dd16e 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -26,7 +26,7 @@ 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', - 'particleproduction', 'cellinstance', 'collision', 'time', 'parentnuclide', + 'particleproduction', 'point', 'cellinstance', 'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface', 'meshmaterial', 'reaction', ) @@ -793,6 +793,88 @@ def from_xml_element(cls, elem, **kwargs): return cls(bins, filter_id=filter_id) +class PointFilter(Filter): + """Bins tally events based on point detectors. + + Parameters + ---------- + bins : sequence of tuple[tuple[Real, Real, Real], Real] + Point detectors positions and exclusion radii. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : sequence of tuple[tuple[Real, Real, Real], Real] + Point detectors positions and exclusion radii. + id : int + Unique identifier for the filter + num_bins : Integral + The number of filter bins + + """ + + __hash__ = Filter.__hash__ + + def __eq__(self, other): + if type(self) is not type(other): + return False + elif len(self.bins) != len(other.bins): + return False + else: + return all(b1==b2 for b1,b2 in zip(self.bins,other.bins)) + + @Filter.bins.setter + def bins(self, bins): + cv.check_type('bins', bins, Sequence, tuple) + for i, item in enumerate(bins): + cv.check_type(f'bins[{i}]', item, tuple) + cv.check_length(f'bins[{i}]', item, 2, 2) + cv.check_type(f'bins[{i}][0]', item[0], tuple, Real) + cv.check_length(f'bins[{i}][0]', item[0], 3, 3) + cv.check_type(f'bins[{i}][1]', item[1], Real) + self._bins = bins + + @classmethod + def from_hdf5(cls, group, **kwargs): + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + flat = group['bins'][()] + # Reconstruct tuple structure: every 4 values = (x, y, z, r0) + bins = [] + for i in range(0, len(flat), 4): + pos = (float(flat[i]), float(flat[i+1]), float(flat[i+2])) + r0 = float(flat[i+3]) + bins.append((pos, r0)) + out = cls(bins, filter_id=filter_id) + out._num_bins = group['n_bins'][()] + return out + + def get_pandas_dataframe(self, data_size, stride, **kwargs): + import pandas as pd + labels = [f"({p[0]}, {p[1]}, {p[2]}) R0={r}" for (p, r) in self.bins] + filter_bins = np.repeat(labels, stride) + tile_factor = data_size // len(filter_bins) + filter_bins = np.tile(filter_bins, tile_factor) + return pd.DataFrame({self.short_name.lower(): filter_bins}) + + def to_xml_element(self): + """Return XML Element representing the Filter. + + Returns + ------- + element : lxml.etree._Element + XML element containing filter data + + """ + element = ET.Element('filter') + element.set('id', str(self.id)) + element.set('type', self.short_name.lower()) + + subelement = ET.SubElement(element, 'bins') + subelement.text = ' '.join(str(b) for item in self.bins + for b in list(item[0])+[item[1]]) + return element + class ParentNuclideFilter(ParticleFilter): """Bins tally events based on the parent nuclide diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 574a37443ae..3db32cd59a5 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -22,7 +22,7 @@ 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', 'MeshMaterialFilter', 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', - 'ParentNuclideFilter', 'ParticleFilter', 'ParticleProductionFilter', 'PolarFilter', + 'ParentNuclideFilter', 'ParticleFilter', 'ParticleProductionFilter', 'PointFilter', 'PolarFilter', 'ReactionFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', 'WeightFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' @@ -607,6 +607,8 @@ def bins(self): self._index, particle_i.ctypes.data_as(POINTER(c_int32))) return [ParticleType(i) for i in particle_i] +class PointFilter(Filter): + filter_type = 'point' class ParticleProductionFilter(Filter): filter_type = 'particleproduction' diff --git a/openmc/tallies.py b/openmc/tallies.py index ceced425533..07b09edb903 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -53,7 +53,7 @@ _FILTER_CLASSES = (Filter, CrossFilter, AggregateFilter) # Valid types of estimators -ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog'} +ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog', 'next-event'} class Tally(IDManagerMixin): diff --git a/src/physics.cpp b/src/physics.cpp index 106bd1aa2b2..71261b4bcb1 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -25,6 +25,7 @@ #include "openmc/simulation.h" #include "openmc/string_utils.h" #include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" #include "openmc/thermal.h" #include "openmc/weight_windows.h" @@ -739,7 +740,7 @@ void scatter(Particle& p, int i_nuclide) // Perform collision physics for inelastic scattering const auto& rx {nuc->reactions_[i]}; - inelastic_scatter(*nuc, *rx, p); + inelastic_scatter(i_nuclide, *rx, p); p.event_mt() = rx->mt_; } @@ -785,6 +786,10 @@ void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p) // Find speed of neutron in CM vel = v_n.norm(); + if (!model::active_point_tallies.empty()) { + score_point_tally(p, i_nuclide, rx, 0, &v_t); + } + // Sample scattering angle, checking if angle distribution is present (assume // isotropic otherwise) double mu_cm; @@ -832,8 +837,13 @@ void sab_scatter(int i_nuclide, int i_sab, Particle& p) // Sample energy and angle double E_out; - data::thermal_scatt[i_sab]->data_[i_temp].sample( - micro, p.E(), &E_out, &p.mu(), p.current_seed()); + auto& sab = data::thermal_scatt[i_sab]->data_[i_temp]; + + if (!model::active_point_tallies.empty()) { + score_point_tally(p, i_nuclide, sab, micro); + } + + sab.sample(micro, p.E(), &E_out, &p.mu(), p.current_seed()); // Set energy to outgoing, change direction of particle p.E() = E_out; @@ -1098,6 +1108,10 @@ void sample_fission_neutron( site->delayed_group = 0; } + if (!model::active_point_tallies.empty()) { + score_point_tally(p, i_nuclide, rx, site->delayed_group, nullptr); + } + // sample from prompt neutron energy distribution int n_sample = 0; double mu; @@ -1123,8 +1137,11 @@ void sample_fission_neutron( site->u = rotate_angle(p.u(), mu, nullptr, seed); } -void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p) +void inelastic_scatter(int i_nuclide, const Reaction& rx, Particle& p) { + // Get pointer to nuclide + const auto& nuc {data::nuclides[i_nuclide]}; + // copy energy of neutron double E_in = p.E(); @@ -1133,13 +1150,17 @@ void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p) double mu; rx.products_[0].sample(E_in, E, mu, p.current_seed()); + if (!model::active_point_tallies.empty()) { + score_point_tally(p, i_nuclide, rx, 0, nullptr); + } + // if scattering system is in center-of-mass, transfer cosine of scattering // angle and outgoing energy from CM to LAB if (rx.scatter_in_cm_) { double E_cm = E; // determine outgoing energy in lab - double A = nuc.awr_; + double A = nuc->awr_; E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) / ((A + 1.0) * (A + 1.0)); diff --git a/src/ray.cpp b/src/ray.cpp index 3d848e3a3a7..f0667ff5e16 100644 --- a/src/ray.cpp +++ b/src/ray.cpp @@ -2,6 +2,8 @@ #include "openmc/error.h" #include "openmc/geometry.h" +#include "openmc/material.h" +#include "openmc/mgxs_interface.h" #include "openmc/settings.h" namespace openmc { @@ -136,8 +138,8 @@ void Ray::trace() cross_lattice(*this, boundary(), settings::verbosity >= 10); } - // Record how far the ray has traveled - traversal_distance_ += boundary().distance(); + update_distance(); + inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10); // Call the specialized logic for this type of ray. Note that we do not @@ -165,4 +167,47 @@ void Ray::trace() } } +void Ray::update_distance() +{ + // Record how far the ray has traveled + traversal_distance_ += boundary().distance(); +} + +void ParticleRay::on_intersection() {} + +void ParticleRay::update_distance() +{ + Ray::update_distance(); + + time() += boundary().distance() / speed(); + + // Calculate microscopic and macroscopic cross sections + if (material() != MATERIAL_VOID) { + if (settings::run_CE) { + if (material() != material_last() || sqrtkT() != sqrtkT_last() || + density_mult() != density_mult_last()) { + // If the material is the same as the last material and the + // temperature hasn't changed, we don't need to lookup cross + // sections again. + model::materials[material()]->calculate_xs(*this); + } + } else { + // Get the MG data; unlike the CE case above, we have to re-calculate + // cross sections for every collision since the cross sections may + // be angle-dependent + data::mg.macro_xs_[material()].calculate_xs(*this); + + // Update the particle's group while we know we are multi-group + g_last() = g(); + } + } else { + macro_xs().total = 0.0; + macro_xs().absorption = 0.0; + macro_xs().fission = 0.0; + macro_xs().nu_fission = 0.0; + } + + traversal_mfp_ += macro_xs().total * boundary().distance(); +} + } // namespace openmc diff --git a/src/simulation.cpp b/src/simulation.cpp index 4fad196a604..c384b86f04c 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -311,6 +311,7 @@ double k_abs_tra {0.0}; double log_spacing; int n_lost_particles {0}; bool need_depletion_rx {false}; +bool nonvacuum_boundary_present {false}; int restart_batch; bool satisfy_triggers {false}; int ssw_current_file; diff --git a/src/source.cpp b/src/source.cpp index 524a72bf0e4..86c6e0049f8 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -33,6 +33,7 @@ #include "openmc/simulation.h" #include "openmc/state_point.h" #include "openmc/string_utils.h" +#include "openmc/tallies/tally_scoring.h" #include "openmc/xml_interface.h" namespace openmc { @@ -706,6 +707,10 @@ SourceSite sample_external_source(uint64_t* seed) site.E = data::mg.num_energy_groups_ - site.E - 1.; } + if (!model::active_point_tallies.empty()) { + score_point_tally(site, i); + } + return site; } diff --git a/src/state_point.cpp b/src/state_point.cpp index da1c141a230..f599ada7947 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -214,6 +214,8 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) write_dataset(tally_group, "estimator", "tracklength"); } else if (tally->estimator_ == TallyEstimator::COLLISION) { write_dataset(tally_group, "estimator", "collision"); + } else if (tally->estimator_ == TallyEstimator::NEXT_EVENT) { + write_dataset(tally_group, "estimator", "next-event"); } write_dataset(tally_group, "n_realizations", tally->n_realizations_); diff --git a/src/surface.cpp b/src/surface.cpp index 81b756deae7..08cdc494807 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -17,6 +17,7 @@ #include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" +#include "openmc/simulation.h" #include "openmc/string_utils.h" #include "openmc/xml_interface.h" @@ -1175,6 +1176,7 @@ void read_surfaces(pugi::xml_node node, std::unordered_map& albedo_map, std::unordered_map& periodic_sense_map) { + simulation::nonvacuum_boundary_present = false; // Count the number of surfaces int n_surfaces = 0; for (pugi::xml_node surf_node : node.children("surface")) { @@ -1245,6 +1247,8 @@ void read_surfaces(pugi::xml_node node, // Check for a periodic surface if (check_for_node(surf_node, "boundary")) { std::string surf_bc = get_node_value(surf_node, "boundary", true, true); + if (surf_bc != "vacuum") + simulation::nonvacuum_boundary_present = true; if (surf_bc == "periodic") { periodic_sense_map[model::surfaces.back()->id_] = 0; // Check for surface albedo. Skip sanity check as it is already done diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index badb9107733..1068b4b5d08 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -32,6 +32,7 @@ #include "openmc/tallies/filter_parent_nuclide.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_particle_production.h" +#include "openmc/tallies/filter_point.h" #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_reaction.h" #include "openmc/tallies/filter_sph_harm.h" @@ -150,6 +151,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "particleproduction") { return Filter::create(id); + } else if (type == "point") { + return Filter::create(id); } else if (type == "polar") { return Filter::create(id); } else if (type == "reaction") { diff --git a/src/tallies/filter_point.cpp b/src/tallies/filter_point.cpp new file mode 100644 index 00000000000..35f4d2e03cf --- /dev/null +++ b/src/tallies/filter_point.cpp @@ -0,0 +1,79 @@ +#include "openmc/tallies/filter_point.h" +#include "openmc/tallies/tally_scoring.h" + +#include + +#include "openmc/math_functions.h" +#include "openmc/simulation.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +void PointFilter::from_xml(pugi::xml_node node) +{ + auto bins = get_node_array(node, "bins"); + + // Convert to vector of detectors + vector> detectors; + size_t n = bins.size() / 4; + for (int i = 0; i < n; ++i) { + Position pos {bins[4 * i], bins[4 * i + 1], bins[4 * i + 2]}; + detectors.push_back(std::make_pair(pos, bins[4 * i + 3])); + } + this->set_detectors(detectors); +} + +void PointFilter::set_detectors(span> detectors) +{ + // Clear existing detectors + detectors_.clear(); + detectors_.reserve(detectors.size()); + + // Set detectors and number of bins + for (auto d : detectors) { + detectors_.push_back(d); + } + n_bins_ = detectors_.size(); +} + +void PointFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + double attenuation = p.wgt() / p.wgt_last(); + int i = 0; + for (auto [pos, r] : detectors_) { + if ((p.r() - pos).norm() < FP_COINCIDENT) { + match.bins_.push_back(i); + double weight; + double distance = (p.r_last() - pos).norm(); + if (distance > r) { + weight = attenuation / (distance * distance); + } else { + weight = 3.0 * exprel(-p.macro_xs().total * r) / (r * r); + } + match.weights_.push_back(weight); + } + ++i; + } +} + +void PointFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + vector detectors; + for (auto [pos, r] : detectors_) { + detectors.push_back(pos[0]); + detectors.push_back(pos[1]); + detectors.push_back(pos[2]); + detectors.push_back(r); + } + write_dataset(filter_group, "bins", detectors); +} + +std::string PointFilter::text_label(int bin) const +{ + auto [pos, r] = detectors_.at(bin); + return fmt::format("Point: {} {}", pos, r); +} + +} // namespace openmc diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 3fe48c1b021..f4de1253500 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -31,6 +31,7 @@ #include "openmc/tallies/filter_meshmaterial.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_particle.h" +#include "openmc/tallies/filter_point.h" #include "openmc/tallies/filter_sph_harm.h" #include "openmc/tallies/filter_surface.h" #include "openmc/tallies/filter_time.h" @@ -60,11 +61,13 @@ vector active_analog_tallies; vector active_tracklength_tallies; vector active_timed_tracklength_tallies; vector active_collision_tallies; +vector active_point_tallies; vector active_meshsurf_tallies; vector active_surface_tallies; vector active_pulse_height_tallies; vector pulse_height_cells; vector time_grid; +std::set active_point_detectors; } // namespace model namespace simulation { @@ -540,6 +543,7 @@ void Tally::set_scores(const vector& scores) bool legendre_present = false; bool cell_present = false; bool cellfrom_present = false; + bool point_present = false; bool material_present = false; bool materialfrom_present = false; bool surface_present = false; @@ -562,6 +566,10 @@ void Tally::set_scores(const vector& scores) materialfrom_present = true; } else if (filt->type() == FilterType::MATERIAL) { material_present = true; + } else if (filt->type() == FilterType::POINT) { + point_present = true; + type_ = TallyType::POINT; + estimator_ = TallyEstimator::NEXT_EVENT; } else if (filt->type() == FilterType::SURFACE) { surface_present = true; } else if (filt->type() == FilterType::MESH_SURFACE) { @@ -574,6 +582,19 @@ void Tally::set_scores(const vector& scores) (surface_present || cell_present || cellfrom_present || material_present || materialfrom_present); + if (point_present) { + if (simulation::nonvacuum_boundary_present) + fatal_error( + "Cannot use point detectors with non-vacuum boundary conditions."); + if (legendre_present) + fatal_error("Cannot use LegendreFilter with PointFilter."); + if (energyout_present) + fatal_error("Cannot use EnergyoutFilter with PointFilter."); + if (surface_present || meshsurface_present) + fatal_error( + "Cannot use surface or mesh-surface filters with PointFilter."); + } + // Iterate over the given scores. for (auto score_str : scores) { // Make sure a delayed group filter wasn't used with an incompatible @@ -623,6 +644,9 @@ void Tally::set_scores(const vector& scores) case SCORE_NU_SCATTER: if (settings::run_CE) { + if (point_present) + fatal_error("Cannot use nu-scatter score with PointFilter in " + "continuous energy mode."); estimator_ = TallyEstimator::ANALOG; } else { if (energyout_present || legendre_present) @@ -631,6 +655,8 @@ void Tally::set_scores(const vector& scores) break; case SCORE_CURRENT: + if (point_present) + fatal_error("Cannot use current score with PointFilter."); // Check which type of current is desired: mesh or surface currents. if (meshsurface_present) { if (non_meshsurface_types_present) @@ -644,11 +670,15 @@ void Tally::set_scores(const vector& scores) break; case HEATING: + if (point_present) + fatal_error("Cannot use heating score with PointFilter."); if (settings::photon_transport) estimator_ = TallyEstimator::COLLISION; break; case SCORE_PULSE_HEIGHT: { + if (point_present) + fatal_error("Cannot use pulse-height score with PointFilter."); if (non_cell_energy_present) { fatal_error("Pulse-height tallies are not compatible with filters " "other than CellFilter and EnergyFilter"); @@ -670,6 +700,8 @@ void Tally::set_scores(const vector& scores) case SCORE_IFP_TIME_NUM: case SCORE_IFP_BETA_NUM: case SCORE_IFP_DENOM: + if (point_present) + fatal_error("Cannot use ifp scores with PointFilter."); estimator_ = TallyEstimator::COLLISION; break; } @@ -1168,6 +1200,8 @@ void setup_active_tallies() model::active_meshsurf_tallies.clear(); model::active_surface_tallies.clear(); model::active_pulse_height_tallies.clear(); + model::active_point_tallies.clear(); + model::active_point_detectors.clear(); model::time_grid.clear(); for (auto i = 0; i < model::tallies.size(); ++i) { @@ -1209,6 +1243,16 @@ void setup_active_tallies() case TallyType::PULSE_HEIGHT: model::active_pulse_height_tallies.push_back(i); break; + + case TallyType::POINT: + model::active_point_tallies.push_back(i); + // Populate the set of unique detector positions from PointFilter + if (auto pf = tally.get_filter()) { + for (const auto& [pos, r0] : pf->detectors()) { + model::active_point_detectors.insert(pos); + } + } + break; } } } @@ -1232,6 +1276,8 @@ void free_memory_tally() model::active_meshsurf_tallies.clear(); model::active_surface_tallies.clear(); model::active_pulse_height_tallies.clear(); + model::active_point_tallies.clear(); + model::active_point_detectors.clear(); model::time_grid.clear(); model::tally_map.clear(); diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 5dcd07331d6..43d545e1029 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -13,6 +13,7 @@ #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/simulation.h" +#include "openmc/source.h" #include "openmc/string_utils.h" #include "openmc/surface.h" #include "openmc/tallies/derivative.h" @@ -2784,4 +2785,82 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) p.E_last() = orig_E_last; } } + +void score_point_tally( + Particle& p, int i_nuclide, const Reaction& rx, int i_product, Direction* v_t) +{ + if (rx.scatter_in_cm_) { + const auto& nuc {data::nuclides[i_nuclide]}; + double awr = nuc->awr_; + double E_in = p.E(); + double vel = std::sqrt(E_in); + Direction v_n = vel * p.u(); + Direction v_cm = v_n / (awr + 1.0); + if (v_t != nullptr) + v_cm += awr / (awr + 1.0) * (*v_t); + Direction u_cm = (v_n - v_cm); + u_cm /= u_cm.norm(); + + auto pdf = [&](Direction u, double& E) { + double mu = u.dot(u_cm); + double E_cm; + double pdf0 = rx.products_[i_product].sample_energy_and_pdf( + p.E(), mu, E_cm, p.current_seed()); + Direction v_out = std::sqrt(E_cm) * u_cm + v_cm; + E = std::pow(v_out.norm(), 2); + double jac = + std::sqrt(E / E_cm) / (1 - mu / (awr + 1) * std::sqrt(E_in / E)); + return pdf0 * std::abs(jac) / (2.0 * PI); + }; + score_point_tally_impl(p.r(), p.type(), p.time(), pdf); + } else { + auto pdf = [&](Direction u, double& E) { + return rx.products_[i_product].sample_energy_and_pdf( + p.E(), u.dot(p.u()), E, p.current_seed()) / + (2.0 * PI); + }; + score_point_tally_impl(p.r(), p.type(), p.time(), pdf); + } +} + +void score_point_tally(Particle& p, int i_nuclide, const ThermalData& sab, + const NuclideMicroXS& micro) +{ + const auto& nuc {data::nuclides[i_nuclide]}; + double awr = nuc->awr_; + double E_in = p.E(); + double vel = std::sqrt(E_in); + Direction v_n = vel * p.u(); + Direction v_cm = v_n / (awr + 1.0); + Direction u_cm = (v_n - v_cm); + u_cm /= u_cm.norm(); + + auto pdf = [&](Direction u, double& E) { + double mu = u.dot(u_cm); + double E_cm; + double pdf0 = + sab.sample_energy_and_pdf(micro, p.E(), mu, E_cm, p.current_seed()); + Direction v_out = std::sqrt(E_cm) * u_cm + v_cm; + E = std::pow(v_out.norm(), 2); + double jac = + std::sqrt(E / E_cm) / (1 - mu / (awr + 1) * std::sqrt(E_in / E)); + return pdf0 * std::abs(jac) / (2.0 * PI); + }; + + score_point_tally_impl(p.r(), p.type(), p.time(), pdf); +} + +void score_point_tally(SourceSite& site, int source_index) +{ + auto src_ = model::external_sources[source_index].get(); + auto src = dynamic_cast(src_); + if (!src) + fatal_error("Only independent source is valid for point detectors."); + auto pdf = [&](Direction u, double& E) { + E = site.E; + return src->angle()->evaluate(u); + }; + score_point_tally_impl(site.r, site.particle, site.time, pdf); +} + } // namespace openmc