Skip to content

Commit 70be650

Browse files
GuyStenpaulromano
andauthored
Implement surface flux tallies (#3742)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent b796afb commit 70be650

19 files changed

Lines changed: 424 additions & 54 deletions

File tree

docs/source/io_formats/settings.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,23 @@ attributes/sub-elements:
12341234
are not eligible to store any particles when using ``cell``, ``cellfrom``
12351235
or ``cellto`` attributes. It is recommended to use surface IDs instead.
12361236

1237+
------------------------------------
1238+
``<surface_grazing_cutoff>`` Element
1239+
------------------------------------
1240+
1241+
The ``<surface_grazing_cutoff>`` element specifies the surface flux cosine cutoff.
1242+
1243+
*Default*: 0.001
1244+
1245+
-----------------------------------
1246+
``<surface_grazing_ratio>`` Element
1247+
-----------------------------------
1248+
1249+
The ``<surface_grazing_ratio>`` element specifies the surface flux cosine
1250+
substitution ratio.
1251+
1252+
*Default*: 0.5
1253+
12371254
------------------------------
12381255
``<survival_biasing>`` Element
12391256
------------------------------

docs/source/methods/tallies.rst

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,71 @@ had a collision at every event. Thus, for tallies with outgoing-energy filters
205205
or for tallies of scattering moments (which require the scattering cosine of
206206
the change-in-angle), we must use an analog estimator.
207207

208-
.. TODO: Add description of surface current tallies
208+
-----------------------------------
209+
Surface-Integrated Flux and Current
210+
-----------------------------------
211+
212+
Surface tallies allow you to measure particle behavior as they cross specific
213+
boundaries in your geometry. Unlike volume tallies, which integrate over a
214+
volumetric region, surface tallies capture the current or flux passing through a
215+
surface. Surface tallies are estimated using an analog estimator.
216+
217+
Current Score
218+
-------------
219+
220+
When tallying the current across a surface, we simply count the weight of
221+
particles that cross the surface of interest:
222+
223+
224+
.. math::
225+
:label: analog-current-estimator
226+
227+
J = \frac{1}{W} \sum_{i \in S} w_i.
228+
229+
where :math:`J` is the area-integrated current passing through surface
230+
:math:`S`, :math:`W` is the total starting weight of the particles, and
231+
:math:`w_i` is the weight of the particle as it crosses the surface :math:`S`.
232+
233+
Flux Score
234+
----------
235+
236+
When tallying flux over a surface, we use the relationship between current and
237+
flux:
238+
239+
240+
.. math::
241+
:label: surface-flux-estimator
242+
243+
\phi_S = \frac{1}{W} \sum_{i \in S} \frac{w_i}{|\mu|}.
244+
245+
where :math:`\phi_S` is the area-integrated flux over surface :math:`S`,
246+
:math:`W` is the total starting weight of the particles, :math:`w_i` is the
247+
weight of the particle as it crosses the surface :math:`S` and :math:`\mu` is
248+
the cosine of angle between the particle direction and the surface normal.
249+
250+
This equation diverges when the particle crossing the surface is nearly parallel
251+
to it (that is, as :math:`\mu` approaches zero). To remove this divergence,
252+
OpenMC scores:
253+
254+
.. math::
255+
:label: modified-surface-flux-estimator
256+
257+
\phi_S = \frac{1}{W} \sum_{i \in S} w_i f(\mu).
258+
259+
and the function :math:`f` is defined by:
260+
261+
.. math::
262+
f(\mu) = \begin{cases}
263+
\frac{1}{|\mu|} & |\mu| > \mu_\text{cut} \\
264+
\frac{1}{c\mu_\text{cut}} & |\mu| \le \mu_\text{cut}
265+
\end{cases}
266+
267+
where :math:`\mu_\text{cut}` is the grazing cosine cutoff and :math:`c` is the
268+
cosine substitution ratio. The parameters :math:`\mu_\text{cut}` and :math:`c`
269+
can be set by the user via the :attr:`openmc.Settings.surface_grazing_cutoff`
270+
and :attr:`openmc.Settings.surface_grazing_ratio` attributes, respectively. The
271+
default values for these parameters are 0.001 and 0.5 as recommended by
272+
`Favorite, Thomas, and Booth <https://doi.org/10.13182/NSE09-72>`_.
209273

210274
.. _tallies_statistics:
211275

docs/source/usersguide/tallies.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,18 +261,21 @@ The following tables show all valid scores:
261261
+----------------------+---------------------------------------------------+
262262
|Score | Description |
263263
+======================+===================================================+
264-
|current |Used in combination with a meshsurface filter: |
264+
|current |It may not be used in conjunction with any other |
265+
| |score except flux. |
266+
| | |
267+
| |When used in combination with a meshsurface filter:|
265268
| |Partial currents on the boundaries of each cell in |
266-
| |a mesh. It may not be used in conjunction with any |
267-
| |other score. Only energy and mesh filters may be |
268-
| |used. |
269-
| |Used in combination with a surface filter: |
269+
| |a mesh. |
270+
| | |
271+
| |When used in combination with a surface filter: |
270272
| |Net currents on any surface previously defined in |
271273
| |the geometry. It may be used along with any other |
272274
| |filter, except meshsurface filters. |
273275
| |Surfaces can alternatively be defined with cell |
274276
| |from and cell filters thereby resulting in tallying|
275277
| |partial currents. |
278+
| | |
276279
| |Units are particles per source particle. |
277280
+----------------------+---------------------------------------------------+
278281
|events |Number of scoring events. Units are events per |

include/openmc/settings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ extern int64_t ssw_cell_id; //!< Cell id for the surface source
181181
//!< write setting
182182
extern SSWCellType ssw_cell_type; //!< Type of option for the cell
183183
//!< argument of surface source write
184+
extern double surface_grazing_cutoff; //!< surface flux cosine cutoff
185+
extern double surface_grazing_ratio; //!< surface flux substitution ratio
184186
extern TemperatureMethod
185187
temperature_method; //!< method for choosing temperatures
186188
extern double

include/openmc/tallies/tally_scoring.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,19 @@ void score_tracklength_tally(Particle& p, double distance);
101101
//! \param total_distance The distance in [cm] traveled by the particle
102102
void score_timed_tracklength_tally(Particle& p, double total_distance);
103103

104-
//! Score surface or mesh-surface tallies for particle currents.
104+
//! Score mesh-surface tallies for particle currents.
105105
//
106106
//! \param p The particle being tracked
107107
//! \param tallies A vector of the indices of the tallies to score to
108-
void score_surface_tally(Particle& p, const vector<int>& tallies);
108+
void score_meshsurface_tally(Particle& p, const vector<int>& tallies);
109+
110+
//! Score surface tallies for particle currents.
111+
//
112+
//! \param p The particle being tracked
113+
//! \param tallies A vector of the indices of the tallies to score to
114+
//! \param surf The surface being crossed
115+
void score_surface_tally(
116+
Particle& p, const vector<int>& tallies, const Surface& surf);
109117

110118
//! Score the pulse-height tally
111119
//! This is triggered at the end of every particle history

openmc/settings.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,14 @@ class Settings:
289289
:cellto: Cell ID used to determine if particles crossing identified
290290
surfaces are to be banked. Particles going to this declared
291291
cell will be banked (int)
292+
surface_grazing_cutoff : float
293+
Surface flux cosine cutoff. If not specified, the default value is
294+
0.001. For more information, see the surface tally section in the theory
295+
manual.
296+
surface_grazing_ratio : float
297+
Surface flux cosine substitution ratio. If not specified, the default
298+
value is 0.5. For more information, see the surface tally section in the
299+
theory manual.
292300
survival_biasing : bool
293301
Indicate whether survival biasing is to be used
294302
tabular_legendre : dict
@@ -399,6 +407,8 @@ def __init__(self, **kwargs):
399407
self._uniform_source_sampling = None
400408
self._seed = None
401409
self._stride = None
410+
self._surface_grazing_cutoff = None
411+
self._surface_grazing_ratio = None
402412
self._survival_biasing = None
403413
self._free_gas_threshold = None
404414

@@ -710,6 +720,27 @@ def stride(self, stride: int):
710720
cv.check_greater_than('random number generator stride', stride, 0)
711721
self._stride = stride
712722

723+
@property
724+
def surface_grazing_cutoff(self) -> float:
725+
return self._surface_grazing_cutoff
726+
727+
@surface_grazing_cutoff.setter
728+
def surface_grazing_cutoff(self, surface_grazing_cutoff: float):
729+
cv.check_type('surface grazing cutoff', surface_grazing_cutoff, float)
730+
cv.check_greater_than('surface grazing cutoff', surface_grazing_cutoff, 0.0)
731+
cv.check_less_than('surface grazing cutoff', surface_grazing_cutoff, 1.0)
732+
self._surface_grazing_cutoff = surface_grazing_cutoff
733+
734+
@property
735+
def surface_grazing_ratio(self) -> float:
736+
return self._surface_grazing_ratio
737+
738+
@surface_grazing_ratio.setter
739+
def surface_grazing_ratio(self, surface_grazing_ratio: float):
740+
cv.check_type('surface grazing ratio', surface_grazing_ratio, float)
741+
cv.check_greater_than('surface grazing ratio', surface_grazing_ratio, 0.0)
742+
self._surface_grazing_ratio = surface_grazing_ratio
743+
713744
@property
714745
def survival_biasing(self) -> bool:
715746
return self._survival_biasing
@@ -1625,6 +1656,16 @@ def _create_stride_subelement(self, root):
16251656
element = ET.SubElement(root, "stride")
16261657
element.text = str(self._stride)
16271658

1659+
def _create_surface_grazing_cutoff_subelement(self, root):
1660+
if self._surface_grazing_cutoff is not None:
1661+
element = ET.SubElement(root, "surface_grazing_cutoff")
1662+
element.text = str(self._surface_grazing_cutoff)
1663+
1664+
def _create_surface_grazing_ratio_subelement(self, root):
1665+
if self._surface_grazing_ratio is not None:
1666+
element = ET.SubElement(root, "surface_grazing_ratio")
1667+
element.text = str(self._surface_grazing_ratio)
1668+
16281669
def _create_survival_biasing_subelement(self, root):
16291670
if self._survival_biasing is not None:
16301671
element = ET.SubElement(root, "survival_biasing")
@@ -2128,6 +2169,16 @@ def _stride_from_xml_element(self, root):
21282169
if text is not None:
21292170
self.stride = int(text)
21302171

2172+
def _surface_grazing_cutoff_from_xml_element(self, root):
2173+
text = get_text(root, 'surface_grazing_cutoff')
2174+
if text is not None:
2175+
self.surface_grazing_cutoff = float(text)
2176+
2177+
def _surface_grazing_ratio_from_xml_element(self, root):
2178+
text = get_text(root, 'surface_grazing_ratio')
2179+
if text is not None:
2180+
self.surface_grazing_ratio = float(text)
2181+
21312182
def _survival_biasing_from_xml_element(self, root):
21322183
text = get_text(root, 'survival_biasing')
21332184
if text is not None:
@@ -2435,6 +2486,8 @@ def to_xml_element(self, mesh_memo=None):
24352486
self._create_ptables_subelement(element)
24362487
self._create_seed_subelement(element)
24372488
self._create_stride_subelement(element)
2489+
self._create_surface_grazing_cutoff_subelement(element)
2490+
self._create_surface_grazing_ratio_subelement(element)
24382491
self._create_survival_biasing_subelement(element)
24392492
self._create_cutoff_subelement(element)
24402493
self._create_entropy_mesh_subelement(element, mesh_memo)
@@ -2549,6 +2602,8 @@ def from_xml_element(cls, elem, meshes=None):
25492602
settings._ptables_from_xml_element(elem)
25502603
settings._seed_from_xml_element(elem)
25512604
settings._stride_from_xml_element(elem)
2605+
settings._surface_grazing_cutoff_from_xml_element(elem)
2606+
settings._surface_grazing_ratio_from_xml_element(elem)
25522607
settings._survival_biasing_from_xml_element(elem)
25532608
settings._cutoff_from_xml_element(elem)
25542609
settings._entropy_mesh_from_xml_element(elem, meshes)

src/finalize.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ int openmc_finalize()
123123
settings::restart_run = false;
124124
settings::run_CE = true;
125125
settings::run_mode = RunMode::UNSET;
126+
settings::surface_grazing_cutoff = 0.001;
127+
settings::surface_grazing_ratio = 0.5;
126128
settings::solver_type = SolverType::MONTE_CARLO;
127129
settings::source_latest = false;
128130
settings::source_rejection_fraction = 0.05;

src/particle.cpp

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ void Particle::event_cross_surface()
302302
surface() = boundary().surface();
303303
n_coord() = boundary().coord_level();
304304

305+
const auto& surf {*model::surfaces[surface_index()].get()};
306+
305307
if (boundary().lattice_translation()[0] != 0 ||
306308
boundary().lattice_translation()[1] != 0 ||
307309
boundary().lattice_translation()[2] != 0) {
@@ -312,15 +314,14 @@ void Particle::event_cross_surface()
312314
event() = TallyEvent::LATTICE;
313315
} else {
314316
// Particle crosses surface
315-
const auto& surf {model::surfaces[surface_index()].get()};
316317
// If BC, add particle to surface source before crossing surface
317-
if (surf->surf_source_ && surf->bc_) {
318-
add_surf_source_to_bank(*this, *surf);
318+
if (surf.surf_source_ && surf.bc_) {
319+
add_surf_source_to_bank(*this, surf);
319320
}
320-
this->cross_surface(*surf);
321+
this->cross_surface(surf);
321322
// If no BC, add particle to surface source after crossing surface
322-
if (surf->surf_source_ && !surf->bc_) {
323-
add_surf_source_to_bank(*this, *surf);
323+
if (surf.surf_source_ && !surf.bc_) {
324+
add_surf_source_to_bank(*this, surf);
324325
}
325326
if (settings::weight_window_checkpoint_surface) {
326327
apply_weight_windows(*this);
@@ -329,7 +330,7 @@ void Particle::event_cross_surface()
329330
}
330331
// Score cell to cell partial currents
331332
if (!model::active_surface_tallies.empty()) {
332-
score_surface_tally(*this, model::active_surface_tallies);
333+
score_surface_tally(*this, model::active_surface_tallies, surf);
333334
}
334335
}
335336

@@ -346,7 +347,7 @@ void Particle::event_collide()
346347
// pre-collision direction to figure out what mesh surfaces were crossed
347348

348349
if (!model::active_meshsurf_tallies.empty())
349-
score_surface_tally(*this, model::active_meshsurf_tallies);
350+
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
350351

351352
// Clear surface component
352353
surface() = SURFACE_NONE;
@@ -648,7 +649,7 @@ void Particle::cross_vacuum_bc(const Surface& surf)
648649
// physically moving the particle forward slightly
649650

650651
r() += TINY_BIT * u();
651-
score_surface_tally(*this, model::active_meshsurf_tallies);
652+
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
652653
}
653654

654655
// Score to global leakage tally
@@ -680,13 +681,13 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
680681
// with a mesh boundary
681682

682683
if (!model::active_surface_tallies.empty()) {
683-
score_surface_tally(*this, model::active_surface_tallies);
684+
score_surface_tally(*this, model::active_surface_tallies, surf);
684685
}
685686

686687
if (!model::active_meshsurf_tallies.empty()) {
687688
Position r {this->r()};
688689
this->r() -= TINY_BIT * u();
689-
score_surface_tally(*this, model::active_meshsurf_tallies);
690+
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
690691
this->r() = r;
691692
}
692693

@@ -736,7 +737,7 @@ void Particle::cross_periodic_bc(
736737
if (!model::active_meshsurf_tallies.empty()) {
737738
Position r {this->r()};
738739
this->r() -= TINY_BIT * u();
739-
score_surface_tally(*this, model::active_meshsurf_tallies);
740+
score_meshsurface_tally(*this, model::active_meshsurf_tallies);
740741
this->r() = r;
741742
}
742743

src/settings.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ int64_t ssw_max_particles;
136136
int64_t ssw_max_files;
137137
int64_t ssw_cell_id {C_NONE};
138138
SSWCellType ssw_cell_type {SSWCellType::None};
139+
double surface_grazing_cutoff {0.001};
140+
double surface_grazing_ratio {0.5};
139141
TemperatureMethod temperature_method {TemperatureMethod::NEAREST};
140142
double temperature_tolerance {10.0};
141143
double temperature_default {293.6};
@@ -678,6 +680,14 @@ void read_settings_xml(pugi::xml_node root)
678680
free_gas_threshold = std::stod(get_node_value(root, "free_gas_threshold"));
679681
}
680682

683+
// Surface grazing
684+
if (check_for_node(root, "surface_grazing_cutoff"))
685+
surface_grazing_cutoff =
686+
std::stod(get_node_value(root, "surface_grazing_cutoff"));
687+
if (check_for_node(root, "surface_grazing_ratio"))
688+
surface_grazing_ratio =
689+
std::stod(get_node_value(root, "surface_grazing_ratio"));
690+
681691
// Survival biasing
682692
if (check_for_node(root, "survival_biasing")) {
683693
survival_biasing = get_node_value_bool(root, "survival_biasing");

src/tallies/filter_surface.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,7 @@ void SurfaceFilter::get_all_bins(
5252
auto search = map_.find(p.surface_index());
5353
if (search != map_.end()) {
5454
match.bins_.push_back(search->second);
55-
if (p.surface() < 0) {
56-
match.weights_.push_back(-1.0);
57-
} else {
58-
match.weights_.push_back(1.0);
59-
}
55+
match.weights_.push_back(1.0);
6056
}
6157
}
6258

0 commit comments

Comments
 (0)