mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-27 21:55:41 -04:00
Implement surface flux tallies (#3742)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
parent
b796afb591
commit
70be650003
19 changed files with 424 additions and 54 deletions
|
|
@ -1234,6 +1234,23 @@ attributes/sub-elements:
|
|||
are not eligible to store any particles when using ``cell``, ``cellfrom``
|
||||
or ``cellto`` attributes. It is recommended to use surface IDs instead.
|
||||
|
||||
------------------------------------
|
||||
``<surface_grazing_cutoff>`` Element
|
||||
------------------------------------
|
||||
|
||||
The ``<surface_grazing_cutoff>`` element specifies the surface flux cosine cutoff.
|
||||
|
||||
*Default*: 0.001
|
||||
|
||||
-----------------------------------
|
||||
``<surface_grazing_ratio>`` Element
|
||||
-----------------------------------
|
||||
|
||||
The ``<surface_grazing_ratio>`` element specifies the surface flux cosine
|
||||
substitution ratio.
|
||||
|
||||
*Default*: 0.5
|
||||
|
||||
------------------------------
|
||||
``<survival_biasing>`` Element
|
||||
------------------------------
|
||||
|
|
|
|||
|
|
@ -205,7 +205,71 @@ had a collision at every event. Thus, for tallies with outgoing-energy filters
|
|||
or for tallies of scattering moments (which require the scattering cosine of
|
||||
the change-in-angle), we must use an analog estimator.
|
||||
|
||||
.. TODO: Add description of surface current tallies
|
||||
-----------------------------------
|
||||
Surface-Integrated Flux and Current
|
||||
-----------------------------------
|
||||
|
||||
Surface tallies allow you to measure particle behavior as they cross specific
|
||||
boundaries in your geometry. Unlike volume tallies, which integrate over a
|
||||
volumetric region, surface tallies capture the current or flux passing through a
|
||||
surface. Surface tallies are estimated using an analog estimator.
|
||||
|
||||
Current Score
|
||||
-------------
|
||||
|
||||
When tallying the current across a surface, we simply count the weight of
|
||||
particles that cross the surface of interest:
|
||||
|
||||
|
||||
.. math::
|
||||
:label: analog-current-estimator
|
||||
|
||||
J = \frac{1}{W} \sum_{i \in S} w_i.
|
||||
|
||||
where :math:`J` is the area-integrated current passing through surface
|
||||
:math:`S`, :math:`W` is the total starting weight of the particles, and
|
||||
:math:`w_i` is the weight of the particle as it crosses the surface :math:`S`.
|
||||
|
||||
Flux Score
|
||||
----------
|
||||
|
||||
When tallying flux over a surface, we use the relationship between current and
|
||||
flux:
|
||||
|
||||
|
||||
.. math::
|
||||
:label: surface-flux-estimator
|
||||
|
||||
\phi_S = \frac{1}{W} \sum_{i \in S} \frac{w_i}{|\mu|}.
|
||||
|
||||
where :math:`\phi_S` is the area-integrated flux over surface :math:`S`,
|
||||
:math:`W` is the total starting weight of the particles, :math:`w_i` is the
|
||||
weight of the particle as it crosses the surface :math:`S` and :math:`\mu` is
|
||||
the cosine of angle between the particle direction and the surface normal.
|
||||
|
||||
This equation diverges when the particle crossing the surface is nearly parallel
|
||||
to it (that is, as :math:`\mu` approaches zero). To remove this divergence,
|
||||
OpenMC scores:
|
||||
|
||||
.. math::
|
||||
:label: modified-surface-flux-estimator
|
||||
|
||||
\phi_S = \frac{1}{W} \sum_{i \in S} w_i f(\mu).
|
||||
|
||||
and the function :math:`f` is defined by:
|
||||
|
||||
.. math::
|
||||
f(\mu) = \begin{cases}
|
||||
\frac{1}{|\mu|} & |\mu| > \mu_\text{cut} \\
|
||||
\frac{1}{c\mu_\text{cut}} & |\mu| \le \mu_\text{cut}
|
||||
\end{cases}
|
||||
|
||||
where :math:`\mu_\text{cut}` is the grazing cosine cutoff and :math:`c` is the
|
||||
cosine substitution ratio. The parameters :math:`\mu_\text{cut}` and :math:`c`
|
||||
can be set by the user via the :attr:`openmc.Settings.surface_grazing_cutoff`
|
||||
and :attr:`openmc.Settings.surface_grazing_ratio` attributes, respectively. The
|
||||
default values for these parameters are 0.001 and 0.5 as recommended by
|
||||
`Favorite, Thomas, and Booth <https://doi.org/10.13182/NSE09-72>`_.
|
||||
|
||||
.. _tallies_statistics:
|
||||
|
||||
|
|
|
|||
|
|
@ -261,18 +261,21 @@ The following tables show all valid scores:
|
|||
+----------------------+---------------------------------------------------+
|
||||
|Score | Description |
|
||||
+======================+===================================================+
|
||||
|current |Used in combination with a meshsurface filter: |
|
||||
|current |It may not be used in conjunction with any other |
|
||||
| |score except flux. |
|
||||
| | |
|
||||
| |When used in combination with a meshsurface filter:|
|
||||
| |Partial currents on the boundaries of each cell in |
|
||||
| |a mesh. It may not be used in conjunction with any |
|
||||
| |other score. Only energy and mesh filters may be |
|
||||
| |used. |
|
||||
| |Used in combination with a surface filter: |
|
||||
| |a mesh. |
|
||||
| | |
|
||||
| |When used in combination with a surface filter: |
|
||||
| |Net currents on any surface previously defined in |
|
||||
| |the geometry. It may be used along with any other |
|
||||
| |filter, except meshsurface filters. |
|
||||
| |Surfaces can alternatively be defined with cell |
|
||||
| |from and cell filters thereby resulting in tallying|
|
||||
| |partial currents. |
|
||||
| | |
|
||||
| |Units are particles per source particle. |
|
||||
+----------------------+---------------------------------------------------+
|
||||
|events |Number of scoring events. Units are events per |
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ extern int64_t ssw_cell_id; //!< Cell id for the surface source
|
|||
//!< write setting
|
||||
extern SSWCellType ssw_cell_type; //!< Type of option for the cell
|
||||
//!< argument of surface source write
|
||||
extern double surface_grazing_cutoff; //!< surface flux cosine cutoff
|
||||
extern double surface_grazing_ratio; //!< surface flux substitution ratio
|
||||
extern TemperatureMethod
|
||||
temperature_method; //!< method for choosing temperatures
|
||||
extern double
|
||||
|
|
|
|||
|
|
@ -101,11 +101,19 @@ void score_tracklength_tally(Particle& p, double distance);
|
|||
//! \param total_distance The distance in [cm] traveled by the particle
|
||||
void score_timed_tracklength_tally(Particle& p, double total_distance);
|
||||
|
||||
//! Score surface or mesh-surface tallies for particle currents.
|
||||
//! Score mesh-surface tallies for particle currents.
|
||||
//
|
||||
//! \param p The particle being tracked
|
||||
//! \param tallies A vector of the indices of the tallies to score to
|
||||
void score_surface_tally(Particle& p, const vector<int>& tallies);
|
||||
void score_meshsurface_tally(Particle& p, const vector<int>& tallies);
|
||||
|
||||
//! Score surface tallies for particle currents.
|
||||
//
|
||||
//! \param p The particle being tracked
|
||||
//! \param tallies A vector of the indices of the tallies to score to
|
||||
//! \param surf The surface being crossed
|
||||
void score_surface_tally(
|
||||
Particle& p, const vector<int>& tallies, const Surface& surf);
|
||||
|
||||
//! Score the pulse-height tally
|
||||
//! This is triggered at the end of every particle history
|
||||
|
|
|
|||
|
|
@ -289,6 +289,14 @@ class Settings:
|
|||
:cellto: Cell ID used to determine if particles crossing identified
|
||||
surfaces are to be banked. Particles going to this declared
|
||||
cell will be banked (int)
|
||||
surface_grazing_cutoff : float
|
||||
Surface flux cosine cutoff. If not specified, the default value is
|
||||
0.001. For more information, see the surface tally section in the theory
|
||||
manual.
|
||||
surface_grazing_ratio : float
|
||||
Surface flux cosine substitution ratio. If not specified, the default
|
||||
value is 0.5. For more information, see the surface tally section in the
|
||||
theory manual.
|
||||
survival_biasing : bool
|
||||
Indicate whether survival biasing is to be used
|
||||
tabular_legendre : dict
|
||||
|
|
@ -399,6 +407,8 @@ class Settings:
|
|||
self._uniform_source_sampling = None
|
||||
self._seed = None
|
||||
self._stride = None
|
||||
self._surface_grazing_cutoff = None
|
||||
self._surface_grazing_ratio = None
|
||||
self._survival_biasing = None
|
||||
self._free_gas_threshold = None
|
||||
|
||||
|
|
@ -710,6 +720,27 @@ class Settings:
|
|||
cv.check_greater_than('random number generator stride', stride, 0)
|
||||
self._stride = stride
|
||||
|
||||
@property
|
||||
def surface_grazing_cutoff(self) -> float:
|
||||
return self._surface_grazing_cutoff
|
||||
|
||||
@surface_grazing_cutoff.setter
|
||||
def surface_grazing_cutoff(self, surface_grazing_cutoff: float):
|
||||
cv.check_type('surface grazing cutoff', surface_grazing_cutoff, float)
|
||||
cv.check_greater_than('surface grazing cutoff', surface_grazing_cutoff, 0.0)
|
||||
cv.check_less_than('surface grazing cutoff', surface_grazing_cutoff, 1.0)
|
||||
self._surface_grazing_cutoff = surface_grazing_cutoff
|
||||
|
||||
@property
|
||||
def surface_grazing_ratio(self) -> float:
|
||||
return self._surface_grazing_ratio
|
||||
|
||||
@surface_grazing_ratio.setter
|
||||
def surface_grazing_ratio(self, surface_grazing_ratio: float):
|
||||
cv.check_type('surface grazing ratio', surface_grazing_ratio, float)
|
||||
cv.check_greater_than('surface grazing ratio', surface_grazing_ratio, 0.0)
|
||||
self._surface_grazing_ratio = surface_grazing_ratio
|
||||
|
||||
@property
|
||||
def survival_biasing(self) -> bool:
|
||||
return self._survival_biasing
|
||||
|
|
@ -1625,6 +1656,16 @@ class Settings:
|
|||
element = ET.SubElement(root, "stride")
|
||||
element.text = str(self._stride)
|
||||
|
||||
def _create_surface_grazing_cutoff_subelement(self, root):
|
||||
if self._surface_grazing_cutoff is not None:
|
||||
element = ET.SubElement(root, "surface_grazing_cutoff")
|
||||
element.text = str(self._surface_grazing_cutoff)
|
||||
|
||||
def _create_surface_grazing_ratio_subelement(self, root):
|
||||
if self._surface_grazing_ratio is not None:
|
||||
element = ET.SubElement(root, "surface_grazing_ratio")
|
||||
element.text = str(self._surface_grazing_ratio)
|
||||
|
||||
def _create_survival_biasing_subelement(self, root):
|
||||
if self._survival_biasing is not None:
|
||||
element = ET.SubElement(root, "survival_biasing")
|
||||
|
|
@ -2128,6 +2169,16 @@ class Settings:
|
|||
if text is not None:
|
||||
self.stride = int(text)
|
||||
|
||||
def _surface_grazing_cutoff_from_xml_element(self, root):
|
||||
text = get_text(root, 'surface_grazing_cutoff')
|
||||
if text is not None:
|
||||
self.surface_grazing_cutoff = float(text)
|
||||
|
||||
def _surface_grazing_ratio_from_xml_element(self, root):
|
||||
text = get_text(root, 'surface_grazing_ratio')
|
||||
if text is not None:
|
||||
self.surface_grazing_ratio = float(text)
|
||||
|
||||
def _survival_biasing_from_xml_element(self, root):
|
||||
text = get_text(root, 'survival_biasing')
|
||||
if text is not None:
|
||||
|
|
@ -2435,6 +2486,8 @@ class Settings:
|
|||
self._create_ptables_subelement(element)
|
||||
self._create_seed_subelement(element)
|
||||
self._create_stride_subelement(element)
|
||||
self._create_surface_grazing_cutoff_subelement(element)
|
||||
self._create_surface_grazing_ratio_subelement(element)
|
||||
self._create_survival_biasing_subelement(element)
|
||||
self._create_cutoff_subelement(element)
|
||||
self._create_entropy_mesh_subelement(element, mesh_memo)
|
||||
|
|
@ -2549,6 +2602,8 @@ class Settings:
|
|||
settings._ptables_from_xml_element(elem)
|
||||
settings._seed_from_xml_element(elem)
|
||||
settings._stride_from_xml_element(elem)
|
||||
settings._surface_grazing_cutoff_from_xml_element(elem)
|
||||
settings._surface_grazing_ratio_from_xml_element(elem)
|
||||
settings._survival_biasing_from_xml_element(elem)
|
||||
settings._cutoff_from_xml_element(elem)
|
||||
settings._entropy_mesh_from_xml_element(elem, meshes)
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@ int openmc_finalize()
|
|||
settings::restart_run = false;
|
||||
settings::run_CE = true;
|
||||
settings::run_mode = RunMode::UNSET;
|
||||
settings::surface_grazing_cutoff = 0.001;
|
||||
settings::surface_grazing_ratio = 0.5;
|
||||
settings::solver_type = SolverType::MONTE_CARLO;
|
||||
settings::source_latest = false;
|
||||
settings::source_rejection_fraction = 0.05;
|
||||
|
|
|
|||
|
|
@ -302,6 +302,8 @@ void Particle::event_cross_surface()
|
|||
surface() = boundary().surface();
|
||||
n_coord() = boundary().coord_level();
|
||||
|
||||
const auto& surf {*model::surfaces[surface_index()].get()};
|
||||
|
||||
if (boundary().lattice_translation()[0] != 0 ||
|
||||
boundary().lattice_translation()[1] != 0 ||
|
||||
boundary().lattice_translation()[2] != 0) {
|
||||
|
|
@ -312,15 +314,14 @@ void Particle::event_cross_surface()
|
|||
event() = TallyEvent::LATTICE;
|
||||
} else {
|
||||
// Particle crosses surface
|
||||
const auto& surf {model::surfaces[surface_index()].get()};
|
||||
// If BC, add particle to surface source before crossing surface
|
||||
if (surf->surf_source_ && surf->bc_) {
|
||||
add_surf_source_to_bank(*this, *surf);
|
||||
if (surf.surf_source_ && surf.bc_) {
|
||||
add_surf_source_to_bank(*this, surf);
|
||||
}
|
||||
this->cross_surface(*surf);
|
||||
this->cross_surface(surf);
|
||||
// If no BC, add particle to surface source after crossing surface
|
||||
if (surf->surf_source_ && !surf->bc_) {
|
||||
add_surf_source_to_bank(*this, *surf);
|
||||
if (surf.surf_source_ && !surf.bc_) {
|
||||
add_surf_source_to_bank(*this, surf);
|
||||
}
|
||||
if (settings::weight_window_checkpoint_surface) {
|
||||
apply_weight_windows(*this);
|
||||
|
|
@ -329,7 +330,7 @@ void Particle::event_cross_surface()
|
|||
}
|
||||
// Score cell to cell partial currents
|
||||
if (!model::active_surface_tallies.empty()) {
|
||||
score_surface_tally(*this, model::active_surface_tallies);
|
||||
score_surface_tally(*this, model::active_surface_tallies, surf);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -346,7 +347,7 @@ void Particle::event_collide()
|
|||
// pre-collision direction to figure out what mesh surfaces were crossed
|
||||
|
||||
if (!model::active_meshsurf_tallies.empty())
|
||||
score_surface_tally(*this, model::active_meshsurf_tallies);
|
||||
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
|
||||
|
||||
// Clear surface component
|
||||
surface() = SURFACE_NONE;
|
||||
|
|
@ -648,7 +649,7 @@ void Particle::cross_vacuum_bc(const Surface& surf)
|
|||
// physically moving the particle forward slightly
|
||||
|
||||
r() += TINY_BIT * u();
|
||||
score_surface_tally(*this, model::active_meshsurf_tallies);
|
||||
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
|
||||
}
|
||||
|
||||
// Score to global leakage tally
|
||||
|
|
@ -680,13 +681,13 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
|
|||
// with a mesh boundary
|
||||
|
||||
if (!model::active_surface_tallies.empty()) {
|
||||
score_surface_tally(*this, model::active_surface_tallies);
|
||||
score_surface_tally(*this, model::active_surface_tallies, surf);
|
||||
}
|
||||
|
||||
if (!model::active_meshsurf_tallies.empty()) {
|
||||
Position r {this->r()};
|
||||
this->r() -= TINY_BIT * u();
|
||||
score_surface_tally(*this, model::active_meshsurf_tallies);
|
||||
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
|
||||
this->r() = r;
|
||||
}
|
||||
|
||||
|
|
@ -736,7 +737,7 @@ void Particle::cross_periodic_bc(
|
|||
if (!model::active_meshsurf_tallies.empty()) {
|
||||
Position r {this->r()};
|
||||
this->r() -= TINY_BIT * u();
|
||||
score_surface_tally(*this, model::active_meshsurf_tallies);
|
||||
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
|
||||
this->r() = r;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ int64_t ssw_max_particles;
|
|||
int64_t ssw_max_files;
|
||||
int64_t ssw_cell_id {C_NONE};
|
||||
SSWCellType ssw_cell_type {SSWCellType::None};
|
||||
double surface_grazing_cutoff {0.001};
|
||||
double surface_grazing_ratio {0.5};
|
||||
TemperatureMethod temperature_method {TemperatureMethod::NEAREST};
|
||||
double temperature_tolerance {10.0};
|
||||
double temperature_default {293.6};
|
||||
|
|
@ -678,6 +680,14 @@ void read_settings_xml(pugi::xml_node root)
|
|||
free_gas_threshold = std::stod(get_node_value(root, "free_gas_threshold"));
|
||||
}
|
||||
|
||||
// Surface grazing
|
||||
if (check_for_node(root, "surface_grazing_cutoff"))
|
||||
surface_grazing_cutoff =
|
||||
std::stod(get_node_value(root, "surface_grazing_cutoff"));
|
||||
if (check_for_node(root, "surface_grazing_ratio"))
|
||||
surface_grazing_ratio =
|
||||
std::stod(get_node_value(root, "surface_grazing_ratio"));
|
||||
|
||||
// Survival biasing
|
||||
if (check_for_node(root, "survival_biasing")) {
|
||||
survival_biasing = get_node_value_bool(root, "survival_biasing");
|
||||
|
|
|
|||
|
|
@ -52,11 +52,7 @@ void SurfaceFilter::get_all_bins(
|
|||
auto search = map_.find(p.surface_index());
|
||||
if (search != map_.end()) {
|
||||
match.bins_.push_back(search->second);
|
||||
if (p.surface() < 0) {
|
||||
match.weights_.push_back(-1.0);
|
||||
} else {
|
||||
match.weights_.push_back(1.0);
|
||||
}
|
||||
match.weights_.push_back(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -540,6 +540,8 @@ void Tally::set_scores(const vector<std::string>& scores)
|
|||
bool legendre_present = false;
|
||||
bool cell_present = false;
|
||||
bool cellfrom_present = false;
|
||||
bool material_present = false;
|
||||
bool materialfrom_present = false;
|
||||
bool surface_present = false;
|
||||
bool meshsurface_present = false;
|
||||
bool non_cell_energy_present = false;
|
||||
|
|
@ -556,12 +558,21 @@ void Tally::set_scores(const vector<std::string>& scores)
|
|||
cellfrom_present = true;
|
||||
} else if (filt->type() == FilterType::CELL) {
|
||||
cell_present = true;
|
||||
} else if (filt->type() == FilterType::MATERIALFROM) {
|
||||
materialfrom_present = true;
|
||||
} else if (filt->type() == FilterType::MATERIAL) {
|
||||
material_present = true;
|
||||
} else if (filt->type() == FilterType::SURFACE) {
|
||||
surface_present = true;
|
||||
} else if (filt->type() == FilterType::MESH_SURFACE) {
|
||||
meshsurface_present = true;
|
||||
}
|
||||
}
|
||||
bool surface_types_present =
|
||||
(surface_present || cellfrom_present || materialfrom_present);
|
||||
bool non_meshsurface_types_present =
|
||||
(surface_present || cell_present || cellfrom_present || material_present ||
|
||||
materialfrom_present);
|
||||
|
||||
// Iterate over the given scores.
|
||||
for (auto score_str : scores) {
|
||||
|
|
@ -583,6 +594,12 @@ void Tally::set_scores(const vector<std::string>& scores)
|
|||
fatal_error("Cannot tally flux for an individual nuclide.");
|
||||
if (energyout_present)
|
||||
fatal_error("Cannot tally flux with an outgoing energy filter.");
|
||||
if (surface_types_present) {
|
||||
if (meshsurface_present)
|
||||
fatal_error("OpenMC does not support mesh surface fluxes yet");
|
||||
type_ = TallyType::SURFACE;
|
||||
estimator_ = TallyEstimator::ANALOG;
|
||||
}
|
||||
break;
|
||||
|
||||
case SCORE_TOTAL:
|
||||
|
|
@ -615,16 +632,14 @@ void Tally::set_scores(const vector<std::string>& scores)
|
|||
|
||||
case SCORE_CURRENT:
|
||||
// Check which type of current is desired: mesh or surface currents.
|
||||
if (surface_present || cell_present || cellfrom_present) {
|
||||
if (meshsurface_present)
|
||||
if (meshsurface_present) {
|
||||
if (non_meshsurface_types_present)
|
||||
fatal_error("Cannot tally mesh surface currents in the same tally as "
|
||||
"normal surface currents");
|
||||
type_ = TallyType::SURFACE;
|
||||
estimator_ = TallyEstimator::ANALOG;
|
||||
} else if (meshsurface_present) {
|
||||
type_ = TallyType::MESH_SURFACE;
|
||||
} else {
|
||||
fatal_error("Cannot tally currents without surface type filters");
|
||||
type_ = TallyType::SURFACE;
|
||||
estimator_ = TallyEstimator::ANALOG;
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -681,15 +696,20 @@ void Tally::set_scores(const vector<std::string>& scores)
|
|||
"in multi-group mode");
|
||||
}
|
||||
|
||||
// Make sure current scores are not mixed in with volumetric scores.
|
||||
if (type_ == TallyType::SURFACE || type_ == TallyType::MESH_SURFACE) {
|
||||
if (scores_.size() != 1)
|
||||
fatal_error("Cannot tally other scores in the same tally as surface "
|
||||
"currents.");
|
||||
// Make sure mesh surface tallies contain only current score.
|
||||
if (meshsurface_present) {
|
||||
if ((scores_[0] != SCORE_CURRENT) || (scores_.size() > 1))
|
||||
fatal_error("Cannot tally score other than 'current' when using a "
|
||||
"mesh-surface filter.");
|
||||
}
|
||||
|
||||
// Make sure surface tallies contain only surface type scores score.
|
||||
if (type_ == TallyType::SURFACE) {
|
||||
for (auto sc : scores_)
|
||||
if ((sc != SCORE_CURRENT) && (sc != SCORE_FLUX))
|
||||
fatal_error("Cannot tally scores other than 'current' or 'flux' "
|
||||
"when using surface filters.");
|
||||
}
|
||||
if ((surface_present || meshsurface_present) && scores_[0] != SCORE_CURRENT)
|
||||
fatal_error("Cannot tally score other than 'current' when using a surface "
|
||||
"or mesh-surface filter.");
|
||||
}
|
||||
|
||||
void Tally::set_nuclides(pugi::xml_node node)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "openmc/settings.h"
|
||||
#include "openmc/simulation.h"
|
||||
#include "openmc/string_utils.h"
|
||||
#include "openmc/surface.h"
|
||||
#include "openmc/tallies/derivative.h"
|
||||
#include "openmc/tallies/filter.h"
|
||||
#include "openmc/tallies/filter_cell.h"
|
||||
|
|
@ -2610,7 +2611,7 @@ void score_collision_tally(Particle& p)
|
|||
match.bins_present_ = false;
|
||||
}
|
||||
|
||||
void score_surface_tally(Particle& p, const vector<int>& tallies)
|
||||
void score_meshsurface_tally(Particle& p, const vector<int>& tallies)
|
||||
{
|
||||
double current = p.wgt_last();
|
||||
|
||||
|
|
@ -2654,6 +2655,69 @@ void score_surface_tally(Particle& p, const vector<int>& tallies)
|
|||
match.bins_present_ = false;
|
||||
}
|
||||
|
||||
void score_surface_tally(
|
||||
Particle& p, const vector<int>& tallies, const Surface& surf)
|
||||
{
|
||||
double wgt = p.wgt_last();
|
||||
|
||||
// Sign for net current: +1 if crossing outward (in direction of normal),
|
||||
// -1 if crossing inward
|
||||
double current_sign = (p.surface() > 0) ? 1.0 : -1.0;
|
||||
|
||||
// Determine absolute cosine of angle between particle direction and surface
|
||||
// normal, needed for the surface-crossing flux estimator.
|
||||
auto n = surf.normal(p.r());
|
||||
n /= n.norm();
|
||||
double abs_mu = std::min(std::abs(p.u().dot(n)), 1.0);
|
||||
if (abs_mu < settings::surface_grazing_cutoff)
|
||||
abs_mu = settings::surface_grazing_ratio * settings::surface_grazing_cutoff;
|
||||
|
||||
for (auto i_tally : tallies) {
|
||||
auto& tally {*model::tallies[i_tally]};
|
||||
|
||||
// Initialize an iterator over valid filter bin combinations. If there are
|
||||
// no valid combinations, use a continue statement to ensure we skip the
|
||||
// assume_separate break below.
|
||||
auto filter_iter = FilterBinIter(tally, p);
|
||||
auto end = FilterBinIter(tally, true, &p.filter_matches());
|
||||
if (filter_iter == end)
|
||||
continue;
|
||||
|
||||
// Loop over filter bins.
|
||||
for (; filter_iter != end; ++filter_iter) {
|
||||
auto filter_index = filter_iter.index_;
|
||||
auto filter_weight = filter_iter.weight_;
|
||||
|
||||
// Loop over scores.
|
||||
for (auto score_index = 0; score_index < tally.scores_.size();
|
||||
++score_index) {
|
||||
auto score_bin = tally.scores_[score_index];
|
||||
double score;
|
||||
if (score_bin == SCORE_CURRENT) {
|
||||
// Net current: weight carries the sign of the crossing direction.
|
||||
score = wgt * current_sign;
|
||||
} else {
|
||||
// SCORE_FLUX: surface-crossing estimator phi_S = sum(w / |mu|).
|
||||
score = wgt / abs_mu;
|
||||
}
|
||||
#pragma omp atomic
|
||||
tally.results_(filter_index, score_index, TallyResult::VALUE) +=
|
||||
score * filter_weight;
|
||||
}
|
||||
}
|
||||
// If the user has specified that we can assume all tallies are spatially
|
||||
// separate, this implies that once a tally has been scored to, we needn't
|
||||
// check the others. This cuts down on overhead when there are many
|
||||
// tallies specified
|
||||
if (settings::assume_separate)
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset all the filter matches for the next tally event.
|
||||
for (auto& match : p.filter_matches())
|
||||
match.bins_present_ = false;
|
||||
}
|
||||
|
||||
void score_pulse_height_tally(Particle& p, const vector<int>& tallies)
|
||||
{
|
||||
// The pulse height tally in OpenMC hijacks the logic of CellFilter and
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
</filter>
|
||||
<tally id="1">
|
||||
<filters>1 2</filters>
|
||||
<scores>current</scores>
|
||||
<scores>current flux</scores>
|
||||
</tally>
|
||||
</tallies>
|
||||
</model>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ tally 1:
|
|||
0.000000E+00
|
||||
0.000000E+00
|
||||
0.000000E+00
|
||||
0.000000E+00
|
||||
0.000000E+00
|
||||
0.000000E+00
|
||||
0.000000E+00
|
||||
9.230000E-01
|
||||
1.791510E-01
|
||||
3.294304E+00
|
||||
2.314403E+00
|
||||
3.869000E+00
|
||||
3.002523E+00
|
||||
5.154125E+00
|
||||
5.314334E+00
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import numpy as np
|
||||
from math import pi
|
||||
|
||||
import openmc
|
||||
import pytest
|
||||
|
||||
|
|
@ -32,7 +29,7 @@ def model():
|
|||
mu_filter = openmc.MuSurfaceFilter([-1.0, -0.5, 0.0, 0.5, 1.0])
|
||||
tally = openmc.Tally()
|
||||
tally.filters = [surf_filter, mu_filter]
|
||||
tally.scores = ['current']
|
||||
tally.scores = ['current', 'flux']
|
||||
model.tallies.append(tally)
|
||||
|
||||
return model
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ mean,std. dev.
|
|||
5.5000000e-03,1.0979779e-03
|
||||
1.2500000e-02,1.5438048e-03
|
||||
4.4700000e-02,1.9035055e-03
|
||||
7.3000000e-03,9.8938814e-04
|
||||
3.6600000e-02,2.5086517e-03
|
||||
4.1600000e-02,2.4864075e-03
|
||||
4.2380000e-01,9.6087923e-03
|
||||
1.0000000e-04,1.0000000e-04
|
||||
1.5000000e-03,4.5338235e-04
|
||||
3.0000000e-04,1.5275252e-04
|
||||
1.7300000e-02,1.4609738e-03
|
||||
-7.3000000e-03,9.8938814e-04
|
||||
-3.6600000e-02,2.5086517e-03
|
||||
-4.1600000e-02,2.4864075e-03
|
||||
-4.2380000e-01,9.6087923e-03
|
||||
-1.0000000e-04,1.0000000e-04
|
||||
-1.5000000e-03,4.5338235e-04
|
||||
-3.0000000e-04,1.5275252e-04
|
||||
-1.7300000e-02,1.4609738e-03
|
||||
-7.3000000e-03,9.8938814e-04
|
||||
-3.6600000e-02,2.5086517e-03
|
||||
-4.1600000e-02,2.4864075e-03
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ class SurfaceTallyTestHarness(PyAPITestHarness):
|
|||
# Extract the relevant data as a CSV string.
|
||||
cols = ('mean', 'std. dev.')
|
||||
return df.to_csv(None, columns=cols, index=False, float_format='%.7e')
|
||||
return outstr
|
||||
|
||||
|
||||
def test_surface_tally():
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ def test_export_to_xml(run_in_tmpdir):
|
|||
s.statepoint = {'batches': [50, 150, 500, 1000]}
|
||||
s.surf_source_read = {'path': 'surface_source_1.h5'}
|
||||
s.surf_source_write = {'surface_ids': [2], 'max_particles': 200}
|
||||
s.surface_grazing_ratio = 0.7
|
||||
s.surface_grazing_cutoff = 0.1
|
||||
s.confidence_intervals = True
|
||||
s.ptables = True
|
||||
s.plot_seed = 100
|
||||
|
|
@ -107,6 +109,8 @@ def test_export_to_xml(run_in_tmpdir):
|
|||
assert s.statepoint == {'batches': [50, 150, 500, 1000]}
|
||||
assert s.surf_source_read['path'].name == 'surface_source_1.h5'
|
||||
assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200}
|
||||
assert s.surface_grazing_ratio == 0.7
|
||||
assert s.surface_grazing_cutoff == 0.1
|
||||
assert s.confidence_intervals
|
||||
assert s.ptables
|
||||
assert s.plot_seed == 100
|
||||
|
|
|
|||
120
tests/unit_tests/test_surface_flux.py
Normal file
120
tests/unit_tests/test_surface_flux.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Tests for surface flux tallying via flux score + SurfaceFilter."""
|
||||
|
||||
import math
|
||||
import pytest
|
||||
|
||||
import openmc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_cell_model():
|
||||
"""Simple two-cell slab model with a monodirectional fixed source.
|
||||
|
||||
Cell1 occupies x in [-10, 0], cell2 x in [0, 10]. The source fires all
|
||||
particles from (-5, 0, 0) in the +x direction with weight 1. Every
|
||||
particle therefore crosses the surface at x=0 from cell1 into cell2 at
|
||||
mu = 1 (normal incidence).
|
||||
"""
|
||||
openmc.reset_auto_ids()
|
||||
model = openmc.Model()
|
||||
|
||||
xmin = openmc.XPlane(-10.0, boundary_type="vacuum")
|
||||
xmid = openmc.XPlane(0.0)
|
||||
xmax = openmc.XPlane(10.0, boundary_type="vacuum")
|
||||
ymin = openmc.YPlane(-10.0, boundary_type="vacuum")
|
||||
ymax = openmc.YPlane(10.0, boundary_type="vacuum")
|
||||
zmin = openmc.ZPlane(-10.0, boundary_type="vacuum")
|
||||
zmax = openmc.ZPlane(10.0, boundary_type="vacuum")
|
||||
|
||||
cell1 = openmc.Cell(region=+xmin & -xmid & +ymin & -ymax & +zmin & -zmax)
|
||||
cell2 = openmc.Cell(region=+xmid & -xmax & +ymin & -ymax & +zmin & -zmax)
|
||||
model.geometry = openmc.Geometry([cell1, cell2])
|
||||
|
||||
src = openmc.IndependentSource()
|
||||
src.space = openmc.stats.Point((-5.0, 0.0, 0.0))
|
||||
src.angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0))
|
||||
|
||||
model.settings.run_mode = 'fixed source'
|
||||
model.settings.batches = 1
|
||||
model.settings.particles = 100
|
||||
model.settings.source = src
|
||||
|
||||
return model, xmid, cell1, cell2
|
||||
|
||||
|
||||
def test_surface_filter_flux_normal_incidence(two_cell_model, run_in_tmpdir):
|
||||
"""SurfaceFilter + flux at mu=1 gives w/|mu| = 1.0 per source particle."""
|
||||
model, xmid, *_ = two_cell_model
|
||||
|
||||
surf_filter = openmc.SurfaceFilter([xmid])
|
||||
flux_tally = openmc.Tally()
|
||||
flux_tally.filters = [surf_filter]
|
||||
flux_tally.scores = ['flux']
|
||||
model.tallies = [flux_tally]
|
||||
|
||||
model.run(apply_tally_results=True)
|
||||
flux_mean = flux_tally.mean.flat[0]
|
||||
|
||||
# Every particle crosses at mu=1 with weight 1, so flux = 1.0
|
||||
assert flux_mean == pytest.approx(1.0, rel=1e-8)
|
||||
|
||||
|
||||
def test_surface_filter_current_outward(two_cell_model, run_in_tmpdir):
|
||||
"""SurfaceFilter + current gives +1.0 for purely outward crossings."""
|
||||
model, xmid, *_ = two_cell_model
|
||||
|
||||
surf_filter = openmc.SurfaceFilter([xmid])
|
||||
current_tally = openmc.Tally()
|
||||
current_tally.filters = [surf_filter]
|
||||
current_tally.scores = ['current']
|
||||
model.tallies = [current_tally]
|
||||
|
||||
model.run(apply_tally_results=True)
|
||||
current_mean = current_tally.mean.flat[0]
|
||||
|
||||
# All crossings are outward → net current = +1.0
|
||||
assert current_mean == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_surface_filter_flux_angled(two_cell_model, run_in_tmpdir):
|
||||
"""Surface flux at 60-degree incidence gives w/|mu| = 2.0."""
|
||||
model, xmid, *_ = two_cell_model
|
||||
|
||||
# Modify source to use 60-degree angle from normal: mu = cos(60°) = 0.5
|
||||
mu = ux = 0.5
|
||||
uy = math.sqrt(1.0 - ux**2)
|
||||
model.settings.source[0].angle = openmc.stats.Monodirectional((ux, uy, 0.0))
|
||||
|
||||
surf_filter = openmc.SurfaceFilter([xmid])
|
||||
flux_tally = openmc.Tally()
|
||||
flux_tally.filters = [surf_filter]
|
||||
flux_tally.scores = ['flux']
|
||||
model.tallies = [flux_tally]
|
||||
|
||||
model.run(apply_tally_results=True)
|
||||
flux_mean = flux_tally.mean.flat[0]
|
||||
|
||||
# flux = w/|mu| = 1/0.5 = 2.0
|
||||
assert flux_mean == pytest.approx(1.0 / mu)
|
||||
|
||||
|
||||
def test_cellfrom_filter_flux_directional(two_cell_model, run_in_tmpdir):
|
||||
"""SurfaceFilter + CellFromFilter + flux scores only the correct direction."""
|
||||
model, xmid, cell1, cell2 = two_cell_model
|
||||
|
||||
surf_filter = openmc.SurfaceFilter([xmid])
|
||||
cellfrom_filter = openmc.CellFromFilter([cell1, cell2])
|
||||
|
||||
tally = openmc.Tally()
|
||||
tally.filters = [surf_filter, cellfrom_filter]
|
||||
tally.scores = ['flux']
|
||||
|
||||
model.tallies = [tally]
|
||||
model.run(apply_tally_results=True)
|
||||
mean_from1 = tally.mean.flat[0]
|
||||
mean_from2 = tally.mean.flat[1]
|
||||
|
||||
# All particles cross xmid from cell1 at mu=1 → flux = 1.0
|
||||
assert mean_from1 == pytest.approx(1.0)
|
||||
# No particles cross xmid from cell2 → flux = 0
|
||||
assert mean_from2 == pytest.approx(0.0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue