Random Ray Transport (#2823)

Co-authored-by: Gavin Ridley <gavin.keith.ridley@gmail.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
John Tramm 2024-04-18 17:10:16 -05:00 committed by GitHub
parent d53155d234
commit 5111aa2621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 4512 additions and 69 deletions

View file

@ -380,6 +380,9 @@ list(APPEND libopenmc_SOURCES
src/progress_bar.cpp
src/random_dist.cpp
src/random_lcg.cpp
src/random_ray/random_ray_simulation.cpp
src/random_ray/random_ray.cpp
src/random_ray/flat_source_domain.cpp
src/reaction.cpp
src/reaction_product.cpp
src/scattdata.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -413,6 +413,32 @@ or sub-elements and can be set to either "false" or "true".
.. note:: This element is not used in the multi-group :ref:`energy_mode`.
------------------------
``<random_ray>`` Element
------------------------
The ``<random_ray>`` element enables random ray mode and contains a number of
settings relevant to the solver. Tips for selecting these parameters can be
found in the :ref:`random ray user guide <random_ray>`.
:distance_inactive:
The inactive ray length (dead zone length) in [cm].
*Default*: None
:distance_active:
The active ray length in [cm].
*Default*: None
:source:
Specifies the starting ray distribution, and follows the format for
:ref:`source_element`. It must be uniform in space and angle and cover the
full domain. It does not represent a physical neutron or photon source -- it
is only used to sample integrating ray starting locations and directions.
*Default*: None
----------------------------------
``<resonance_scattering>`` Element
----------------------------------

View file

@ -20,3 +20,4 @@ Theory and Methodology
energy_deposition
parallelization
cmfd
random_ray

View file

@ -0,0 +1,804 @@
.. _methods_random_ray:
==========
Random Ray
==========
.. _methods_random_ray_intro:
-------------------
What is Random Ray?
-------------------
`Random ray <Tramm-2017a>`_ is a stochastic transport method, closely related to
the deterministic Method of Characteristics (MOC) [Askew-1972]_. Rather than
each ray representing a single neutron as in Monte Carlo, it represents a
characteristic line through the simulation geometry upon which the transport
equation can be written as an ordinary differential equation that can be solved
analytically (although with discretization required in energy, making it a
multigroup method). The behavior of the governing transport equation can be
approximated by solving along many characteristic tracks (rays) through the
system. Unlike particles in Monte Carlo, rays in random ray or MOC are not
affected by the material characteristics of the simulated problem---rays are
selected so as to explore the full simulation problem with a statistically equal
distribution in space and angle.
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/pHQq3FE4PDo?si=kPm9ngMBr95wLRGC" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
The above animation is an example of the random ray integration process at work,
showing a series of random rays being sampled and transported through the
geometry. In the following sections, we will discuss how the random ray solver
works.
----------------------------------------------
Why is a Random Ray Solver Included in OpenMC?
----------------------------------------------
* One area that Monte Carlo struggles with is maintaining numerical efficiency
in regions of low physical particle flux. Random ray, on the other hand, has
approximately even variance throughout the entire global simulation domain,
such that areas with low neutron flux are no less well known that areas of
high neutron flux. Absent weight windows in MC, random ray can be several
orders of magnitude faster than multigroup Monte Carlo in classes of problems
where areas with low physical neutron flux need to be resolved. While MC
uncertainty can be greatly improved with variance reduction techniques, they
add some user complexity, and weight windows can often be expensive to
generate via MC transport alone (e.g., via the `MAGIC method
<https://doi.org/10.1016/j.fusengdes.2011.01.059>`_). The random ray solver
may be used in future versions of OpenMC as a fast way to generate weight
windows for subsequent usage by the MC solver in OpenMC.
* In practical implementation terms, random ray is mechanically very similar to
how Monte Carlo works, in terms of the process of ray tracing on constructive
solid geometry (CSG) and handling stochastic convergence, etc. In the original
1972 paper by Askew that introduces MOC (which random ray is a variant of), he
stated:
.. epigraph::
"One of the features of the method proposed [MoC] is that ... the
tracking process needed to perform this operation is common to the
proposed method ... and to Monte Carlo methods. Thus a single tracking
routine capable of recognizing a geometric arrangement could be utilized
to service all types of solution, choice being made depending which was
more appropriate to the problem size and required accuracy."
-- Askew [Askew-1972]_
This prediction holds up---the additional requirements needed in OpenMC to
handle random ray transport turned out to be fairly small.
* It amortizes the code complexity in OpenMC for representing multigroup cross
sections. There is a significant amount of interface code, documentation, and
complexity in allowing OpenMC to generate and use multigroup XS data in its
MGMC mode. Random ray allows the same multigroup data to be used, making full
reuse of these existing capabilities.
-------------------------------
Random Ray Numerical Derivation
-------------------------------
In this section, we will derive the numerical basis for the random ray solver
mode in OpenMC. The derivation of random ray is also discussed in several papers
(`1 <Tramm-2017a>`_, `2 <Tramm-2017b>`_, `3 <Tramm-2018>`_), and some of those
derivations are reproduced here verbatim. Several extensions are also made to
add clarity, particularly on the topic of OpenMC's treatment of cell volumes in
the random ray solver.
~~~~~~~~~~~~~~~~~~~~~~~~~
Method of Characteristics
~~~~~~~~~~~~~~~~~~~~~~~~~
The Boltzmann neutron transport equation is a partial differential equation
(PDE) that describes the angular flux within a system. It is a balance equation,
with the streaming and absorption terms typically appearing on the left hand
side, which are balanced by the scattering source and fission source terms on
the right hand side.
.. math::
:label: transport
\begin{align*}
\mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E) & + \Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E) = \\
& \int_0^\infty d E^\prime \int_{4\pi} d \Omega^{\prime} \Sigma_s(\mathbf{r},\mathbf{\Omega}^\prime \rightarrow \mathbf{\Omega}, E^\prime \rightarrow E) \psi(\mathbf{r},\mathbf{\Omega}^\prime, E^\prime) \\
& + \frac{\chi(\mathbf{r}, E)}{4\pi k_{eff}} \int_0^\infty dE^\prime \nu \Sigma_f(\mathbf{r},E^\prime) \int_{4\pi}d \Omega^\prime \psi(\mathbf{r},\mathbf{\Omega}^\prime,E^\prime)
\end{align*}
In Equation :eq:`transport`, :math:`\psi` is the angular neutron flux. This
parameter represents the total distance traveled by all neutrons in a particular
direction inside of a control volume per second, and is often given in units of
:math:`1/(\text{cm}^{2} \text{s})`. As OpenMC does not support time dependence
in the random ray solver mode, we consider the steady state equation, where the
units of flux become :math:`1/\text{cm}^{2}`. The angular direction unit vector,
:math:`\mathbf{\Omega}`, represents the direction of travel for the neutron. The
spatial position vector, :math:`\mathbf{r}`, represents the location within the
simulation. The neutron energy, :math:`E`, or speed in continuous space, is
often given in units of electron volts. The total macroscopic neutron cross
section is :math:`\Sigma_t`. This value represents the total probability of
interaction between a neutron traveling at a certain speed (i.e., neutron energy
:math:`E`) and a target nucleus (i.e., the material through which the neutron is
traveling) per unit path length, typically given in units of
:math:`1/\text{cm}`. Macroscopic cross section data is a combination of
empirical data and quantum mechanical modeling employed in order to generate an
evaluation represented either in pointwise form or resonance parameters for each
target isotope of interest in a material, as well as the density of the
material, and is provided as input to a simulation. The scattering neutron cross
section, :math:`\Sigma_s`, is similar to the total cross section but only
measures scattering interactions between the neutron and the target nucleus, and
depends on the change in angle and energy the neutron experiences as a result of
the interaction. Several additional reactions like (n,2n) and (n,3n) are
included in the scattering transfer cross section. The fission neutron cross
section, :math:`\Sigma_f`, is also similar to the total cross section but only
measures the fission interaction between a neutron and a target nucleus. The
energy spectrum for neutrons born from fission, :math:`\chi`, represents a known
distribution of outgoing neutron energies based on the material that fissioned,
which is taken as input data to a computation. The average number of neutrons
born per fission is :math:`\nu`. The eigenvalue of the equation,
:math:`k_{eff}`, represents the effective neutron multiplication factor. If the
right hand side of Equation :eq:`transport` is condensed into a single term,
represented by the total neutron source term :math:`Q(\mathbf{r}, \mathbf{\Omega},E)`,
the form given in Equation :eq:`transport_simple` is reached.
.. math::
:label: transport_simple
\overbrace{\mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E)}^{\text{streaming term}} + \overbrace{\Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E)}^{\text{absorption term}} = \overbrace{Q(\mathbf{r}, \mathbf{\Omega},E)}^{\text{total neutron source term}}
Fundamentally, MOC works by solving Equation :eq:`transport_simple` along a
single characteristic line, thus altering the full spatial and angular scope of
the transport equation into something that holds true only for a particular
linear path (or track) through the reactor. These tracks are linear for neutral
particles that are not subject to field effects. With our transport equation in
hand, we will now derive the solution along a track. To accomplish this, we
parameterize :math:`\mathbf{r}` with respect to some reference location
:math:`\mathbf{r}_0` such that :math:`\mathbf{r} = \mathbf{r}_0 + s\mathbf{\Omega}`. In this
manner, Equation :eq:`transport_simple` can be rewritten for a specific segment
length :math:`s` at a specific angle :math:`\mathbf{\Omega}` through a constant
cross section region of the reactor geometry as in Equation :eq:`char_long`.
.. math::
:label: char_long
\mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r}_0 + s\mathbf{\Omega},\mathbf{\Omega},E) + \Sigma_t(\mathbf{r}_0 + s\mathbf{\Omega},E) \psi(\mathbf{r}_0 + s\mathbf{\Omega},\mathbf{\Omega},E) = Q(\mathbf{r}_0 + s\mathbf{\Omega}, \mathbf{\Omega},E)
As this equation holds along a one dimensional path, we can assume the
dependence of :math:`s` on :math:`\mathbf{r}_0` and :math:`\mathbf{\Omega}` such that
:math:`\mathbf{r}_0 + s\mathbf{\Omega}` simplifies to :math:`s`. When the differential
operator is also applied to the angular flux :math:`\psi`, we arrive at the
characteristic form of the Boltzmann Neutron Transport Equation given in
Equation :eq:`char`.
.. math::
:label: char
\frac{d}{ds} \psi(s,\mathbf{\Omega},E) + \Sigma_t(s,E) \psi(s,\mathbf{\Omega},E) = Q(s, \mathbf{\Omega},E)
An analytical solution to this characteristic equation can be achieved with the
use of an integrating factor:
.. math::
:label: int_factor
e^{ \int_0^s ds' \Sigma_t (s', E)}
to arrive at the final form of the characteristic equation shown in Equation
:eq:`full_char`.
.. math::
:label: full_char
\psi(s,\mathbf{\Omega},E) = \psi(\mathbf{r}_0,\mathbf{\Omega},E) e^{-\int_0^s ds^\prime \Sigma_t(s^\prime,E)} + \int_0^s ds^{\prime\prime} Q(s^{\prime\prime},\mathbf{\Omega}, E) e^{-\int_{s^{\prime\prime}}^s ds^\prime \Sigma_t(s^\prime,E)}
With this characteristic form of the transport equation, we now have an
analytical solution along a linear path through any constant cross section
region of a system. While the solution only holds along a linear track, no
discretizations have yet been made.
Similar to many other solution approaches to the Boltzmann neutron transport
equation, the MOC approach also uses a "multigroup" approximation in order to
discretize the continuous energy spectrum of neutrons traveling through the
system into fixed set of energy groups :math:`G`, where each group :math:`g \in
G` has its own specific cross section parameters. This makes the difficult
non-linear continuous energy dependence much more manageable as group wise cross
section data can be precomputed and fed into a simulation as input data. The
computation of multigroup cross section data is not a trivial task and can
introduce errors in the simulation. However, this is an active field of research
common to all multigroup methods, and there are numerous generation methods
available that are capable of reducing the biases introduced by the multigroup
approximation. Commonly used methods include the subgroup self-shielding method
and use of fast (unconverged) Monte Carlo simulations to produce cross section
estimates. It is important to note that Monte Carlo methods are capable of
treating the energy variable of the neutron continuously, meaning that they do
not need to make this approximation and are therefore not subject to any
multigroup errors.
Following the multigroup discretization, another assumption made is that a large
and complex problem can be broken up into small constant cross section regions,
and that these regions have group dependent, flat, isotropic sources (fission
and scattering), :math:`Q_g`. Anisotropic as well as higher order sources are
also possible with MOC-based methods but are not used yet in OpenMC for
simplicity. With these key assumptions, the multigroup MOC form of the neutron
transport equation can be written as in Equation :eq:`moc_final`.
.. math::
:label: moc_final
\psi_g(s, \mathbf{\Omega}) = \psi_g(\mathbf{r_0}, \mathbf{\Omega}) e^{-\int_0^s ds^\prime \Sigma_{t_g}(s^\prime)} + \int_0^s ds^{\prime\prime} Q_g(s^{\prime\prime},\mathbf{\Omega}) e^{-\int_{s^{\prime\prime}}^s ds^\prime \Sigma_{t_g}(s^\prime)}
The CSG definition of the system is used to create spatially defined source
regions (each region being denoted as :math:`i`). These neutron source regions
are often approximated as being constant
(flat) in source intensity but can also be defined using a higher order source
(linear, quadratic, etc.) that allows for fewer source regions to be required to
achieve a specified solution fidelity. In OpenMC, the approximation of a
spatially constant isotropic fission and scattering source :math:`Q_{i,g}` in
cell :math:`i` leads
to simple exponential attenuation along an individual characteristic of length
:math:`s` given by Equation :eq:`fsr_attenuation`.
.. math::
:label: fsr_attenuation
\psi_g(s) = \psi_g(0) e^{-\Sigma_{t,i,g} s} + \frac{Q_{i,g}}{\Sigma_{t,i,g}} \left( 1 - e^{-\Sigma_{t,i,g} s} \right)
For convenience, we can also write this equation in terms of the incoming and
outgoing angular flux (:math:`\psi_g^{in}` and :math:`\psi_g^{out}`), and
consider a specific tracklength for a particular ray :math:`r` crossing cell
:math:`i` as :math:`\ell_r`, as in:
.. math::
:label: fsr_attenuation_in_out
\psi_g^{out} = \psi_g^{in} e^{-\Sigma_{t,i,g} \ell_r} + \frac{Q_{i,g}}{\Sigma_{t,i,g}} \left( 1 - e^{-\Sigma_{t,i,g} \ell_r} \right) .
We can then define the average angular flux of a single ray passing through the
cell as:
.. math::
:label: average
\overline{\psi}_{r,i,g} = \frac{1}{\ell_r} \int_0^{\ell_r} \psi_{g}(s)ds .
We can then substitute in Equation :eq:`fsr_attenuation` and solve, resulting
in:
.. math::
:label: average_solved
\overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} - \frac{\psi_{r,g}^{out} - \psi_{r,g}^{in}}{\ell_r \Sigma_{t,i,g}} .
By rearranging Equation :eq:`fsr_attenuation_in_out`, we can then define
:math:`\Delta \psi_{r,g}` as the change in angular flux for ray :math:`r`
passing through region :math:`i` as:
.. math::
:label: delta_psi
\Delta \psi_{r,g} = \psi_{r,g}^{in} - \psi_{r,g}^{out} = \left(\psi_{r,g}^{in} - \frac{Q_{i,g}}{\Sigma_{t,i,g}} \right) \left( 1 - e^{-\Sigma_{t,i,g} \ell_r} \right) .
Equation :eq:`delta_psi` is a useful expression as it is easily computed with
the known inputs for a ray crossing through the region.
By substituting :eq:`delta_psi` into :eq:`average_solved`, we can arrive at a
final expression for the average angular flux for a ray crossing a region as:
.. math::
:label: average_psi_final
\overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}}
~~~~~~~~~~~
Random Rays
~~~~~~~~~~~
In the previous subsection, the governing characteristic equation along a 1D
line through the system was written, such that an analytical solution for the
ODE can be computed. If enough characteristic tracks (ODEs) are solved, then the
behavior of the governing PDE can be numerically approximated. In traditional
deterministic MOC, the selection of tracks is chosen deterministically, where
azimuthal and polar quadratures are defined along with even track spacing in
three dimensions. This is the point at which random ray diverges from
deterministic MOC numerically. In the random ray method, rays are randomly
sampled from a uniform distribution in space and angle and tracked along a
predefined distance through the geometry before terminating. **Importantly,
different rays are sampled each power iteration, leading to a fully stochastic
convergence process.** This results in a need to utilize both inactive and
active batches as in the Monte Carlo method.
While Monte Carlo implicitly converges the scattering source fully within each
iteration, random ray (and MOC) solvers are not typically written to fully
converge the scattering source within a single iteration. Rather, both the
fission and scattering sources are updated each power iteration, thus requiring
enough outer iterations to reach a stationary distribution in both the fission
source and scattering source. So, even in a low dominance ratio problem like a
2D pincell, several hundred inactive batches may still be required with random
ray to allow the scattering source to fully develop, as neutrons undergoing
hundreds of scatters may constitute a non-trivial contribution to the fission
source. We note that use of a two-level second iteration scheme is sometimes
used by some MOC or random ray solvers so as to fully converge the scattering
source with many inner iterations before updating the fission source in the
outer iteration. It is typically more efficient to use the single level
iteration scheme, as there is little reason to spend so much work converging the
scattering source if the fission source is not yet converged.
Overall, the difference in how random ray and Monte Carlo converge the
scattering source means that in practice, random ray typically requires more
inactive iterations than are required in Monte Carlo. While a Monte Carlo
simulation may need 100 inactive iterations to reach a stationary source
distribution for many problems, a random ray solve will likely require 1,000
iterations or more. Source convergence metrics (e.g., Shannon entropy) are thus
recommended when performing random ray simulations to ascertain when the source
has fully developed.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Converting Angular Flux to Scalar Flux
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Thus far in our derivation, we have been able to write analytical equations that
solve for the change in angular flux of a ray crossing a flat source region
(Equation :eq:`delta_psi`) as well as the ray's average angular flux through
that region (Equation :eq:`average_psi_final`). To determine the source for the
next power iteration, we need to assemble our estimates of angular fluxes from
all the sampled rays into scalar fluxes within each FSR.
We can define the scalar flux in region :math:`i` as:
.. math::
:label: integral
\phi_i = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} .
The integral in the numerator:
.. math::
:label: numerator
\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r} .
is not known analytically, but with random ray, we are going the numerically
approximate it by discretizing over a finite number of tracks (with a finite
number of locations and angles) crossing the domain. We can then use the
characteristic method to determine the total angular flux along that line.
Conceptually, this can be thought of as taking a volume-weighted sum of angular
fluxes for all :math:`N_i` rays that happen to pass through cell :math:`i` that
iteration. When written in discretized form (with the discretization happening
in terms of individual ray segments :math:`r` that pass through region
:math:`i`), we arrive at:
.. math::
:label: discretized
\phi_{i,g} = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} = \overline{\overline{\psi}}_{i,g} \approx \frac{\sum\limits_{r=1}^{N_i} \ell_r w_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r w_r} .
Here we introduce the term :math:`w_r`, which represents the "weight" of the ray
(its 2D area), such that the volume that a ray is responsible for can be
determined by multiplying its length :math:`\ell` by its weight :math:`w`. As
the scalar flux vector is a shape function only, we are actually free to
multiply all ray weights :math:`w` by any constant such that the overall shape
is still maintained, even if the magnitude of the shape function changes. Thus,
we can simply set :math:`w_r` to be unity for all rays, such that:
.. math::
:label: weights
\text{Volume of cell } i = V_i \approx \sum\limits_{r=1}^{N_i} \ell_r w_r = \sum\limits_{r=1}^{N_i} \ell_r .
We can then rewrite our discretized equation as:
.. math::
:label: discretized_2
\phi_{i,g} \approx \frac{\sum\limits_{r=1}^{N_i} \ell_r w_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r w_r} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r} .
Thus, the scalar flux can be inferred if we know the volume weighted sum of the
average angular fluxes that pass through the cell. Substituting
:eq:`average_psi_final` into :eq:`discretized_2`, we arrive at:
.. math::
:label: scalar_full
\phi_{i,g} = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} = \overline{\overline{\psi}}_{i,g} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}}}{\sum\limits_{r=1}^{N_i} \ell_r},
which when partially simplified becomes:
.. math::
:label: scalar_four_vols
\phi = \frac{Q_{i,g} \sum\limits_{r=1}^{N_i} \ell_r}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} + \frac{\sum\limits_{r=1}^{N_i} \ell_r \frac{\Delta \psi_i}{\ell_r}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} .
Note that there are now four (seemingly identical) volume terms in this equation.
~~~~~~~~~~~~~~
Volume Dilemma
~~~~~~~~~~~~~~
At first glance, Equation :eq:`scalar_four_vols` appears ripe for cancellation
of terms. Mathematically, such cancellation allows us to arrive at the following
"naive" estimator for the scalar flux:
.. math::
:label: phi_naive
\phi_{i,g}^{naive} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} .
This derivation appears mathematically sound at first glance but unfortunately
raises a serious issue as discussed in more depth by `Tramm et al.
<Tramm-2020>`_ and `Cosgrove and Tramm <Cosgrove-2023>`_. Namely, the second
term:
.. math::
:label: ratio_estimator
\frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r}
features stochastic variables (the sums over random ray lengths and angular
fluxes) in both the numerator and denominator, making it a stochastic ratio
estimator, which is inherently biased. In practice, usage of the naive estimator
does result in a biased, but "consistent" estimator (i.e., it is biased, but
the bias tends towards zero as the sample size increases). Experimentally, the
right answer can be obtained with this estimator, though a very fine ray density
is required to eliminate the bias.
How might we solve the biased ratio estimator problem? While there is no obvious
way to alter the numerator term (which arises from the characteristic
integration approach itself), there is potentially more flexibility in how we
treat the stochastic term in the denominator, :math:`\sum\limits_{r=1}^{N_i}
\ell_r` . From Equation :eq:`weights` we know that this term can be directly
inferred from the volume of the problem, which does not actually change between
iterations. Thus, an alternative treatment for this "volume" term in the
denominator is to replace the actual stochastically sampled total track length
with the expected value of the total track length. For instance, if the true
volume of the FSR is known (as is the total volume of the full simulation domain
and the total tracklength used for integration that iteration), then we know the
true expected value of the tracklength in that FSR. That is, if a FSR accounts
for 2% of the overall volume of a simulation domain, then we know that the
expected value of tracklength in that FSR will be 2% of the total tracklength
for all rays that iteration. This is a key insight, as it allows us to the
replace the actual tracklength that was accumulated inside that FSR each
iteration with the expected value.
If we know the analytical volumes, then those can be used to directly compute
the expected value of the tracklength in each cell. However, as the analytical
volumes are not typically known in OpenMC due to the usage of user-defined
constructive solid geometry, we need to source this quantity from elsewhere. An
obvious choice is to simply accumulate the total tracklength through each FSR
across all iterations (batches) and to use that sum to compute the expected
average length per iteration, as:
.. math::
:label: sim_estimator
\sum\limits^{}_{i} \ell_i \approx \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}
where :math:`b` is a single batch in :math:`B` total batches simulated so far.
In this manner, the expected value of the tracklength will become more refined
as iterations continue, until after many iterations the variance of the
denominator term becomes trivial compared to the numerator term, essentially
eliminating the presence of the stochastic ratio estimator. A "simulation
averaged" estimator is therefore:
.. math::
:label: phi_sim
\phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}}
In practical terms, the "simulation averaged" estimator is virtually
indistinguishable numerically from use of the true analytical volume to estimate
this term. Note also that the term "simulation averaged" refers only to the
volume/length treatment, the scalar flux estimate itself is computed fully again
each iteration.
There are some drawbacks to this method. Recall, this denominator volume term
originally stemmed from taking a volume weighted integral of the angular flux,
in which case the denominator served as a normalization term for the numerator
integral in Equation :eq:`integral`. Essentially, we have now used a different
term for the volume in the numerator as compared to the normalizing volume in
the denominator. The inevitable mismatch (due to noise) between these two
quantities results in a significant increase in variance. Notably, the same
problem occurs if using a tracklength estimate based on the analytical volume,
as again the numerator integral and the normalizing denominator integral no
longer match on a per-iteration basis.
In practice, the simulation averaged method does completely remove the bias,
though at the cost of a notable increase in variance. Empirical testing reveals
that on most problems, the simulation averaged estimator does win out overall in
numerical performance, as a much coarser quadrature can be used resulting in
faster runtimes overall. Thus, OpenMC uses the simulation averaged estimator in
its random ray mode.
~~~~~~~~~~~~~~~
Power Iteration
~~~~~~~~~~~~~~~
Given a starting source term, we now have a way of computing an estimate of the
scalar flux in each cell by way of transporting rays randomly through the
domain, recording the change in angular flux for the rays into each cell as they
make their traversals, and summing these contributions up as in Equation
:eq:`phi_sim`. How then do we turn this into an iterative process such that we
improve the estimate of the source and scalar flux over many iterations, given
that our initial starting source will just be a guess?
The source :math:`Q^{n}` for iteration :math:`n` can be inferred
from the scalar flux from the previous iteration :math:`n-1` as:
.. math::
:label: source_update
Q^{n}(i, g) = \frac{\chi}{k^{n-1}_{eff}} \nu \Sigma_f(i, g) \phi^{n-1}(g) + \sum\limits^{G}_{g'} \Sigma_{s}(i,g,g') \phi^{n-1}(g')
where :math:`Q^{n}(i, g)` is the total source (fission + scattering) in region
:math:`i` and energy group :math:`g`. Notably, the in-scattering source in group
:math:`g` must be computed by summing over the contributions from all groups
:math:`g' \in G`.
In a similar manner, the eigenvalue for iteration :math:`n` can be computed as:
.. math::
:label: eigenvalue_update
k^{n}_{eff} = k^{n-1}_{eff} \frac{F^n}{F^{n-1}},
where the total spatial- and energy-integrated fission rate :math:`F^n` in
iteration :math:`n` can be computed as:
.. math::
:label: fission_source
F^n = \sum\limits^{M}_{i} \left( V_i \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right)
where :math:`M` is the total number of FSRs in the simulation. Similarly, the
total spatial- and energy-integrated fission rate :math:`F^{n-1}` in iteration
:math:`n-1` can be computed as:
.. math::
:label: fission_source_prev
F^{n-1} = \sum\limits^{M}_{i} \left( V_i \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n-1}(g) \right)
Notably, the volume term :math:`V_i` appears in the eigenvalue update equation.
The same logic applies to the treatment of this term as was discussed earlier.
In OpenMC, we use the "simulation averaged" volume derived from summing over all
ray tracklength contributions to a FSR over all iterations and dividing by the
total integration tracklength to date. Thus, Equation :eq:`fission_source`
becomes:
.. math::
:label: fission_source_volumed
F^n = \sum\limits^{M}_{i} \left( \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right)
and a similar substitution can be made to update Equation
:eq:`fission_source_prev` . In OpenMC, the most up-to-date version of the volume
estimate is used, such that the total fission source from the previous iteration
(:math:`n-1`) is also recomputed each iteration.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ray Starting Conditions and Inactive Length
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Another key area of divergence between deterministic MOC and random ray is the
starting conditions for rays. In deterministic MOC, the angular flux spectrum
for rays are stored at any reflective or periodic boundaries so as to provide a
starting condition for the next iteration. As there are many tracks, storage of
angular fluxes can become costly in terms of memory consumption unless there are
only vacuum boundaries present.
In random ray, as the starting locations of rays are sampled anew each
iteration, the initial angular flux spectrum for the ray is unknown. While a
guess can be made by taking the isotropic source from the FSR the ray was
sampled in, direct usage of this quantity would result in significant bias and
error being imparted on the simulation.
Thus, an `on-the-fly approximation method <Tramm-2017a>`_ was developed (known
as the "dead zone"), where the first several mean free paths of a ray are
considered to be "inactive" or "read only". In this sense, the angular flux is
solved for using the MOC equation, but the ray does not "tally" any scalar flux
back to the FSRs that it travels through. After several mean free paths have
been traversed, the ray's angular flux spectrum typically becomes dominated by
the accumulated source terms from the cells it has traveled through, while the
(incorrect) starting conditions have been attenuated away. In the animation in
the :ref:`introductory section on this page <methods_random_ray_intro>`, the
yellow portion of the ray lengths is the dead zone. As can be seen in this
animation, the tallied :math:`\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}` term
that is plotted is not affected by the ray when the ray is within its inactive
length. Only when the ray enters its active mode does the ray contribute to the
:math:`\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}` sum for the iteration.
~~~~~~~~~~~~~~~~~~~~~
Ray Ending Conditions
~~~~~~~~~~~~~~~~~~~~~
To ensure that a uniform density of rays is integrated in space and angle
throughout the simulation domain, after exiting the initial inactive "dead zone"
portion of the ray, the rays are run for a user-specified distance. Typically, a
choice of at least several times the length of the inactive "dead zone" is made
so as to amortize the cost of the dead zone. For example, if a dead zone of 30
cm is selected, then an active length of 300 cm might be selected so that the
cost of the dead zone is at most 10% of the overall runtime.
--------------------
Simplified Algorithm
--------------------
A simplified set of functions that execute a single random ray power iteration
are given below. Not all global variables are defined in this illustrative
example, but the high level components of the algorithm are shown. A number of
significant simplifications are made for clarity---for example, no inactive
"dead zone" length is shown, geometry operations are abstracted, no parallelism
(or thread safety) is expressed, a naive exponential treatment is used, and rays
are not halted at their exact termination distances, among other subtleties.
Nonetheless, the below algorithms may be useful for gaining intuition on the
basic components of the random ray process. Rather than expressing the algorithm
in abstract pseudocode, C++ is used to make the control flow easier to
understand.
The first block below shows the logic for a single power iteration (batch):
.. code-block:: C++
double power_iteration(double k_eff) {
// Update source term (scattering + fission)
update_neutron_source(k_eff);
// Reset scalar fluxes to zero
fill<float>(global::scalar_flux_new, 0.0f);
// Transport sweep over all random rays for the iteration
for (int i = 0; i < nrays; i++) {
RandomRay ray;
initialize_ray(ray);
transport_single_ray(ray);
}
// Normalize scalar flux and update volumes
normalize_scalar_flux_and_volumes();
// Add source to scalar flux, compute number of FSR hits
add_source_to_scalar_flux();
// Compute k-eff using updated scalar flux
k_eff = compute_k_eff(k_eff);
// Set phi_old = phi_new
global::scalar_flux_old.swap(global::scalar_flux_new);
return k_eff;
}
The second function shows the logic for transporting a single ray within the
transport loop:
.. code-block:: C++
void transport_single_ray(RandomRay& ray) {
// Reset distance to zero
double distance = 0.0;
// Continue transport of ray until active length is reached
while (distance < user_setting::active_length) {
// Ray trace to find distance to next surface (i.e., segment length)
double s = distance_to_nearest_boundary(ray);
// Attenuate flux (and accumulate source/attenuate) on segment
attenuate_flux(ray, s);
// Advance particle to next surface
ray.location = ray.location + s * ray.direction;
// Move ray across the surface
cross_surface(ray);
// Add segment length "s" to total distance traveled
distance += s;
}
}
The final function below shows the logic for solving for the characteristic MOC
equation (and accumulating the scalar flux contribution of the ray into the
scalar flux value for the FSR).
.. code-block:: C++
void attenuate_flux(RandomRay& ray, double s) {
// Determine which flat source region (FSR) the ray is currently in
int fsr = get_fsr_id(ray.location);
// Determine material type
int material = get_material_type(fsr);
// MOC incoming flux attenuation + source contribution/attenuation equation
for (int e = 0; e < global::n_energy_groups; e++) {
float sigma_t = global::macro_xs[material].total;
float tau = sigma_t * s;
float delta_psi = (ray.angular_flux[e] - global::source[fsr][e] / sigma_t) * (1 - exp(-tau));
ray.angular_flux_[e] -= delta_psi;
global::scalar_flux_new[fsr][e] += delta_psi;
}
// Record total tracklength in this FSR (to compute volume)
global::volume[fsr] += s;
}
------------------------
How are Tallies Handled?
------------------------
Most tallies, filters, and scores that you would expect to work with a
multigroup solver like random ray should work. For example, you can define 3D
mesh tallies with energy filters and flux, fission, and nu-fission scores, etc.
There are some restrictions though. For starters, it is assumed that all filter
mesh boundaries will conform to physical surface boundaries (or lattice
boundaries) in the simulation geometry. It is acceptable for multiple cells
(FSRs) to be contained within a filter mesh cell (e.g., pincell-level or
assembly-level tallies should work), but it is currently left as undefined
behavior if a single simulation cell is able to score to multiple filter mesh
cells. In the future, the capability to fully support mesh tallies may be added
to OpenMC, but for now this restriction needs to be respected.
---------------------------
Fundamental Sources of Bias
---------------------------
Compared to continuous energy Monte Carlo simulations, the known sources of bias
in random ray particle transport are:
- **Multigroup Energy Discretization:** The multigroup treatment of flux and
cross sections incurs a significant bias, as a reaction rate (:math:`R_g =
V \phi_g \Sigma_g`) for an energy group :math:`g` can only be conserved
for a given choice of multigroup cross section :math:`\Sigma_g` if the
flux (:math:`\phi_g`) is known a priori. If the flux was already known,
then there would be no point to the simulation, resulting in a fundamental
need for approximating this quantity. There are numerous methods for
generating relatively accurate multigroup cross section libraries that can
each be applied to a narrow design area reliably, although there are
always limitations and/or complexities that arise with a multigroup energy
treatment. This is by far the most significant source of simulation bias
between Monte Carlo and random ray for most problems. While the other
areas typically have solutions that are highly effective at mitigating
bias, error stemming from multigroup energy discretization is much harder
to remedy.
- **Flat Source Approximation:**. In OpenMC, a "flat" (0th order) source
approximation is made, wherein the scattering and fission sources within a
cell are assumed to be spatially uniform. As the source in reality is a
continuous function, this leads to bias, although the bias can be reduced
to acceptable levels if the flat source regions are sufficiently small.
The bias can also be mitigated by assuming a higher-order source (e.g.,
linear or quadratic), although OpenMC does not yet have this capability.
In practical terms, this source of bias can become very large if cells are
large (with dimensions beyond that of a typical particle mean free path),
but the subdivision of cells can often reduce this bias to trivial levels.
- **Anisotropic Source Approximation:** In OpenMC, the source is not only
assumed to be flat but also isotropic, leading to bias. It is possible for
MOC (and likely random ray) to treat anisotropy explicitly, but this is
not currently supported in OpenMC. This source of bias is not significant
for some problems, but becomes more problematic for others. Even in the
absence of explicit treatment of anistropy, use of transport-corrected
multigroup cross sections can often mitigate this bias, particularly for
light water reactor simulation problems.
- **Angular Flux Initial Conditions:** Each time a ray is sampled, its
starting angular flux is unknown, so a guess must be made (typically the
source term for the cell it starts in). Usage of an adequate inactive ray
length (dead zone) mitigates this error. As the starting guess is
attenuated at a rate of :math:`\exp(-\Sigma_t \ell)`, this bias can driven
below machine precision in a low cost manner on many problems.
.. _Tramm-2017a: https://doi.org/10.1016/j.jcp.2017.04.038
.. _Tramm-2017b: https://doi.org/10.1016/j.anucene.2017.10.015
.. _Tramm-2018: https://dspace.mit.edu/handle/1721.1/119038
.. _Tramm-2020: https://doi.org/10.1051/EPJCONF/202124703021
.. _Cosgrove-2023: https://doi.org/10.1080/00295639.2023.2270618
.. only:: html
.. rubric:: References
.. [Askew-1972] Askew, “A Characteristics Formulation of the Neutron Transport
Equation in Complicated Geometries.” Technical Report AAEW-M 1108, UK Atomic
Energy Establishment (1972).

View file

@ -279,6 +279,8 @@ The `official ENDF/B-VII.1 HDF5 library
multipole library, so if you are using this library, the windowed multipole data
will already be available to you.
.. _create_mgxs:
-------------------------
Multigroup Cross Sections
-------------------------

View file

@ -25,4 +25,6 @@ essential aspects of using OpenMC to perform simulations.
processing
parallel
volume
random_ray
troubleshoot

View file

@ -0,0 +1,480 @@
.. _random_ray:
=================
Random Ray Solver
=================
In general, the random ray solver mode uses most of the same settings and
:ref:`run strategies <usersguide_particles>` as the standard Monte Carlo solver
mode. For instance, random ray solves are also split up into :ref:`inactive and
active batches <usersguide_batches>`. However, there are a couple of settings
that are unique to the random ray solver and a few areas that the random ray
run strategy differs, both of which will be described in this section.
------------------------
Enabling Random Ray Mode
------------------------
To utilize the random ray solver, the :attr:`~openmc.Settings.random_ray`
dictionary must be present in the :class:`openmc.Settings` Python class. There
are a number of additional settings that must be specified within this
dictionary that will be discussed below. Additionally, the "multi-group" energy
mode must be specified.
-------
Batches
-------
In Monte Carlo simulations, inactive batches are used to let the fission source
develop into a stationary distribution before active batches are performed that
actually accumulate statistics. While this is true of random ray as well, in the
random ray mode the inactive batches are also used to let the scattering source
develop. Monte Carlo fully represents the scattering source within each
iteration (by its nature of fully simulating particles from birth to death
through any number of physical scattering events), whereas the scattering source
in random ray can only represent as many scattering events as batches have been
completed. For example, by iteration 10 in random ray, the scattering source
only captures the behavior of neutrons through their 10th scattering event.
Thus, while inactive batches are only required in an eigenvalue solve in Monte
Carlo, **inactive batches are required for both eigenvalue and fixed source
solves in random ray mode** due to this additional need to converge the
scattering source.
The additional burden of converging the scattering source generally results in a
higher requirement for the number of inactive batches---often by an order of
magnitude or more. For instance, it may be reasonable to only use 50 inactive
batches for a light water reactor simulation with Monte Carlo, but random ray
might require 500 or more inactive batches. Similar to Monte Carlo,
:ref:`Shannon entropy <usersguide_entropy>` can be used to gauge whether the
combined scattering and fission source has fully developed.
Similar to Monte Carlo, active batches are used in the random ray solver mode to
accumulate and converge statistics on unknown quantities (i.e., the random ray
sources, scalar fluxes, as well as any user-specified tallies).
The batch parameters are set in the same manner as with the regular Monte Carlo
solver::
settings = openmc.Settings()
settings.energy_mode = "multi-group"
settings.batches = 1200
settings.inactive = 600
-------------------------------
Inactive Ray Length (Dead Zone)
-------------------------------
A major issue with random ray is that the starting angular flux distribution for
each sampled ray is unknown. Thus, an on-the-fly method is used to build a high
quality approximation of the angular flux of the ray each iteration. This is
accomplished by running the ray through an inactive length (also known as a dead
zone length), where the ray is moved through the geometry and its angular flux
is solved for via the normal :ref:`MOC <methods_random_ray_intro>` equation, but
no information is written back to the system. Thus, the ray is run in a "read
only" mode for the set inactive length. This parameter can be adjusted, in units
of cm, as::
settings.random_ray['distance_inactive'] = 40.0
After several mean free paths are traversed, the angular flux spectrum of the
ray becomes dominated by the in-scattering and fission source components that it
picked up when travelling through the geometry, while its original (incorrect)
starting angular flux is attenuated toward zero. Thus, longer selections of
inactive ray length will asymptotically approach the true angular flux.
In practice, 10 mean free paths are sufficient (with light water reactors often
requiring only about 10--50 cm of inactive ray length for the error to become
undetectable). However, we caution that certain models with large quantities of
void regions (even if just limited to a few streaming channels) may require
significantly longer inactive ray lengths to ensure that the angular flux is
accurate before the conclusion of the inactive ray length. Additionally,
problems where a sensitive estimate of the uncollided flux is required (e.g.,
the detector response to fast neutrons is required, and the detected is located
far away from the source in a moderator region) may require the user to specify
an inactive length that is derived from the pyhsical geometry of the simulation
problem rather than its material properties. For instance, consider a detector
placed 30 cm outside of a reactor core, with a moderator region separating the
detector from the core. In this case, rays sampled in the moderator region and
heading toward the detector will begin life with a highly scattered thermal
spectrum and will have an inaccurate fast spectrum. If the dead zone length is
only 20 cm, we might imagine such rays writing to the detector tally within
their active lengths, despite their innaccurate estimate of the uncollided fast
angular flux. Thus, an inactive length of 100--200 cm would ensure that any such
rays would still be within their inactive regions, and only rays that have
actually traversed through the core (and thus have an accurate representation of
the core's emitted fast flux) will score to the detector region while in their
active phase.
------------------------------------
Active Ray Length and Number of Rays
------------------------------------
Once the inactive length of the ray has completed, the active region of the ray
begins. The ray is now run in regular mode, where changes in angular flux as it
traverses through each flat source region are written back to the system, so as
to contribute to the estimate for the iteration scalar flux (which is used to
compute the source for the next iteration). The active ray length can be
adjusted, in units of [cm], as::
settings.random_ray['distance_active'] = 400.0
Assuming that a sufficient inactive ray length is used so that the starting
angular flux is highly accurate, any selection of active length greater than
zero is theoretically acceptable. However, in order to adequately sample the
full integration domain, a selection of a very short track length would require
a very high number of rays to be selected. Due to the static costs per ray of
computing the starting angular flux in the dead zone, typically very short ray
lengths are undesireable. Thus, to amortize the per-ray cost of the inactive
region of the ray, it is desirable to select a very long inactive ray length.
For example, if the inactive length is set to 20 cm, a 200 cm active ray length
ensures that only about 10% of the overall simulation runtime is spent in the
inactive ray phase integration, making the dead zone a relatively inexpensive
way of estimating the angular flux.
Thus, to fully amortize the cost of the dead zone integration, one might ask why
not simply run a single ray per iteration with an extremely long active length?
While this is also theoretically possible, this results in two issues. The first
problem is that each ray only represents a single angular sample. As we want to
sample the angular phase space of the simulation with similar fidelity to the
spatial phase space, we naturally want a lot of angles. This means in practice,
we want to balance the need to amortize the cost of the inactive region of the
ray with the need to sample lots of angles. The second problem is that
parallelism in OpenMC is expressed in terms of rays, with each being processed
by an independent MPI rank and/or OpenMP thread, thus we want to ensure each
thread has many rays to process.
In practical terms, the best strategy is typically to set an active ray length
that is about 10 times that of the inactive ray length. This is often the right
balance between ensuring not too much time is spent in the dead zone, while
still adequately sampling the angular phase space. However, as discussed in the
previous section, some types of simulation may demand that additional thought be
applied to this parameter. For instance, in the same example where we have a
detector region far outside a reactor core, we want to make sure that there is
enough active ray length that rays exiting the core can reach the detector
region. For example, if the detector were to be 30 cm outside of the core, then
we would need to ensure that at least a few hundred cm of active length were
used so as to ensure even rays with indirect angles will be able to reach the
target region.
The number of rays each iteration can be set by reusing the normal Monte Carlo
particle count selection parameter, as::
settings.particles = 2000
-----------
Ray Density
-----------
In the preceding sections, it was argued that for most use cases, the inactive
length for a ray can be determined by taking a multiple of the mean free path
for the limiting energy group. The active ray length could then be set by taking
a multiple of the inactive length. With these parameters set, how many rays per
iteration should be run?
There are three basic settings that control the density of the stochastic
quadrature being used to integrate the domain each iteration. These three
variables are:
- The number of rays (in OpenMC settings parlance, "particles")
- The inactive distance per ray
- The active distance per ray
While the inactive and active ray lengths can usually be chosen by simply
examining the geometry, tallies, and cross section data, one has much more
flexibility in the choice of the number of rays to run. Consider a few
scenarios:
- If a choice of zero rays is made, then no information is gained by the system
after each batch.
- If a choice of rays close to zero is made, then some information is gained
after each batch, but many source regions may not have been visited that
iteration, which is not ideal numerically and can result in instability.
Empirically, we have found that the simulation can remain stable and produce
accurate results even when on average 20% or more of the cells have zero rays
passing through them each iteration. However, besides the cost of transporting
rays, a new neutron source must be computed based on the scalar flux at each
iteration. This cost is dictated only by the number of source regions and
energy groups---it is independent of the number of rays. Thus, in practical
terms, if too few rays are run, then the simulation runtime becomes dominated
by the fixed cost of source updates, making it inefficient overall given that
a huge number of active batches will likely be required to converge statistics
to acceptable levels. Additionally, if many cells are missed each iteration,
then the fission and scattering sources may not develop very quickly,
resulting in a need for far more inactive batches than might otherwise be
required.
- If a choice of running a very large number of rays is made such that you
guarantee that all cells are hit each iteration, this avoids any issues with
numerical instability. As even more rays are run, this reduces the number of
active batches that must be used to converge statistics and therefore
minimizes the fixed per-iteration source update costs. While this seems
advantageous, it has the same practical downside as with Monte Carlo---namely,
that the inactive batches tend to be overly well integrated, resulting in a
lot of wasted time. This issue is actually much more serious than in Monte
Carlo (where typically only tens of inactive batches are needed), as random
ray often requires hundreds or even thousands of inactive batches. Thus,
minimizing the cost of the source updates in the active phase needs to be
balanced against the increased cost of the inactive phase of the simulation.
- If a choice of rays is made such that relatively few (e.g., around 0.1%) of
cells are missed each iteration, the cost of the inactive batches of the
simulation is minimized. In this "goldilocks" regime, there is very little
chance of numerical instability, and enough information is gained by each cell
to progress the fission and scattering sources forward at their maximum rate.
However, the inactive batches can proceed with minimal cost. While this will
result in the active phase of the simulation requiring more batches (and
correspondingly higher source update costs), the added cost is typically far
less than the savings by making the inactive phase much cheaper.
To help you set this parameter, OpenMC will report the average flat source
region miss rate at the end of the simulation. Additionally, OpenMC will alert
you if very high miss rates are detected, indicating that more rays and/or a
longer active ray length might improve numerical performance. Thus, a "guess and
check" approach to this parameter is recommended, where a very low guess is
made, a few iterations are performed, and then the simulation is restarted with
a larger value until the "low ray density" messages go away.
.. note::
In summary, the user should select an inactive length corresponding to many
times the mean free path of a particle, generally O(10--100) cm, to ensure accuracy of
the starting angular flux. The active length should be 10× the inactive
length to amortize its cost. The number of rays should be enough so that
nearly all :ref:`FSRs <subdivision_fsr>` are hit at least once each power iteration (the hit fraction
is reported by OpenMC for empirical user adjustment).
.. warning::
For simulations where long range uncollided flux estimates need to be
accurately resolved (e.g., shielding, detector response, and problems with
significant void areas), make sure that selections for inactive and active
ray lengths are sufficiently long to allow for transport to occur between
source and target regions of interest.
----------
Ray Source
----------
Random ray requires that the ray source be uniform in space and isotropic in
angle. To facilitate sampling, the user must specify a single random ray source
for sampling rays in both eigenvalue and fixed source solver modes. The random
ray integration source should be of type :class:`openmc.IndependentSource`, and
is specified as part of the :attr:`openmc.Settings.random_ray` dictionary. Note
that the source must not be limited to only fissionable regions. Additionally,
the source box must cover the entire simulation domain. In the case of a
simulation domain that is not box shaped, a box source should still be used to
bound the domain but with the source limited to rejection sampling the actual
simulation universe (which can be specified via the ``domains`` field of the
:class:`openmc.IndependentSource` Python class). Similar to Monte Carlo sources,
for two-dimensional problems (e.g., a 2D pincell) it is desirable to make the
source bounded near the origin of the infinite dimension. An example of an
acceptable ray source for a two-dimensional 2x2 lattice would look like:
::
pitch = 1.26
lower_left = (-pitch, -pitch, -pitch)
upper_right = ( pitch, pitch, pitch)
uniform_dist = openmc.stats.Box(lower_left, upper_right)
settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist)
.. note::
The random ray source is not related to the underlying particle flux or
source distribution of the simulation problem. It is akin to the selection
of an integration quadrature. Thus, in fixed source mode, the ray source
still needs to be provided and still needs to be uniform in space and angle
throughout the simulation domain. In fixed source mode, the user will
provide physical particle fixed sources in addition to the random ray
source.
.. _subdivision_fsr:
----------------------------------
Subdivision of Flat Source Regions
----------------------------------
While the scattering and fission sources in Monte Carlo
are treated continuously, they are assumed to be invariant (flat) within a
MOC or random ray flat source region (FSR). This introduces bias into the
simulation, which can be remedied by reducing the physical size of the FSR
to dimensions below that of typical mean free paths of particles.
In OpenMC, this subdivision currently must be done manually. The level of
subdivision needed will be dependent on the fidelity the user requires. For
typical light water reactor analysis, consider the following example subdivision
of a two-dimensional 2x2 reflective pincell lattice:
.. figure:: ../_images/2x2_materials.jpeg
:class: with-border
:width: 400
Material definition for an asymmetrical 2x2 lattice (1.26 cm pitch)
.. figure:: ../_images/2x2_fsrs.jpeg
:class: with-border
:width: 400
FSR decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch)
In the future, automated subdivision of FSRs via mesh overlay may be supported.
-------
Tallies
-------
Most tallies, filters, and scores that you would expect to work with a
multigroup solver like random ray are supported. For example, you can define 3D
mesh tallies with energy filters and flux, fission, and nu-fission scores, etc.
There are some restrictions though. For starters, it is assumed that all filter
mesh boundaries will conform to physical surface boundaries (or lattice
boundaries) in the simulation geometry. It is acceptable for multiple cells
(FSRs) to be contained within a mesh element (e.g., pincell-level or
assembly-level tallies should work), but it is currently left as undefined
behavior if a single simulation cell is contained in multiple mesh elements.
Supported scores:
- flux
- total
- fission
- nu-fission
- events
Supported Estimators:
- tracklength
Supported Filters:
- cell
- cell instance
- distribcell
- energy
- material
- mesh
- universe
Note that there is no difference between the analog, tracklength, and collision
estimators in random ray mode as individual particles are not being simulated.
Tracklength-style tally estimation is inherent to the random ray method.
--------
Plotting
--------
Visualization of geometry is handled in the same way as normal with OpenMC (see
:ref:`plotting guide <usersguide_plots>` for more details). That is, ``openmc
--plot`` is handled without any modifications, as the random ray solver uses the
same geometry definition as in Monte Carlo.
In addition to OpenMC's standard geometry plotting mode, the random ray solver
also features an additional method of data visualization. If a ``plots.xml``
file is present, any voxel plots that are defined will be output at the end of a
random ray simulation. Rather than being stored in HDF5 file format, the random
ray plotting will generate ``.vtk`` files that can be directly read and plotted
with `Paraview <https://www.paraview.org/>`_.
In fixed source Monte Carlo (MC) simulations, by default the only thing global
tally provided is the leakage fraction. In a k-eigenvalue MC simulation, by
default global tallies are collected for the eigenvalue and leakage fraction.
Spatial flux information must be manually requested, and often fine-grained
spatial meshes are considered costly/unnecessary, so it is impractical in MC
mode to plot spatial flux or power info by default. Conversely, in random ray,
the solver functions by estimating the multigroup source and flux spectrums in
every fine-grained FSR each iteration. Thus, for random ray, in both fixed
source and eigenvalue simulations, the simulation always finishes with a well
converged flux estimate for all areas. As such, it is much more common in random
ray, MOC, and other deterministic codes to provide spatial flux information by
default. In the future, all FSR data will be made available in the statepoint
file, which facilitates plotting and manipulation through the Python API; at
present, statepoint support is not available.
Only voxel plots will be used to generate output; other plot types present in
the ``plots.xml`` file will be ignored. The following fields will be written to
the VTK structured grid file:
- material
- FSR index
- flux spectrum (for each energy group)
- total fission source (integrated across all energy groups)
------------------------------------------
Inputting Multigroup Cross Sections (MGXS)
------------------------------------------
Multigroup cross sections for use with OpenMC's random ray solver are input the
same way as with OpenMC's traditional multigroup Monte Carlo mode. There is more
information on generating multigroup cross sections via OpenMC in the
:ref:`multigroup materials <create_mgxs>` user guide. You may also wish to
use an existing multigroup library. An example of using OpenMC's Python
interface to generate a correctly formatted ``mgxs.h5`` input file is given
in the `OpenMC Jupyter notebook collection
<https://nbviewer.org/github/openmc-dev/openmc-notebooks/blob/main/mg-mode-part-i.ipynb>`_.
.. note::
Currently only isotropic and isothermal multigroup cross sections are
supported in random ray mode. To represent multiple material temperatures,
separate materials can be defined each with a separate multigroup dataset
corresponding to a given temperature.
---------------------------------------
Putting it All Together: Example Inputs
---------------------------------------
An example of a settings definition for random ray is given below::
# Geometry and MGXS material definition of 2x2 lattice (not shown)
pitch = 1.26
group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6]
...
# Instantiate a settings object for a random ray solve
settings = openmc.Settings()
settings.energy_mode = "multi-group"
settings.batches = 1200
settings.inactive = 600
settings.particles = 2000
settings.random_ray['distance_inactive'] = 40.0
settings.random_ray['distance_active'] = 400.0
# Create an initial uniform spatial source distribution for sampling rays
lower_left = (-pitch, -pitch, -pitch)
upper_right = ( pitch, pitch, pitch)
uniform_dist = openmc.stats.Box(lower_left, upper_right)
settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist)
settings.export_to_xml()
# Define tallies
# Create a mesh filter
mesh = openmc.RegularMesh()
mesh.dimension = (2, 2)
mesh.lower_left = (-pitch/2, -pitch/2)
mesh.upper_right = (pitch/2, pitch/2)
mesh_filter = openmc.MeshFilter(mesh)
# Create a multigroup energy filter
energy_filter = openmc.EnergyFilter(group_edges)
# Create tally using our two filters and add scores
tally = openmc.Tally()
tally.filters = [mesh_filter, energy_filter]
tally.scores = ['flux', 'fission', 'nu-fission']
# Instantiate a Tallies collection and export to XML
tallies = openmc.Tallies([tally])
tallies.export_to_xml()
# Create voxel plot
plot = openmc.Plot()
plot.origin = [0, 0, 0]
plot.width = [2*pitch, 2*pitch, 1]
plot.pixels = [1000, 1000, 1]
plot.type = 'voxel'
# Instantiate a Plots collection and export to XML
plots = openmc.Plots([plot])
plots.export_to_xml()
All other inputs (e.g., geometry, materials) will be unchanged from a typical
Monte Carlo run (see the :ref:`geometry <usersguide_geometry>` and
:ref:`multigroup materials <create_mgxs>` user guides for more information).
There is also a complete example of a pincell available in the
``openmc/examples/pincell_random_ray`` folder.

View file

@ -0,0 +1,203 @@
import numpy as np
import openmc
import openmc.mgxs
###############################################################################
# Create multigroup data
# Instantiate the energy group data
group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6]
groups = openmc.mgxs.EnergyGroups(group_edges)
# Instantiate the 7-group (C5G7) cross section data
uo2_xsdata = openmc.XSdata('UO2', groups)
uo2_xsdata.order = 0
uo2_xsdata.set_total(
[0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678,
0.5644058])
uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02,
3.0020e-02, 1.1126e-01, 2.8278e-01])
scatter_matrix = np.array(
[[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]])
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
uo2_xsdata.set_scatter_matrix(scatter_matrix)
uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03,
1.85648e-02, 1.78084e-02, 8.30348e-02,
2.16004e-01])
uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02,
4.518301e-02, 4.334208e-02, 2.020901e-01,
5.257105e-01])
uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00,
0.0000e+00, 0.0000e+00])
h2o_xsdata = openmc.XSdata('LWTR', groups)
h2o_xsdata.order = 0
h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435,
0.718, 1.2544497, 2.650379])
h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04,
1.9406e-03, 5.7416e-03, 1.5001e-02,
3.7239e-02])
scatter_matrix = np.array(
[[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000],
[0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010],
[0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034],
[0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390],
[0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]])
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
h2o_xsdata.set_scatter_matrix(scatter_matrix)
mg_cross_sections_file = openmc.MGXSLibrary(groups)
mg_cross_sections_file.add_xsdatas([uo2_xsdata, h2o_xsdata])
mg_cross_sections_file.export_to_hdf5()
###############################################################################
# Create materials for the problem
# Instantiate some Materials and register the appropriate macroscopic data
uo2 = openmc.Material(name='UO2 fuel')
uo2.set_density('macro', 1.0)
uo2.add_macroscopic('UO2')
water = openmc.Material(name='Water')
water.set_density('macro', 1.0)
water.add_macroscopic('LWTR')
# Instantiate a Materials collection and export to XML
materials_file = openmc.Materials([uo2, water])
materials_file.cross_sections = "mgxs.h5"
materials_file.export_to_xml()
###############################################################################
# Define problem geometry
# The geometry we will define a simplified pincell with fuel radius 0.54 cm
# surrounded by moderator (same as in the multigroup example).
# In random ray, we typically want several radial regions and azimuthal
# sectors in both the fuel and moderator areas of the pincell. This is
# due to the flat source approximation requiring that source regions are
# small compared to the typical mean free path of a neutron. Below we
# sudivide the basic pincell into 8 aziumthal sectors (pizza slices) and
# 5 concentric rings in both the fuel and moderator.
# TODO: When available in OpenMC, use cylindrical lattice instead to
# simplify definition and improve runtime performance.
pincell_base = openmc.Universe()
# These are the subdivided radii (creating 5 concentric regions in the
# fuel and moderator)
ring_radii = [0.241, 0.341, 0.418, 0.482, 0.54, 0.572, 0.612, 0.694, 0.786]
fills = [uo2, uo2, uo2, uo2, uo2, water, water, water, water, water]
# We then create cells representing the bounded rings, with special
# treatment for both the innermost and outermost cells
cells = []
for r in range(10):
cell = []
if r == 0:
outer_bound = openmc.ZCylinder(r=ring_radii[r])
cell = openmc.Cell(fill=fills[r], region=-outer_bound)
elif r == 9:
inner_bound = openmc.ZCylinder(r=ring_radii[r-1])
cell = openmc.Cell(fill=fills[r], region=+inner_bound)
else:
inner_bound = openmc.ZCylinder(r=ring_radii[r-1])
outer_bound = openmc.ZCylinder(r=ring_radii[r])
cell = openmc.Cell(fill=fills[r], region=+inner_bound & -outer_bound)
pincell_base.add_cell(cell)
# We then generate 8 planes to bound 8 azimuthal sectors
azimuthal_planes = []
for i in range(8):
angle = 2 * i * openmc.pi / 8
normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0)
azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0))
# Create a cell for each azimuthal sector using the pincell base class
azimuthal_cells = []
for i in range(8):
azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}')
azimuthal_cell.fill = pincell_base
azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8]
azimuthal_cells.append(azimuthal_cell)
# Create the (subdivided) geometry with the azimuthal universes
pincell = openmc.Universe(cells=azimuthal_cells)
# Create a region represented as the inside of a rectangular prism
pitch = 1.26
box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective')
pincell_bounded = openmc.Cell(fill=pincell, region=-box, name='pincell')
# Create a geometry (specifying merge surfaces option to remove
# all the redundant cylinder/plane surfaces) and export to XML
geometry = openmc.Geometry([pincell_bounded], merge_surfaces=True)
geometry.export_to_xml()
###############################################################################
# Define problem settings
# Instantiate a Settings object, set all runtime parameters, and export to XML
settings = openmc.Settings()
settings.energy_mode = "multi-group"
settings.batches = 600
settings.inactive = 300
settings.particles = 50
# Create an initial uniform spatial source distribution for sampling rays.
# Note that this must be uniform in space and angle.
lower_left = (-pitch/2, -pitch/2, -1)
upper_right = (pitch/2, pitch/2, 1)
uniform_dist = openmc.stats.Box(lower_left, upper_right)
settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist)
settings.random_ray['distance_inactive'] = 40.0
settings.random_ray['distance_active'] = 400.0
settings.export_to_xml()
###############################################################################
# Define tallies
# Create a mesh that will be used for tallying
mesh = openmc.RegularMesh()
mesh.dimension = (2, 2)
mesh.lower_left = (-pitch/2, -pitch/2)
mesh.upper_right = (pitch/2, pitch/2)
# Create a mesh filter that can be used in a tally
mesh_filter = openmc.MeshFilter(mesh)
# Let's also create a filter to measure each group
# indepdendently
energy_filter = openmc.EnergyFilter(group_edges)
# Now use the mesh filter in a tally and indicate what scores are desired
tally = openmc.Tally(name="Mesh and Energy tally")
tally.filters = [mesh_filter, energy_filter]
tally.scores = ['flux', 'fission', 'nu-fission']
# Instantiate a Tallies collection and export to XML
tallies = openmc.Tallies([tally])
tallies.export_to_xml()
###############################################################################
# Exporting to OpenMC plots.xml file
###############################################################################
plot = openmc.Plot()
plot.origin = [0, 0, 0]
plot.width = [pitch, pitch, pitch]
plot.pixels = [1000, 1000, 1]
plot.type = 'voxel'
# Instantiate a Plots collection and export to XML
plots = openmc.Plots([plot])
plots.export_to_xml()

View file

@ -10,6 +10,7 @@ namespace openmc {
// Forward declare some types used in function arguments.
class Particle;
class RandomRay;
class Surface;
//==============================================================================

View file

@ -249,6 +249,42 @@ public:
std::unordered_map<int32_t, vector<int32_t>> get_contained_cells(
int32_t instance = 0, Position* hint = nullptr) const;
//! Determine the material index corresponding to a specific cell instance,
//! taking into account presence of distribcell material
//! \param[in] instance of the cell
//! \return material index
int32_t material(int32_t instance) const
{
// If distributed materials are used, then each instance has its own
// material definition. If distributed materials are not used, then
// all instances used the same material stored at material_[0]. The
// presence of distributed materials is inferred from the size of
// the material_ vector being greater than one.
if (material_.size() > 1) {
return material_[instance];
} else {
return material_[0];
}
}
//! Determine the temperature index corresponding to a specific cell instance,
//! taking into account presence of distribcell temperature
//! \param[in] instance of the cell
//! \return temperature index
double sqrtkT(int32_t instance) const
{
// If distributed materials are used, then each instance has its own
// temperature definition. If distributed materials are not used, then
// all instances used the same temperature stored at sqrtkT_[0]. The
// presence of distributed materials is inferred from the size of
// the sqrtkT_ vector being greater than one.
if (sqrtkT_.size() > 1) {
return sqrtkT_[instance];
} else {
return sqrtkT_[0];
}
}
protected:
//! Determine the path to this cell instance in the geometry hierarchy
//! \param[in] instance of the cell to find parent cells for

View file

@ -340,6 +340,8 @@ enum class RunMode {
VOLUME
};
enum class SolverType { MONTE_CARLO, RANDOM_RAY };
//==============================================================================
// Geometry Constants

View file

@ -29,7 +29,6 @@ private:
int num_delayed_groups; // number of delayed neutron groups
vector<XsData> xs; // Cross section data
// MGXS Incoming Flux Angular grid information
bool is_isotropic; // used to skip search for angle indices if isotropic
int n_pol;
int n_azi;
vector<double> polar;
@ -85,6 +84,8 @@ public:
std::string name; // name of dataset, e.g., UO2
double awr; // atomic weight ratio
bool fissionable; // Is this fissionable
bool is_isotropic {
true}; // used to skip search for angle indices if isotropic
Mgxs() = default;

View file

@ -57,6 +57,8 @@ void print_results();
void write_tallies();
void show_time(const char* label, double secs, int indent_level = 0);
} // namespace openmc
#endif // OPENMC_OUTPUT_H
@ -80,4 +82,4 @@ struct formatter<std::array<T, 2>> {
}
};
} // namespace fmt
} // namespace fmt

View file

@ -100,6 +100,7 @@ public:
virtual void print_info() const = 0;
const std::string& path_plot() const { return path_plot_; }
std::string& path_plot() { return path_plot_; }
int id() const { return id_; }
int level() const { return level_; }

View file

@ -0,0 +1,141 @@
#ifndef OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H
#define OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H
#include "openmc/openmp_interface.h"
#include "openmc/position.h"
namespace openmc {
/*
* The FlatSourceDomain class encompasses data and methods for storing
* scalar flux and source region for all flat source regions in a
* random ray simulation domain.
*/
class FlatSourceDomain {
public:
//----------------------------------------------------------------------------
// Helper Structs
// A mapping object that is used to map between a specific random ray
// source region and an OpenMC native tally bin that it should score to
// every iteration.
struct TallyTask {
int tally_idx;
int filter_idx;
int score_idx;
int score_type;
TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type)
: tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx),
score_type(score_type)
{}
};
//----------------------------------------------------------------------------
// Constructors
FlatSourceDomain();
//----------------------------------------------------------------------------
// Methods
void update_neutron_source(double k_eff);
double compute_k_eff(double k_eff_old) const;
void normalize_scalar_flux_and_volumes(
double total_active_distance_per_iteration);
int64_t add_source_to_scalar_flux();
void batch_reset();
void convert_source_regions_to_tallies();
void random_ray_tally() const;
void accumulate_iteration_flux();
void output_to_vtk() const;
void all_reduce_replicated_source_regions();
//----------------------------------------------------------------------------
// Public Data members
bool mapped_all_tallies_ {false}; // If all source regions have been visited
int64_t n_source_regions_ {0}; // Total number of source regions in the model
// 1D array representing source region starting offset for each OpenMC Cell
// in model::cells
vector<int64_t> source_region_offsets_;
// 1D arrays representing values for all source regions
vector<OpenMPMutex> lock_;
vector<int> was_hit_;
vector<double> volume_;
vector<int> position_recorded_;
vector<Position> position_;
// 2D arrays stored in 1D representing values for all source regions x energy
// groups
vector<float> scalar_flux_old_;
vector<float> scalar_flux_new_;
vector<float> source_;
//----------------------------------------------------------------------------
// Private data members
private:
int negroups_; // Number of energy groups in simulation
int64_t n_source_elements_ {0}; // Total number of source regions in the model
// times the number of energy groups
// 2D array representing values for all source regions x energy groups x tally
// tasks
vector<vector<TallyTask>> tally_task_;
// 1D arrays representing values for all source regions
vector<int> material_;
vector<double> volume_t_;
// 2D arrays stored in 1D representing values for all source regions x energy
// groups
vector<float> scalar_flux_final_;
}; // class FlatSourceDomain
//============================================================================
//! Non-member functions
//============================================================================
// Returns the inputted value in big endian byte ordering. If the system is
// little endian, the byte ordering is flipped. If the system is big endian,
// the inputted value is returned as is. This function is necessary as
// .vtk binary files use big endian byte ordering.
template<typename T>
T convert_to_big_endian(T in)
{
// 4 byte integer
uint32_t test = 1;
// 1 byte pointer to first byte of test integer
uint8_t* ptr = reinterpret_cast<uint8_t*>(&test);
// If the first byte of test is 0, then the system is big endian. In this
// case, we don't have to do anything as .vtk files are big endian
if (*ptr == 0)
return in;
// Otherwise, the system is in little endian, so we need to flip the
// endianness
uint8_t* orig = reinterpret_cast<uint8_t*>(&in);
uint8_t swapper[sizeof(T)];
for (int i = 0; i < sizeof(T); i++) {
swapper[i] = orig[sizeof(T) - i - 1];
}
T out = *reinterpret_cast<T*>(&swapper);
return out;
}
template<typename T>
void parallel_fill(vector<T>& arr, T value)
{
#pragma omp parallel for schedule(static)
for (int i = 0; i < arr.size(); i++) {
arr[i] = value;
}
}
} // namespace openmc
#endif // OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H

View file

@ -0,0 +1,55 @@
#ifndef OPENMC_RANDOM_RAY_H
#define OPENMC_RANDOM_RAY_H
#include "openmc/memory.h"
#include "openmc/particle.h"
#include "openmc/random_ray/flat_source_domain.h"
#include "openmc/source.h"
namespace openmc {
/*
* The RandomRay class encompasses data and methods for transporting random rays
* through the model. It is a small extension of the Particle class.
*/
// TODO: Inherit from GeometryState instead of Particle
class RandomRay : public Particle {
public:
//----------------------------------------------------------------------------
// Constructors
RandomRay();
RandomRay(uint64_t ray_id, FlatSourceDomain* domain);
//----------------------------------------------------------------------------
// Methods
void event_advance_ray();
void attenuate_flux(double distance, bool is_active);
void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain);
uint64_t transport_history_based_single_ray();
//----------------------------------------------------------------------------
// Static data members
static double distance_inactive_; // Inactive (dead zone) ray length
static double distance_active_; // Active ray length
static unique_ptr<Source> ray_source_; // Starting source for ray sampling
//----------------------------------------------------------------------------
// Public data members
vector<float> angular_flux_;
//----------------------------------------------------------------------------
// Private data members
private:
FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source
// data needed for ray transport
vector<float> delta_psi_;
double distance_travelled_ {0};
int negroups_;
bool is_active_ {false};
bool is_alive_ {true};
}; // class RandomRay
} // namespace openmc
#endif // OPENMC_RANDOM_RAY_H

View file

@ -0,0 +1,59 @@
#ifndef OPENMC_RANDOM_RAY_SIMULATION_H
#define OPENMC_RANDOM_RAY_SIMULATION_H
#include "openmc/random_ray/flat_source_domain.h"
namespace openmc {
/*
* The RandomRaySimulation class encompasses data and methods for running a
* random ray simulation.
*/
class RandomRaySimulation {
public:
//----------------------------------------------------------------------------
// Constructors
RandomRaySimulation();
//----------------------------------------------------------------------------
// Methods
void simulate();
void reduce_simulation_statistics();
void output_simulation_results() const;
void instability_check(
int64_t n_hits, double k_eff, double& avg_miss_rate) const;
void print_results_random_ray(uint64_t total_geometric_intersections,
double avg_miss_rate, int negroups, int64_t n_source_regions) const;
//----------------------------------------------------------------------------
// Data members
private:
// Contains all flat source region data
FlatSourceDomain domain_;
// Random ray eigenvalue
double k_eff_ {1.0};
// Tracks the average FSR miss rate for analysis and reporting
double avg_miss_rate_ {0.0};
// Tracks the total number of geometric intersections by all rays for
// reporting
uint64_t total_geometric_intersections_ {0};
// Number of energy groups
int negroups_;
}; // class RandomRaySimulation
//============================================================================
//! Non-member functions
//============================================================================
void openmc_run_random_ray();
void validate_random_ray_inputs();
} // namespace openmc
#endif // OPENMC_RANDOM_RAY_SIMULATION_H

View file

@ -112,8 +112,9 @@ extern ResScatMethod res_scat_method; //!< resonance upscattering method
extern double res_scat_energy_min; //!< Min energy in [eV] for res. upscattering
extern double res_scat_energy_max; //!< Max energy in [eV] for res. upscattering
extern vector<std::string>
res_scat_nuclides; //!< Nuclides using res. upscattering treatment
extern RunMode run_mode; //!< Run mode (eigenvalue, fixed src, etc.)
res_scat_nuclides; //!< Nuclides using res. upscattering treatment
extern RunMode run_mode; //!< Run mode (eigenvalue, fixed src, etc.)
extern SolverType solver_type; //!< Solver Type (Monte Carlo or Random Ray)
extern std::unordered_set<int>
sourcepoint_batch; //!< Batches when source should be written
extern std::unordered_set<int>
@ -139,6 +140,7 @@ extern int trigger_batch_interval; //!< Batch interval for triggers
extern "C" int verbosity; //!< How verbose to make output
extern double weight_cutoff; //!< Weight cutoff for Russian roulette
extern double weight_survive; //!< Survival weight after Russian roulette
} // namespace settings
//==============================================================================

View file

@ -31,6 +31,7 @@ extern Timer time_event_advance_particle;
extern Timer time_event_surface_crossing;
extern Timer time_event_collision;
extern Timer time_event_death;
extern Timer time_update_src;
} // namespace simulation

View file

@ -86,7 +86,8 @@ class Settings:
.. versionadded:: 0.12
rel_max_lost_particles : float
Maximum number of lost particles, relative to the total number of particles
Maximum number of lost particles, relative to the total number of
particles
.. versionadded:: 0.12
inactive : int
@ -146,6 +147,18 @@ class Settings:
Initial seed for randomly generated plot colors.
ptables : bool
Determine whether probability tables are used.
random_ray : dict
Options for configuring the random ray solver. Acceptable keys are:
:distance_inactive:
Indicates the total active distance in [cm] a ray should travel
:distance_active:
Indicates the total active distance in [cm] a ray should travel
:ray_source:
Starting ray distribution (must be uniform in space and angle) as
specified by a :class:`openmc.SourceBase` object.
.. versionadded:: 0.14.1
resonance_scattering : dict
Settings for resonance elastic scattering. Accepted keys are 'enable'
(bool), 'method' (str), 'energy_min' (float), 'energy_max' (float), and
@ -153,10 +166,10 @@ class Settings:
rejection correction) or 'rvs' (relative velocity sampling). If not
specified, 'rvs' is the default method. The 'energy_min' and
'energy_max' values indicate the minimum and maximum energies above and
below which the resonance elastic scattering method is to be
applied. The 'nuclides' list indicates what nuclides the method should
be applied to. In its absence, the method will be applied to all
nuclides with 0 K elastic scattering data present.
below which the resonance elastic scattering method is to be applied.
The 'nuclides' list indicates what nuclides the method should be applied
to. In its absence, the method will be applied to all nuclides with 0 K
elastic scattering data present.
run_mode : {'eigenvalue', 'fixed source', 'plot', 'volume', 'particle restart'}
The type of calculation to perform (default is 'eigenvalue')
seed : int
@ -185,26 +198,26 @@ class Settings:
:surface_ids: List of surface ids at which crossing particles are to be
banked (int)
:max_particles: Maximum number of particles to be banked on
surfaces per process (int)
:max_particles: Maximum number of particles to be banked on surfaces per
process (int)
:mcpl: Output in the form of an MCPL-file (bool)
survival_biasing : bool
Indicate whether survival biasing is to be used
tabular_legendre : dict
Determines if a multi-group scattering moment kernel expanded via
Legendre polynomials is to be converted to a tabular distribution or
not. Accepted keys are 'enable' and 'num_points'. The value for
'enable' is a bool stating whether the conversion to tabular is
performed; the value for 'num_points' sets the number of points to use
in the tabular distribution, should 'enable' be True.
not. Accepted keys are 'enable' and 'num_points'. The value for 'enable'
is a bool stating whether the conversion to tabular is performed; the
value for 'num_points' sets the number of points to use in the tabular
distribution, should 'enable' be True.
temperature : dict
Defines a default temperature and method for treating intermediate
temperatures at which nuclear data doesn't exist. Accepted keys are
'default', 'method', 'range', 'tolerance', and 'multipole'. The value
for 'default' should be a float representing the default temperature in
Kelvin. The value for 'method' should be 'nearest' or 'interpolation'.
If the method is 'nearest', 'tolerance' indicates a range of
temperature within which cross sections may be used. If the method is
If the method is 'nearest', 'tolerance' indicates a range of temperature
within which cross sections may be used. If the method is
'interpolation', 'tolerance' indicates the range of temperatures outside
of the available cross section temperatures where cross sections will
evaluate to the nearer bound. The value for 'range' should be a pair of
@ -348,6 +361,8 @@ class Settings:
self._max_splits = None
self._max_tracks = None
self._random_ray = {}
for key, value in kwargs.items():
setattr(self, key, value)
@ -1030,6 +1045,31 @@ class Settings:
wwgs = [wwgs]
self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators', wwgs)
@property
def random_ray(self) -> dict:
return self._random_ray
@random_ray.setter
def random_ray(self, random_ray: dict):
if not isinstance(random_ray, Mapping):
raise ValueError(f'Unable to set random_ray from "{random_ray}" '
'which is not a dict.')
for key in random_ray:
if key == 'distance_active':
cv.check_type('active ray length', random_ray[key], Real)
cv.check_greater_than('active ray length', random_ray[key], 0.0)
elif key == 'distance_inactive':
cv.check_type('inactive ray length', random_ray[key], Real)
cv.check_greater_than('inactive ray length',
random_ray[key], 0.0, True)
elif key == 'ray_source':
cv.check_type('random ray source', random_ray[key], SourceBase)
else:
raise ValueError(f'Unable to set random ray to "{key}" which is '
'unsupported by OpenMC')
self._random_ray = random_ray
def _create_run_mode_subelement(self, root):
elem = ET.SubElement(root, "run_mode")
elem.text = self._run_mode.value
@ -1442,6 +1482,17 @@ class Settings:
elem = ET.SubElement(root, "max_tracks")
elem.text = str(self._max_tracks)
def _create_random_ray_subelement(self, root):
if self._random_ray:
element = ET.SubElement(root, "random_ray")
for key, value in self._random_ray.items():
if key == 'ray_source' and isinstance(value, SourceBase):
source_element = value.to_xml_element()
element.append(source_element)
else:
subelement = ET.SubElement(element, key)
subelement.text = str(value)
def _eigenvalue_from_xml_element(self, root):
elem = root.find('eigenvalue')
if elem is not None:
@ -1793,6 +1844,17 @@ class Settings:
if text is not None:
self.max_tracks = int(text)
def _random_ray_from_xml_element(self, root):
elem = root.find('random_ray')
if elem is not None:
self.random_ray = {}
for child in elem:
if child.tag in ('distance_inactive', 'distance_active'):
self.random_ray[child.tag] = float(child.text)
elif child.tag == 'source':
source = SourceBase.from_xml_element(child)
self.random_ray['ray_source'] = source
def to_xml_element(self, mesh_memo=None):
"""Create a 'settings' element to be written to an XML file.
@ -1855,6 +1917,7 @@ class Settings:
self._create_weight_window_checkpoints_subelement(element)
self._create_max_splits_subelement(element)
self._create_max_tracks_subelement(element)
self._create_random_ray_subelement(element)
# Clean the indentation in the file to be user-readable
clean_indentation(element)
@ -1958,6 +2021,7 @@ class Settings:
settings._weight_window_checkpoints_from_xml_element(elem)
settings._max_splits_from_xml_element(elem)
settings._max_tracks_from_xml_element(elem)
settings._random_ray_from_xml_element(elem)
# TODO: Get volume calculations
return settings

View file

@ -6,6 +6,7 @@
#include "openmc/constants.h"
#include "openmc/error.h"
#include "openmc/random_ray/random_ray.h"
#include "openmc/surface.h"
namespace openmc {
@ -16,7 +17,18 @@ namespace openmc {
void VacuumBC::handle_particle(Particle& p, const Surface& surf) const
{
p.cross_vacuum_bc(surf);
// Random ray and Monte Carlo need different treatments at vacuum BCs
if (settings::solver_type == SolverType::RANDOM_RAY) {
// Reflect ray off of the surface
ReflectiveBC().handle_particle(p, surf);
// Set ray's angular flux spectrum to vacuum conditions (zero)
RandomRay* r = static_cast<RandomRay*>(&p);
std::fill(r->angular_flux_.begin(), r->angular_flux_.end(), 0.0);
} else {
p.cross_vacuum_bc(surf);
}
}
//==============================================================================

View file

@ -57,16 +57,28 @@ void calculate_generation_keff()
double keff_reduced;
#ifdef OPENMC_MPI
// Combine values across all processors
MPI_Allreduce(&simulation::keff_generation, &keff_reduced, 1, MPI_DOUBLE,
MPI_SUM, mpi::intracomm);
if (settings::solver_type != SolverType::RANDOM_RAY) {
// Combine values across all processors
MPI_Allreduce(&simulation::keff_generation, &keff_reduced, 1, MPI_DOUBLE,
MPI_SUM, mpi::intracomm);
} else {
// If using random ray, MPI parallelism is provided by domain replication.
// As such, all fluxes will be reduced at the end of each transport sweep,
// such that all ranks have identical scalar flux vectors, and will all
// independently compute the same value of k. Thus, there is no need to
// perform any additional MPI reduction here.
keff_reduced = simulation::keff_generation;
}
#else
keff_reduced = simulation::keff_generation;
#endif
// Normalize single batch estimate of k
// TODO: This should be normalized by total_weight, not by n_particles
keff_reduced /= settings::n_particles;
if (settings::solver_type != SolverType::RANDOM_RAY) {
keff_reduced /= settings::n_particles;
}
simulation::k_generation.push_back(keff_reduced);
}
@ -370,7 +382,8 @@ int openmc_get_keff(double* k_combined)
// Special case for n <=3. Notice that at the end,
// there is a N-3 term in a denominator.
if (simulation::n_realizations <= 3) {
if (simulation::n_realizations <= 3 ||
settings::solver_type == SolverType::RANDOM_RAY) {
k_combined[0] = simulation::keff;
k_combined[1] = simulation::keff_std;
if (simulation::n_realizations <= 1) {

View file

@ -174,17 +174,9 @@ bool find_cell_inner(
// Set the material and temperature.
p.material_last() = p.material();
if (c.material_.size() > 1) {
p.material() = c.material_[p.cell_instance()];
} else {
p.material() = c.material_[0];
}
p.material() = c.material(p.cell_instance());
p.sqrtkT_last() = p.sqrtkT();
if (c.sqrtkT_.size() > 1) {
p.sqrtkT() = c.sqrtkT_[p.cell_instance()];
} else {
p.sqrtkT() = c.sqrtkT_[0];
}
p.sqrtkT() = c.sqrtkT(p.cell_instance());
return true;

View file

@ -6,6 +6,7 @@
#include "openmc/error.h"
#include "openmc/message_passing.h"
#include "openmc/particle_restart.h"
#include "openmc/random_ray/random_ray_simulation.h"
#include "openmc/settings.h"
int main(int argc, char* argv[])
@ -31,7 +32,15 @@ int main(int argc, char* argv[])
switch (settings::run_mode) {
case RunMode::FIXED_SOURCE:
case RunMode::EIGENVALUE:
err = openmc_run();
switch (settings::solver_type) {
case SolverType::MONTE_CARLO:
err = openmc_run();
break;
case SolverType::RANDOM_RAY:
openmc_run_random_ray();
err = 0;
break;
}
break;
case RunMode::PLOTTING:
err = openmc_plot_geometry();

View file

@ -582,7 +582,7 @@ void StructuredMesh::raytrace_mesh(
// Compute the length of the entire track.
double total_distance = (r1 - r0).norm();
if (total_distance == 0.0)
if (total_distance == 0.0 && settings::solver_type != SolverType::RANDOM_RAY)
return;
const int n = n_dimension_;

View file

@ -31,6 +31,7 @@
#include "openmc/mgxs_interface.h"
#include "openmc/nuclide.h"
#include "openmc/plot.h"
#include "openmc/random_ray/flat_source_domain.h"
#include "openmc/reaction.h"
#include "openmc/settings.h"
#include "openmc/simulation.h"
@ -409,7 +410,7 @@ void print_generation()
//==============================================================================
void show_time(const char* label, double secs, int indent_level = 0)
void show_time(const char* label, double secs, int indent_level)
{
int width = 33 - indent_level * 2;
fmt::print("{0:{1}} {2:<{3}} = {4:>10.4e} seconds\n", "", 2 * indent_level,

View file

@ -0,0 +1,686 @@
#include "openmc/random_ray/flat_source_domain.h"
#include "openmc/cell.h"
#include "openmc/geometry.h"
#include "openmc/message_passing.h"
#include "openmc/mgxs_interface.h"
#include "openmc/output.h"
#include "openmc/plot.h"
#include "openmc/random_ray/random_ray.h"
#include "openmc/simulation.h"
#include "openmc/tallies/filter.h"
#include "openmc/tallies/tally.h"
#include "openmc/tallies/tally_scoring.h"
#include "openmc/timer.h"
#include <cstdio>
namespace openmc {
//==============================================================================
// FlatSourceDomain implementation
//==============================================================================
FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_)
{
// Count the number of source regions, compute the cell offset
// indices, and store the material type The reason for the offsets is that
// some cell types may not have material fills, and therefore do not
// produce FSRs. Thus, we cannot index into the global arrays directly
for (const auto& c : model::cells) {
if (c->type_ != Fill::MATERIAL) {
source_region_offsets_.push_back(-1);
} else {
source_region_offsets_.push_back(n_source_regions_);
n_source_regions_ += c->n_instances_;
n_source_elements_ += c->n_instances_ * negroups_;
}
}
// Initialize cell-wise arrays
lock_.resize(n_source_regions_);
material_.resize(n_source_regions_);
position_recorded_.assign(n_source_regions_, 0);
position_.resize(n_source_regions_);
volume_.assign(n_source_regions_, 0.0);
volume_t_.assign(n_source_regions_, 0.0);
was_hit_.assign(n_source_regions_, 0);
// Initialize element-wise arrays
scalar_flux_new_.assign(n_source_elements_, 0.0);
scalar_flux_old_.assign(n_source_elements_, 1.0);
scalar_flux_final_.assign(n_source_elements_, 0.0);
source_.resize(n_source_elements_);
tally_task_.resize(n_source_elements_);
// Initialize material array
int64_t source_region_id = 0;
for (int i = 0; i < model::cells.size(); i++) {
Cell& cell = *model::cells[i];
if (cell.type_ == Fill::MATERIAL) {
for (int j = 0; j < cell.n_instances_; j++) {
material_[source_region_id++] = cell.material(j);
}
}
}
// Sanity check
if (source_region_id != n_source_regions_) {
fatal_error("Unexpected number of source regions");
}
}
void FlatSourceDomain::batch_reset()
{
// Reset scalar fluxes, iteration volume tallies, and region hit flags to
// zero
parallel_fill<float>(scalar_flux_new_, 0.0f);
parallel_fill<double>(volume_, 0.0);
parallel_fill<int>(was_hit_, 0);
}
void FlatSourceDomain::accumulate_iteration_flux()
{
#pragma omp parallel for
for (int64_t se = 0; se < n_source_elements_; se++) {
scalar_flux_final_[se] += scalar_flux_new_[se];
}
}
// Compute new estimate of scattering + fission sources in each source region
// based on the flux estimate from the previous iteration.
void FlatSourceDomain::update_neutron_source(double k_eff)
{
simulation::time_update_src.start();
double inverse_k_eff = 1.0 / k_eff;
// Temperature and angle indices, if using multiple temperature
// data sets and/or anisotropic data sets.
// TODO: Currently assumes we are only using single temp/single
// angle data.
const int t = 0;
const int a = 0;
#pragma omp parallel for
for (int sr = 0; sr < n_source_regions_; sr++) {
int material = material_[sr];
for (int e_out = 0; e_out < negroups_; e_out++) {
float sigma_t = data::mg.macro_xs_[material].get_xs(
MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a);
float scatter_source = 0.0f;
float fission_source = 0.0f;
for (int e_in = 0; e_in < negroups_; e_in++) {
float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in];
float sigma_s = data::mg.macro_xs_[material].get_xs(
MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a);
float nu_sigma_f = data::mg.macro_xs_[material].get_xs(
MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a);
float chi = data::mg.macro_xs_[material].get_xs(
MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a);
scatter_source += sigma_s * scalar_flux;
fission_source += nu_sigma_f * scalar_flux * chi;
}
fission_source *= inverse_k_eff;
float new_isotropic_source = (scatter_source + fission_source) / sigma_t;
source_[sr * negroups_ + e_out] = new_isotropic_source;
}
}
simulation::time_update_src.stop();
}
// Normalizes flux and updates simulation-averaged volume estimate
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
double total_active_distance_per_iteration)
{
float normalization_factor = 1.0 / total_active_distance_per_iteration;
double volume_normalization_factor =
1.0 / (total_active_distance_per_iteration * simulation::current_batch);
// Normalize scalar flux to total distance travelled by all rays this iteration
#pragma omp parallel for
for (int64_t e = 0; e < scalar_flux_new_.size(); e++) {
scalar_flux_new_[e] *= normalization_factor;
}
// Accumulate cell-wise ray length tallies collected this iteration, then
// update the simulation-averaged cell-wise volume estimates
#pragma omp parallel for
for (int64_t sr = 0; sr < n_source_regions_; sr++) {
volume_t_[sr] += volume_[sr];
volume_[sr] = volume_t_[sr] * volume_normalization_factor;
}
}
// Combine transport flux contributions and flat source contributions from the
// previous iteration to generate this iteration's estimate of scalar flux.
int64_t FlatSourceDomain::add_source_to_scalar_flux()
{
int64_t n_hits = 0;
// Temperature and angle indices, if using multiple temperature
// data sets and/or anisotropic data sets.
// TODO: Currently assumes we are only using single temp/single
// angle data.
const int t = 0;
const int a = 0;
#pragma omp parallel for reduction(+ : n_hits)
for (int sr = 0; sr < n_source_regions_; sr++) {
// Check if this cell was hit this iteration
int was_cell_hit = was_hit_[sr];
if (was_cell_hit) {
n_hits++;
}
double volume = volume_[sr];
int material = material_[sr];
for (int g = 0; g < negroups_; g++) {
int64_t idx = (sr * negroups_) + g;
// There are three scenarios we need to consider:
if (was_cell_hit) {
// 1. If the FSR was hit this iteration, then the new flux is equal to
// the flat source from the previous iteration plus the contributions
// from rays passing through the source region (computed during the
// transport sweep)
float sigma_t = data::mg.macro_xs_[material].get_xs(
MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a);
scalar_flux_new_[idx] /= (sigma_t * volume);
scalar_flux_new_[idx] += source_[idx];
} else if (volume > 0.0) {
// 2. If the FSR was not hit this iteration, but has been hit some
// previous iteration, then we simply set the new scalar flux to be
// equal to the contribution from the flat source alone.
scalar_flux_new_[idx] = source_[idx];
} else {
// If the FSR was not hit this iteration, and it has never been hit in
// any iteration (i.e., volume is zero), then we want to set this to 0
// to avoid dividing anything by a zero volume.
scalar_flux_new_[idx] = 0.0f;
}
}
}
// Return the number of source regions that were hit this iteration
return n_hits;
}
// Generates new estimate of k_eff based on the differences between this
// iteration's estimate of the scalar flux and the last iteration's estimate.
double FlatSourceDomain::compute_k_eff(double k_eff_old) const
{
double fission_rate_old = 0;
double fission_rate_new = 0;
// Temperature and angle indices, if using multiple temperature
// data sets and/or anisotropic data sets.
// TODO: Currently assumes we are only using single temp/single
// angle data.
const int t = 0;
const int a = 0;
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
for (int sr = 0; sr < n_source_regions_; sr++) {
// If simulation averaged volume is zero, don't include this cell
double volume = volume_[sr];
if (volume == 0.0) {
continue;
}
int material = material_[sr];
double sr_fission_source_old = 0;
double sr_fission_source_new = 0;
for (int g = 0; g < negroups_; g++) {
int64_t idx = (sr * negroups_) + g;
double nu_sigma_f = data::mg.macro_xs_[material].get_xs(
MgxsType::NU_FISSION, g, nullptr, nullptr, nullptr, t, a);
sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx];
sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx];
}
fission_rate_old += sr_fission_source_old * volume;
fission_rate_new += sr_fission_source_new * volume;
}
double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old);
return k_eff_new;
}
// This function is responsible for generating a mapping between random
// ray flat source regions (cell instances) and tally bins. The mapping
// takes the form of a "TallyTask" object, which accounts for one single
// score being applied to a single tally. Thus, a single source region
// may have anywhere from zero to many tally tasks associated with it ---
// meaning that the global "tally_task" data structure is in 2D. The outer
// dimension corresponds to the source element (i.e., each entry corresponds
// to a specific energy group within a specific source region), and the
// inner dimension corresponds to the tallying task itself. Mechanically,
// the mapping between FSRs and spatial filters is done by considering
// the location of a single known ray midpoint that passed through the
// FSR. I.e., during transport, the first ray to pass through a given FSR
// will write down its midpoint for use with this function. This is a cheap
// and easy way of mapping FSRs to spatial tally filters, but comes with
// the downside of adding the restriction that spatial tally filters must
// share boundaries with the physical geometry of the simulation (so as
// not to subdivide any FSR). It is acceptable for a spatial tally region
// to contain multiple FSRs, but not the other way around.
// TODO: In future work, it would be preferable to offer a more general
// (but perhaps slightly more expensive) option for handling arbitrary
// spatial tallies that would be allowed to subdivide FSRs.
// Besides generating the mapping structure, this function also keeps track
// of whether or not all flat source regions have been hit yet. This is
// required, as there is no guarantee that all flat source regions will
// be hit every iteration, such that in the first few iterations some FSRs
// may not have a known position within them yet to facilitate mapping to
// spatial tally filters. However, after several iterations, if all FSRs
// have been hit and have had a tally map generated, then this status will
// be passed back to the caller to alert them that this function doesn't
// need to be called for the remainder of the simulation.
void FlatSourceDomain::convert_source_regions_to_tallies()
{
openmc::simulation::time_tallies.start();
// Tracks if we've generated a mapping yet for all source regions.
bool all_source_regions_mapped = true;
// Attempt to generate mapping for all source regions
#pragma omp parallel for
for (int sr = 0; sr < n_source_regions_; sr++) {
// If this source region has not been hit by a ray yet, then
// we aren't going to be able to map it, so skip it.
if (!position_recorded_[sr]) {
all_source_regions_mapped = false;
continue;
}
// A particle located at the recorded midpoint of a ray
// crossing through this source region is used to estabilish
// the spatial location of the source region
Particle p;
p.r() = position_[sr];
p.r_last() = position_[sr];
bool found = exhaustive_find_cell(p);
// Loop over energy groups (so as to support energy filters)
for (int g = 0; g < negroups_; g++) {
// Set particle to the current energy
p.g() = g;
p.g_last() = g;
p.E() = data::mg.energy_bin_avg_[p.g()];
p.E_last() = p.E();
int64_t source_element = sr * negroups_ + g;
// If this task has already been populated, we don't need to do
// it again.
if (tally_task_[source_element].size() > 0) {
continue;
}
// Loop over all active tallies. This logic is essentially identical
// to what happens when scanning for applicable tallies during
// MC transport.
for (auto i_tally : model::active_tallies) {
Tally& 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];
// If a valid tally, filter, and score cobination has been found,
// then add it to the list of tally tasks for this source element.
tally_task_[source_element].emplace_back(
i_tally, filter_index, score_index, score_bin);
}
}
}
// Reset all the filter matches for the next tally event.
for (auto& match : p.filter_matches())
match.bins_present_ = false;
}
}
openmc::simulation::time_tallies.stop();
mapped_all_tallies_ = all_source_regions_mapped;
}
// Tallying in random ray is not done directly during transport, rather,
// it is done only once after each power iteration. This is made possible
// by way of a mapping data structure that relates spatial source regions
// (FSRs) to tally/filter/score combinations. The mechanism by which the
// mapping is done (and the limitations incurred) is documented in the
// "convert_source_regions_to_tallies()" function comments above. The present
// tally function simply traverses the mapping data structure and executes
// the scoring operations to OpenMC's native tally result arrays.
void FlatSourceDomain::random_ray_tally() const
{
openmc::simulation::time_tallies.start();
// Temperature and angle indices, if using multiple temperature
// data sets and/or anisotropic data sets.
// TODO: Currently assumes we are only using single temp/single
// angle data.
const int t = 0;
const int a = 0;
// We loop over all source regions and energy groups. For each
// element, we check if there are any scores needed and apply
// them.
#pragma omp parallel for
for (int sr = 0; sr < n_source_regions_; sr++) {
double volume = volume_[sr];
double material = material_[sr];
for (int g = 0; g < negroups_; g++) {
int idx = sr * negroups_ + g;
double flux = scalar_flux_new_[idx] * volume;
for (auto& task : tally_task_[idx]) {
double score;
switch (task.score_type) {
case SCORE_FLUX:
score = flux;
break;
case SCORE_TOTAL:
score = flux * data::mg.macro_xs_[material].get_xs(
MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
break;
case SCORE_FISSION:
score = flux * data::mg.macro_xs_[material].get_xs(
MgxsType::FISSION, g, NULL, NULL, NULL, t, a);
break;
case SCORE_NU_FISSION:
score = flux * data::mg.macro_xs_[material].get_xs(
MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a);
break;
case SCORE_EVENTS:
score = 1.0;
break;
default:
fatal_error("Invalid score specified in tallies.xml. Only flux, "
"total, fission, nu-fission, and events are supported in "
"random ray mode.");
break;
}
Tally& tally {*model::tallies[task.tally_idx]};
#pragma omp atomic
tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
score;
}
}
}
}
void FlatSourceDomain::all_reduce_replicated_source_regions()
{
#ifdef OPENMC_MPI
// If we only have 1 MPI rank, no need
// to reduce anything.
if (mpi::n_procs <= 1)
return;
simulation::time_bank_sendrecv.start();
// The "position_recorded" variable needs to be allreduced (and maxed),
// as whether or not a cell was hit will affect some decisions in how the
// source is calculated in the next iteration so as to avoid dividing
// by zero. We take the max rather than the sum as the hit values are
// expected to be zero or 1.
MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), n_source_regions_,
MPI_INT, MPI_MAX, mpi::intracomm);
// The position variable is more complicated to reduce than the others,
// as we do not want the sum of all positions in each cell, rather, we
// want to just pick any single valid position. Thus, we perform a gather
// and then pick the first valid position we find for all source regions
// that have had a position recorded. This operation does not need to
// be broadcast back to other ranks, as this value is only used for the
// tally conversion operation, which is only performed on the master rank.
// While this is expensive, it only needs to be done for active batches,
// and only if we have not mapped all the tallies yet. Once tallies are
// fully mapped, then the position vector is fully populated, so this
// operation can be skipped.
// First, we broadcast the fully mapped tally status variable so that
// all ranks are on the same page
int mapped_all_tallies_i = static_cast<int>(mapped_all_tallies_);
MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm);
// Then, we perform the gather of position data, if needed
if (simulation::current_batch > settings::n_inactive &&
!mapped_all_tallies_i) {
// Master rank will gather results and pick valid positions
if (mpi::master) {
// Initialize temporary vector for receiving positions
vector<vector<Position>> all_position;
all_position.resize(mpi::n_procs);
for (int i = 0; i < mpi::n_procs; i++) {
all_position[i].resize(n_source_regions_);
}
// Copy master rank data into gathered vector for convenience
all_position[0] = position_;
// Receive all data into gather vector
for (int i = 1; i < mpi::n_procs; i++) {
MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i,
0, mpi::intracomm, MPI_STATUS_IGNORE);
}
// Scan through gathered data and pick first valid cell posiiton
for (int sr = 0; sr < n_source_regions_; sr++) {
if (position_recorded_[sr] == 1) {
for (int i = 0; i < mpi::n_procs; i++) {
if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 ||
all_position[i][sr].z != 0.0) {
position_[sr] = all_position[i][sr];
break;
}
}
}
}
} else {
// Other ranks just send in their data
MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0,
mpi::intracomm);
}
}
// For the rest of the source region data, we simply perform an all reduce,
// as these values will be needed on all ranks for transport during the
// next iteration.
MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE,
MPI_SUM, mpi::intracomm);
MPI_Allreduce(MPI_IN_PLACE, was_hit_.data(), n_source_regions_, MPI_INT,
MPI_SUM, mpi::intracomm);
MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_,
MPI_FLOAT, MPI_SUM, mpi::intracomm);
simulation::time_bank_sendrecv.stop();
#endif
}
// Outputs all basic material, FSR ID, multigroup flux, and
// fission source data to .vtk file that can be directly
// loaded and displayed by Paraview. Note that .vtk binary
// files require big endian byte ordering, so endianness
// is checked and flipped if necessary.
void FlatSourceDomain::output_to_vtk() const
{
// Rename .h5 plot filename(s) to .vtk filenames
for (int p = 0; p < model::plots.size(); p++) {
PlottableInterface* plot = model::plots[p].get();
plot->path_plot() =
plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
}
// Print header information
print_plot();
// Outer loop over plots
for (int p = 0; p < model::plots.size(); p++) {
// Get handle to OpenMC plot object and extract params
Plot* openmc_plot = dynamic_cast<Plot*>(model::plots[p].get());
// Random ray plots only support voxel plots
if (!openmc_plot) {
warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
"is allowed in random ray mode.",
p));
continue;
} else if (openmc_plot->type_ != Plot::PlotType::voxel) {
warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
"is allowed in random ray mode.",
p));
continue;
}
int Nx = openmc_plot->pixels_[0];
int Ny = openmc_plot->pixels_[1];
int Nz = openmc_plot->pixels_[2];
Position origin = openmc_plot->origin_;
Position width = openmc_plot->width_;
Position ll = origin - width / 2.0;
double x_delta = width.x / Nx;
double y_delta = width.y / Ny;
double z_delta = width.z / Nz;
std::string filename = openmc_plot->path_plot();
// Perform sanity checks on file size
uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
openmc_plot->id(), filename, bytes / 1.0e6);
if (bytes / 1.0e9 > 1.0) {
warning("Voxel plot specification is very large (>1 GB). Plotting may be "
"slow.");
} else if (bytes / 1.0e9 > 100.0) {
fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
}
// Relate voxel spatial locations to random ray source regions
vector<int> voxel_indices(Nx * Ny * Nz);
#pragma omp parallel for collapse(3)
for (int z = 0; z < Nz; z++) {
for (int y = 0; y < Ny; y++) {
for (int x = 0; x < Nx; x++) {
Position sample;
sample.z = ll.z + z_delta / 2.0 + z * z_delta;
sample.y = ll.y + y_delta / 2.0 + y * y_delta;
sample.x = ll.x + x_delta / 2.0 + x * x_delta;
Particle p;
p.r() = sample;
bool found = exhaustive_find_cell(p);
int i_cell = p.lowest_coord().cell;
int64_t source_region_idx =
source_region_offsets_[i_cell] + p.cell_instance();
voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx;
}
}
}
// Open file for writing
std::FILE* plot = std::fopen(filename.c_str(), "wb");
// Write vtk metadata
std::fprintf(plot, "# vtk DataFile Version 2.0\n");
std::fprintf(plot, "Dataset File\n");
std::fprintf(plot, "BINARY\n");
std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
std::fprintf(plot, "ORIGIN 0 0 0\n");
std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
// Plot multigroup flux data
for (int g = 0; g < negroups_; g++) {
std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
std::fprintf(plot, "LOOKUP_TABLE default\n");
for (int fsr : voxel_indices) {
int64_t source_element = fsr * negroups_ + g;
float flux = scalar_flux_final_[source_element];
flux /= (settings::n_batches - settings::n_inactive);
flux = convert_to_big_endian<float>(flux);
std::fwrite(&flux, sizeof(float), 1, plot);
}
}
// Plot FSRs
std::fprintf(plot, "SCALARS FSRs float\n");
std::fprintf(plot, "LOOKUP_TABLE default\n");
for (int fsr : voxel_indices) {
float value = future_prn(10, fsr);
value = convert_to_big_endian<float>(value);
std::fwrite(&value, sizeof(float), 1, plot);
}
// Plot Materials
std::fprintf(plot, "SCALARS Materials int\n");
std::fprintf(plot, "LOOKUP_TABLE default\n");
for (int fsr : voxel_indices) {
int mat = material_[fsr];
mat = convert_to_big_endian<int>(mat);
std::fwrite(&mat, sizeof(int), 1, plot);
}
// Plot fission source
std::fprintf(plot, "SCALARS total_fission_source float\n");
std::fprintf(plot, "LOOKUP_TABLE default\n");
for (int fsr : voxel_indices) {
float total_fission = 0.0;
int mat = material_[fsr];
for (int g = 0; g < negroups_; g++) {
int64_t source_element = fsr * negroups_ + g;
float flux = scalar_flux_final_[source_element];
flux /= (settings::n_batches - settings::n_inactive);
float Sigma_f = data::mg.macro_xs_[mat].get_xs(
MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0);
total_fission += Sigma_f * flux;
}
total_fission = convert_to_big_endian<float>(total_fission);
std::fwrite(&total_fission, sizeof(float), 1, plot);
}
std::fclose(plot);
}
}
} // namespace openmc

View file

@ -0,0 +1,289 @@
#include "openmc/random_ray/random_ray.h"
#include "openmc/geometry.h"
#include "openmc/message_passing.h"
#include "openmc/mgxs_interface.h"
#include "openmc/random_ray/flat_source_domain.h"
#include "openmc/search.h"
#include "openmc/settings.h"
#include "openmc/simulation.h"
#include "openmc/source.h"
namespace openmc {
//==============================================================================
// Non-method functions
//==============================================================================
// returns 1 - exp(-tau)
// Equivalent to -(_expm1f(-tau)), but faster
// Written by Colin Josey.
float cjosey_exponential(float tau)
{
constexpr float c1n = -1.0000013559236386308f;
constexpr float c2n = 0.23151368626911062025f;
constexpr float c3n = -0.061481916409314966140f;
constexpr float c4n = 0.0098619906458127653020f;
constexpr float c5n = -0.0012629460503540849940f;
constexpr float c6n = 0.00010360973791574984608f;
constexpr float c7n = -0.000013276571933735820960f;
constexpr float c0d = 1.0f;
constexpr float c1d = -0.73151337729389001396f;
constexpr float c2d = 0.26058381273536471371f;
constexpr float c3d = -0.059892419041316836940f;
constexpr float c4d = 0.0099070188241094279067f;
constexpr float c5d = -0.0012623388962473160860f;
constexpr float c6d = 0.00010361277635498731388f;
constexpr float c7d = -0.000013276569500666698498f;
float x = -tau;
float den = c7d;
den = den * x + c6d;
den = den * x + c5d;
den = den * x + c4d;
den = den * x + c3d;
den = den * x + c2d;
den = den * x + c1d;
den = den * x + c0d;
float num = c7n;
num = num * x + c6n;
num = num * x + c5n;
num = num * x + c4n;
num = num * x + c3n;
num = num * x + c2n;
num = num * x + c1n;
num = num * x;
return num / den;
}
//==============================================================================
// RandomRay implementation
//==============================================================================
// Static Variable Declarations
double RandomRay::distance_inactive_;
double RandomRay::distance_active_;
unique_ptr<Source> RandomRay::ray_source_;
RandomRay::RandomRay()
: negroups_(data::mg.num_energy_groups_),
angular_flux_(data::mg.num_energy_groups_),
delta_psi_(data::mg.num_energy_groups_)
{}
RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay()
{
initialize_ray(ray_id, domain);
}
// Transports ray until termination criteria are met
uint64_t RandomRay::transport_history_based_single_ray()
{
using namespace openmc;
while (alive()) {
event_advance_ray();
if (!alive())
break;
event_cross_surface();
}
return n_event();
}
// Transports ray across a single source region
void RandomRay::event_advance_ray()
{
// Find the distance to the nearest boundary
boundary() = distance_to_boundary(*this);
double distance = boundary().distance;
if (distance <= 0.0) {
mark_as_lost("Negative transport distance detected for particle " +
std::to_string(id()));
return;
}
if (is_active_) {
// If the ray is in the active length, need to check if it has
// reached its maximum termination distance. If so, reduce
// the ray traced length so that the ray does not overrun the
// maximum numerical length (so as to avoid numerical bias).
if (distance_travelled_ + distance >= distance_active_) {
distance = distance_active_ - distance_travelled_;
wgt() = 0.0;
}
distance_travelled_ += distance;
attenuate_flux(distance, true);
} else {
// If the ray is still in the dead zone, need to check if it
// has entered the active phase. If so, split into two segments (one
// representing the final part of the dead zone, the other representing the
// first part of the active length) and attenuate each. Otherwise, if the
// full length of the segment is within the dead zone, attenuate as normal.
if (distance_travelled_ + distance >= distance_inactive_) {
is_active_ = true;
double distance_dead = distance_inactive_ - distance_travelled_;
attenuate_flux(distance_dead, false);
double distance_alive = distance - distance_dead;
// Ensure we haven't travelled past the active phase as well
if (distance_alive > distance_active_) {
distance_alive = distance_active_;
wgt() = 0.0;
}
attenuate_flux(distance_alive, true);
distance_travelled_ = distance_alive;
} else {
distance_travelled_ += distance;
attenuate_flux(distance, false);
}
}
// Advance particle
for (int j = 0; j < n_coord(); ++j) {
coord(j).r += distance * coord(j).u;
}
}
// This function forms the inner loop of the random ray transport process.
// It is responsible for several tasks. Based on the incoming angular flux
// of the ray and the source term in the region, the outgoing angular flux
// is computed. The delta psi between the incoming and outgoing fluxes is
// contributed to the estimate of the total scalar flux in the source region.
// Additionally, the contribution of the ray path to the stochastically
// estimated volume is also kept track of. All tasks involving writing
// to the data for the source region are done with a lock over the entire
// source region. Locks are used instead of atomics as all energy groups
// must be written, such that locking once is typically much more efficient
// than use of many atomic operations corresponding to each energy group
// individually (at least on CPU). Several other bookkeeping tasks are also
// performed when inside the lock.
void RandomRay::attenuate_flux(double distance, bool is_active)
{
// The number of geometric intersections is counted for reporting purposes
n_event()++;
// Determine source region index etc.
int i_cell = lowest_coord().cell;
// The source region is the spatial region index
int64_t source_region =
domain_->source_region_offsets_[i_cell] + cell_instance();
// The source element is the energy-specific region index
int64_t source_element = source_region * negroups_;
int material = this->material();
// Temperature and angle indices, if using multiple temperature
// data sets and/or anisotropic data sets.
// TODO: Currently assumes we are only using single temp/single
// angle data.
const int t = 0;
const int a = 0;
// MOC incoming flux attenuation + source contribution/attenuation equation
for (int g = 0; g < negroups_; g++) {
float sigma_t = data::mg.macro_xs_[material].get_xs(
MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
float tau = sigma_t * distance;
float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau)
float new_delta_psi =
(angular_flux_[g] - domain_->source_[source_element + g]) * exponential;
delta_psi_[g] = new_delta_psi;
angular_flux_[g] -= new_delta_psi;
}
// If ray is in the active phase (not in dead zone), make contributions to
// source region bookkeeping
if (is_active) {
// Aquire lock for source region
domain_->lock_[source_region].lock();
// Accumulate delta psi into new estimate of source region flux for
// this iteration
for (int g = 0; g < negroups_; g++) {
domain_->scalar_flux_new_[source_element + g] += delta_psi_[g];
}
// If the source region hasn't been hit yet this iteration,
// indicate that it now has
if (domain_->was_hit_[source_region] == 0) {
domain_->was_hit_[source_region] = 1;
}
// Accomulate volume (ray distance) into this iteration's estimate
// of the source region's volume
domain_->volume_[source_region] += distance;
// Tally valid position inside the source region (e.g., midpoint of
// the ray) if not done already
if (!domain_->position_recorded_[source_region]) {
Position midpoint = r() + u() * (distance / 2.0);
domain_->position_[source_region] = midpoint;
domain_->position_recorded_[source_region] = 1;
}
// Release lock
domain_->lock_[source_region].unlock();
}
}
void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
{
domain_ = domain;
// Reset particle event counter
n_event() = 0;
is_active_ = (distance_inactive_ <= 0.0);
wgt() = 1.0;
// set identifier for particle
id() = simulation::work_index[mpi::rank] + ray_id;
// set random number seed
int64_t particle_seed =
(simulation::current_batch - 1) * settings::n_particles + id();
init_particle_seeds(particle_seed, seeds());
stream() = STREAM_TRACKING;
// Sample from ray source distribution
SourceSite site {ray_source_->sample(current_seed())};
site.E = lower_bound_index(
data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E);
site.E = negroups_ - site.E - 1.;
this->from_source(&site);
// Locate ray
if (lowest_coord().cell == C_NONE) {
if (!exhaustive_find_cell(*this)) {
this->mark_as_lost(
"Could not find the cell containing particle " + std::to_string(id()));
}
// Set birth cell attribute
if (cell_born() == C_NONE)
cell_born() = lowest_coord().cell;
}
// Initialize ray's starting angular flux to starting location's isotropic
// source
int i_cell = lowest_coord().cell;
int64_t source_region_idx =
domain_->source_region_offsets_[i_cell] + cell_instance();
for (int g = 0; g < negroups_; g++) {
angular_flux_[g] = domain_->source_[source_region_idx * negroups_ + g];
}
}
} // namespace openmc

View file

@ -0,0 +1,397 @@
#include "openmc/random_ray/random_ray_simulation.h"
#include "openmc/eigenvalue.h"
#include "openmc/geometry.h"
#include "openmc/message_passing.h"
#include "openmc/mgxs_interface.h"
#include "openmc/output.h"
#include "openmc/plot.h"
#include "openmc/random_ray/random_ray.h"
#include "openmc/simulation.h"
#include "openmc/source.h"
#include "openmc/tallies/filter.h"
#include "openmc/tallies/tally.h"
#include "openmc/tallies/tally_scoring.h"
#include "openmc/timer.h"
namespace openmc {
//==============================================================================
// Non-member functions
//==============================================================================
void openmc_run_random_ray()
{
// Initialize OpenMC general data structures
openmc_simulation_init();
// Validate that inputs meet requirements for random ray mode
if (mpi::master)
validate_random_ray_inputs();
// Initialize Random Ray Simulation Object
RandomRaySimulation sim;
// Begin main simulation timer
simulation::time_total.start();
// Execute random ray simulation
sim.simulate();
// End main simulation timer
openmc::simulation::time_total.stop();
// Finalize OpenMC
openmc_simulation_finalize();
// Reduce variables across MPI ranks
sim.reduce_simulation_statistics();
// Output all simulation results
sim.output_simulation_results();
}
// Enforces restrictions on inputs in random ray mode. While there are
// many features that don't make sense in random ray mode, and are therefore
// unsupported, we limit our testing/enforcement operations only to inputs
// that may cause erroneous/misleading output or crashes from the solver.
void validate_random_ray_inputs()
{
// Validate tallies
///////////////////////////////////////////////////////////////////
for (auto& tally : model::tallies) {
// Validate score types
for (auto score_bin : tally->scores_) {
switch (score_bin) {
case SCORE_FLUX:
case SCORE_TOTAL:
case SCORE_FISSION:
case SCORE_NU_FISSION:
case SCORE_EVENTS:
break;
default:
fatal_error(
"Invalid score specified. Only flux, total, fission, nu-fission, and "
"event scores are supported in random ray mode.");
}
}
// Validate filter types
for (auto f : tally->filters()) {
auto& filter = *model::tally_filters[f];
switch (filter.type()) {
case FilterType::CELL:
case FilterType::CELL_INSTANCE:
case FilterType::DISTRIBCELL:
case FilterType::ENERGY:
case FilterType::MATERIAL:
case FilterType::MESH:
case FilterType::UNIVERSE:
break;
default:
fatal_error("Invalid filter specified. Only cell, cell_instance, "
"distribcell, energy, material, mesh, and universe filters "
"are supported in random ray mode.");
}
}
}
// Validate MGXS data
///////////////////////////////////////////////////////////////////
for (auto& material : data::mg.macro_xs_) {
if (!material.is_isotropic) {
fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets "
"supported in random ray mode.");
}
if (material.get_xsdata().size() > 1) {
fatal_error("Non-isothermal MGXS detected. Only isothermal XS data sets "
"supported in random ray mode.");
}
}
// Validate solver mode
///////////////////////////////////////////////////////////////////
if (settings::run_mode == RunMode::FIXED_SOURCE) {
fatal_error(
"Invalid run mode. Fixed source not yet supported in random ray mode.");
}
// Validate ray source
///////////////////////////////////////////////////////////////////
// Check for independent source
IndependentSource* is =
dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
if (!is) {
fatal_error(
"Invalid ray source definition. Ray source must be IndependentSource.");
}
// Check for box source
SpatialDistribution* space_dist = is->space();
SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
if (!sb) {
fatal_error(
"Invalid source definition -- only box sources are allowed in random "
"ray "
"mode. If no source is specified, OpenMC default is an isotropic point "
"source at the origin, which is invalid in random ray mode.");
}
// Check that box source is not restricted to fissionable areas
if (sb->only_fissionable()) {
fatal_error("Invalid source definition -- fissionable spatial distribution "
"not allowed for random ray source.");
}
// Check for isotropic source
UnitSphereDistribution* angle_dist = is->angle();
Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
if (!id) {
fatal_error("Invalid source definition -- only isotropic sources are "
"allowed for random ray source.");
}
// Validate plotting files
///////////////////////////////////////////////////////////////////
for (int p = 0; p < model::plots.size(); p++) {
// Get handle to OpenMC plot object
Plot* openmc_plot = dynamic_cast<Plot*>(model::plots[p].get());
// Random ray plots only support voxel plots
if (!openmc_plot) {
warning(fmt::format(
"Plot {} will not be used for end of simulation data plotting -- only "
"voxel plotting is allowed in random ray mode.",
p));
continue;
} else if (openmc_plot->type_ != Plot::PlotType::voxel) {
warning(fmt::format(
"Plot {} will not be used for end of simulation data plotting -- only "
"voxel plotting is allowed in random ray mode.",
p));
continue;
}
}
// Warn about slow MPI domain replication, if detected
///////////////////////////////////////////////////////////////////
#ifdef OPENMC_MPI
if (mpi::n_procs > 1) {
warning(
"Domain replication in random ray is supported, but suffers from poor "
"scaling of source all-reduce operations. Performance may severely "
"degrade beyond just a few MPI ranks. Domain decomposition may be "
"implemented in the future to provide efficient scaling.");
}
#endif
}
//==============================================================================
// RandomRaySimulation implementation
//==============================================================================
RandomRaySimulation::RandomRaySimulation()
: negroups_(data::mg.num_energy_groups_)
{
// There are no source sites in random ray mode, so be sure to disable to
// ensure we don't attempt to write source sites to statepoint
settings::source_write = false;
// Random ray mode does not have an inner loop over generations within a
// batch, so set the current gen to 1
simulation::current_gen = 1;
}
void RandomRaySimulation::simulate()
{
// Random ray power iteration loop
while (simulation::current_batch < settings::n_batches) {
// Initialize the current batch
initialize_batch();
initialize_generation();
// Reset total starting particle weight used for normalizing tallies
simulation::total_weight = 1.0;
// Update source term (scattering + fission)
domain_.update_neutron_source(k_eff_);
// Reset scalar fluxes, iteration volume tallies, and region hit flags to
// zero
domain_.batch_reset();
// Start timer for transport
simulation::time_transport.start();
// Transport sweep over all random rays for the iteration
#pragma omp parallel for schedule(dynamic) \
reduction(+ : total_geometric_intersections_)
for (int i = 0; i < simulation::work_per_rank; i++) {
RandomRay ray(i, &domain_);
total_geometric_intersections_ +=
ray.transport_history_based_single_ray();
}
simulation::time_transport.stop();
// If using multiple MPI ranks, perform all reduce on all transport results
domain_.all_reduce_replicated_source_regions();
// Normalize scalar flux and update volumes
domain_.normalize_scalar_flux_and_volumes(
settings::n_particles * RandomRay::distance_active_);
// Add source to scalar flux, compute number of FSR hits
int64_t n_hits = domain_.add_source_to_scalar_flux();
// Compute random ray k-eff
k_eff_ = domain_.compute_k_eff(k_eff_);
// Store random ray k-eff into OpenMC's native k-eff variable
global_tally_tracklength = k_eff_;
// Execute all tallying tasks, if this is an active batch
if (simulation::current_batch > settings::n_inactive && mpi::master) {
// Generate mapping between source regions and tallies
if (!domain_.mapped_all_tallies_) {
domain_.convert_source_regions_to_tallies();
}
// Use above mapping to contribute FSR flux data to appropriate tallies
domain_.random_ray_tally();
// Add this iteration's scalar flux estimate to final accumulated estimate
domain_.accumulate_iteration_flux();
}
// Set phi_old = phi_new
domain_.scalar_flux_old_.swap(domain_.scalar_flux_new_);
// Check for any obvious insabilities/nans/infs
instability_check(n_hits, k_eff_, avg_miss_rate_);
// Finalize the current batch
finalize_generation();
finalize_batch();
} // End random ray power iteration loop
}
void RandomRaySimulation::reduce_simulation_statistics()
{
// Reduce number of intersections
#ifdef OPENMC_MPI
if (mpi::n_procs > 1) {
uint64_t total_geometric_intersections_reduced = 0;
MPI_Reduce(&total_geometric_intersections_,
&total_geometric_intersections_reduced, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0,
mpi::intracomm);
total_geometric_intersections_ = total_geometric_intersections_reduced;
}
#endif
}
void RandomRaySimulation::output_simulation_results() const
{
// Print random ray results
if (mpi::master) {
print_results_random_ray(total_geometric_intersections_,
avg_miss_rate_ / settings::n_batches, negroups_,
domain_.n_source_regions_);
if (model::plots.size() > 0) {
domain_.output_to_vtk();
}
}
}
// Apply a few sanity checks to catch obvious cases of numerical instability.
// Instability typically only occurs if ray density is extremely low.
void RandomRaySimulation::instability_check(
int64_t n_hits, double k_eff, double& avg_miss_rate) const
{
double percent_missed = ((domain_.n_source_regions_ - n_hits) /
static_cast<double>(domain_.n_source_regions_)) *
100.0;
avg_miss_rate += percent_missed;
if (percent_missed > 10.0) {
warning(fmt::format(
"Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
"Increase ray density by adding more rays and/or active distance.",
percent_missed));
} else if (percent_missed > 0.01) {
warning(fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
"ray density by adding more rays and/or active "
"distance may improve simulation efficiency.",
percent_missed));
}
if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
fatal_error("Instability detected");
}
}
// Print random ray simulation results
void RandomRaySimulation::print_results_random_ray(
uint64_t total_geometric_intersections, double avg_miss_rate, int negroups,
int64_t n_source_regions) const
{
using namespace simulation;
if (settings::verbosity >= 6) {
double total_integrations = total_geometric_intersections * negroups;
double time_per_integration =
simulation::time_transport.elapsed() / total_integrations;
double misc_time = time_total.elapsed() - time_update_src.elapsed() -
time_transport.elapsed() - time_tallies.elapsed() -
time_bank_sendrecv.elapsed();
header("Simulation Statistics", 4);
fmt::print(
" Total Iterations = {}\n", settings::n_batches);
fmt::print(" Flat Source Regions (FSRs) = {}\n", n_source_regions);
fmt::print(" Total Geometric Intersections = {:.4e}\n",
static_cast<double>(total_geometric_intersections));
fmt::print(" Avg per Iteration = {:.4e}\n",
static_cast<double>(total_geometric_intersections) / settings::n_batches);
fmt::print(" Avg per Iteration per FSR = {:.2f}\n",
static_cast<double>(total_geometric_intersections) /
static_cast<double>(settings::n_batches) / n_source_regions);
fmt::print(" Avg FSR Miss Rate per Iteration = {:.4f}%\n", avg_miss_rate);
fmt::print(" Energy Groups = {}\n", negroups);
fmt::print(
" Total Integrations = {:.4e}\n", total_integrations);
fmt::print(" Avg per Iteration = {:.4e}\n",
total_integrations / settings::n_batches);
header("Timing Statistics", 4);
show_time("Total time for initialization", time_initialize.elapsed());
show_time("Reading cross sections", time_read_xs.elapsed(), 1);
show_time("Total simulation time", time_total.elapsed());
show_time("Transport sweep only", time_transport.elapsed(), 1);
show_time("Source update only", time_update_src.elapsed(), 1);
show_time("Tally conversion only", time_tallies.elapsed(), 1);
show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
show_time("Other iteration routines", misc_time, 1);
if (settings::run_mode == RunMode::EIGENVALUE) {
show_time("Time in inactive batches", time_inactive.elapsed());
}
show_time("Time in active batches", time_active.elapsed());
show_time("Time writing statepoints", time_statepoint.elapsed());
show_time("Total time for finalization", time_finalize.elapsed());
show_time("Time per integration", time_per_integration);
}
if (settings::verbosity >= 4) {
header("Results", 4);
fmt::print(" k-effective = {:.5f} +/- {:.5f}\n",
simulation::keff, simulation::keff_std);
}
}
} // namespace openmc

View file

@ -24,6 +24,7 @@
#include "openmc/output.h"
#include "openmc/plot.h"
#include "openmc/random_lcg.h"
#include "openmc/random_ray/random_ray.h"
#include "openmc/simulation.h"
#include "openmc/source.h"
#include "openmc/string_utils.h"
@ -113,6 +114,7 @@ double res_scat_energy_min {0.01};
double res_scat_energy_max {1000.0};
vector<std::string> res_scat_nuclides;
RunMode run_mode {RunMode::UNSET};
SolverType solver_type {SolverType::MONTE_CARLO};
std::unordered_set<int> sourcepoint_batch;
std::unordered_set<int> statepoint_batch;
std::unordered_set<int> source_write_surf_id;
@ -233,6 +235,38 @@ void get_run_parameters(pugi::xml_node node_base)
}
}
}
// Random ray variables
if (solver_type == SolverType::RANDOM_RAY) {
xml_node random_ray_node = node_base.child("random_ray");
if (check_for_node(random_ray_node, "distance_active")) {
RandomRay::distance_active_ =
std::stod(get_node_value(random_ray_node, "distance_active"));
if (RandomRay::distance_active_ <= 0.0) {
fatal_error("Random ray active distance must be greater than 0");
}
} else {
fatal_error("Specify random ray active distance in settings XML");
}
if (check_for_node(random_ray_node, "distance_inactive")) {
RandomRay::distance_inactive_ =
std::stod(get_node_value(random_ray_node, "distance_inactive"));
if (RandomRay::distance_inactive_ < 0) {
fatal_error(
"Random ray inactive distance must be greater than or equal to 0");
}
} else {
fatal_error("Specify random ray inactive distance in settings XML");
}
if (check_for_node(random_ray_node, "source")) {
xml_node source_node = random_ray_node.child("source");
// Get point to list of <source> elements and make sure there is at least
// one
RandomRay::ray_source_ = Source::create(source_node);
} else {
fatal_error("Specify random ray source in settings XML");
}
}
}
void read_settings_xml()
@ -389,6 +423,14 @@ void read_settings_xml(pugi::xml_node root)
}
}
// Check solver type
if (check_for_node(root, "random_ray")) {
solver_type = SolverType::RANDOM_RAY;
if (run_CE)
fatal_error("multi-group energy mode must be specified in settings XML "
"when using the random ray solver.");
}
if (run_mode == RunMode::EIGENVALUE || run_mode == RunMode::FIXED_SOURCE) {
// Read run parameters
get_run_parameters(node_mode);

View file

@ -122,7 +122,8 @@ int openmc_simulation_init()
write_message("Resuming simulation...", 6);
} else {
// Only initialize primary source bank for eigenvalue simulations
if (settings::run_mode == RunMode::EIGENVALUE) {
if (settings::run_mode == RunMode::EIGENVALUE &&
settings::solver_type == SolverType::MONTE_CARLO) {
initialize_source();
}
}
@ -132,7 +133,11 @@ int openmc_simulation_init()
if (settings::run_mode == RunMode::FIXED_SOURCE) {
header("FIXED SOURCE TRANSPORT SIMULATION", 3);
} else if (settings::run_mode == RunMode::EIGENVALUE) {
header("K EIGENVALUE SIMULATION", 3);
if (settings::solver_type == SolverType::MONTE_CARLO) {
header("K EIGENVALUE SIMULATION", 3);
} else if (settings::solver_type == SolverType::RANDOM_RAY) {
header("K EIGENVALUE SIMULATION (RANDOM RAY SOLVER)", 3);
}
if (settings::verbosity >= 7)
print_columns();
}
@ -196,10 +201,12 @@ int openmc_simulation_finalize()
simulation::time_finalize.stop();
simulation::time_total.stop();
if (mpi::master) {
if (settings::verbosity >= 6)
print_runtime();
if (settings::verbosity >= 4)
print_results();
if (settings::solver_type != SolverType::RANDOM_RAY) {
if (settings::verbosity >= 6)
print_runtime();
if (settings::verbosity >= 4)
print_results();
}
}
if (settings::check_overlaps)
print_overlap_check();
@ -309,7 +316,8 @@ vector<int64_t> work_index;
void allocate_banks()
{
if (settings::run_mode == RunMode::EIGENVALUE) {
if (settings::run_mode == RunMode::EIGENVALUE &&
settings::solver_type == SolverType::MONTE_CARLO) {
// Allocate source bank
simulation::source_bank.resize(simulation::work_per_rank);
@ -493,7 +501,8 @@ void finalize_generation()
}
global_tally_leakage = 0.0;
if (settings::run_mode == RunMode::EIGENVALUE) {
if (settings::run_mode == RunMode::EIGENVALUE &&
settings::solver_type == SolverType::MONTE_CARLO) {
// If using shared memory, stable sort the fission bank (by parent IDs)
// so as to allow for reproducibility regardless of which order particles
// are run in.
@ -501,6 +510,9 @@ void finalize_generation()
// Distribute fission bank across processors evenly
synchronize_bank();
}
if (settings::run_mode == RunMode::EIGENVALUE) {
// Calculate shannon entropy
if (settings::entropy_on)

View file

@ -243,36 +243,40 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
// Sample angle
site.u = angle_->sample(seed);
// Check for monoenergetic source above maximum particle energy
auto p = static_cast<int>(particle_);
auto energy_ptr = dynamic_cast<Discrete*>(energy_.get());
if (energy_ptr) {
auto energies = xt::adapt(energy_ptr->x());
if (xt::any(energies > data::energy_max[p])) {
fatal_error("Source energy above range of energies of at least "
"one cross section table");
// Sample energy and time for neutron and photon sources
if (settings::solver_type != SolverType::RANDOM_RAY) {
// Check for monoenergetic source above maximum particle energy
auto p = static_cast<int>(particle_);
auto energy_ptr = dynamic_cast<Discrete*>(energy_.get());
if (energy_ptr) {
auto energies = xt::adapt(energy_ptr->x());
if (xt::any(energies > data::energy_max[p])) {
fatal_error("Source energy above range of energies of at least "
"one cross section table");
}
}
}
while (true) {
// Sample energy spectrum
site.E = energy_->sample(seed);
while (true) {
// Sample energy spectrum
site.E = energy_->sample(seed);
// Resample if energy falls above maximum particle energy
if (site.E < data::energy_max[p])
break;
// Resample if energy falls above maximum particle energy
if (site.E < data::energy_max[p])
break;
n_reject++;
if (n_reject >= EXTSRC_REJECT_THRESHOLD &&
static_cast<double>(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) {
fatal_error("More than 95% of external source sites sampled were "
"rejected. Please check your external source energy spectrum "
"definition.");
n_reject++;
if (n_reject >= EXTSRC_REJECT_THRESHOLD &&
static_cast<double>(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) {
fatal_error(
"More than 95% of external source sites sampled were "
"rejected. Please check your external source energy spectrum "
"definition.");
}
}
}
// Sample particle creation time
site.time = time_->sample(seed);
// Sample particle creation time
site.time = time_->sample(seed);
}
// Increment number of accepted samples
++n_accept;

View file

@ -757,6 +757,10 @@ void Tally::accumulate()
double norm =
total_source / (settings::n_particles * settings::gen_per_batch);
if (settings::solver_type == SolverType::RANDOM_RAY) {
norm = 1.0;
}
// Accumulate each result
#pragma omp parallel for
for (int i = 0; i < results_.shape()[0]; ++i) {
@ -953,8 +957,9 @@ void accumulate_tallies()
{
#ifdef OPENMC_MPI
// Combine tally results onto master process
if (mpi::n_procs > 1)
if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
reduce_tally_results();
}
#endif
// Increase number of realizations (only used for global tallies)

View file

@ -26,6 +26,7 @@ Timer time_event_advance_particle;
Timer time_event_surface_crossing;
Timer time_event_collision;
Timer time_event_death;
Timer time_update_src;
} // namespace simulation
@ -85,6 +86,7 @@ void reset_timers()
simulation::time_event_surface_crossing.reset();
simulation::time_event_collision.reset();
simulation::time_event_death.reset();
simulation::time_update_src.reset();
}
} // namespace openmc

View file

@ -0,0 +1,108 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<cross_sections>mgxs.h5</cross_sections>
<material id="1" name="UO2 fuel">
<density units="macro" value="1.0"/>
<macroscopic name="UO2"/>
</material>
<material id="2" name="Water">
<density units="macro" value="1.0"/>
<macroscopic name="LWTR"/>
</material>
</materials>
<geometry>
<cell id="1" material="1" name="fuel inner a" region="-2" universe="1"/>
<cell id="2" material="1" name="fuel inner b" region="2 -3" universe="1"/>
<cell id="3" material="1" name="fuel inner c" region="3 -1" universe="1"/>
<cell id="4" material="2" name="moderator inner a" region="1 -4" universe="1"/>
<cell id="5" material="2" name="moderator outer b" region="4 -5" universe="1"/>
<cell id="6" material="2" name="moderator outer c" region="5" universe="1"/>
<cell fill="1" id="7" name="azimuthal_cell_0" region="6 -7" universe="2"/>
<cell fill="1" id="8" name="azimuthal_cell_1" region="7 -8" universe="2"/>
<cell fill="1" id="9" name="azimuthal_cell_2" region="8 -9" universe="2"/>
<cell fill="1" id="10" name="azimuthal_cell_3" region="9 -10" universe="2"/>
<cell fill="1" id="11" name="azimuthal_cell_4" region="10 -11" universe="2"/>
<cell fill="1" id="12" name="azimuthal_cell_5" region="11 -12" universe="2"/>
<cell fill="1" id="13" name="azimuthal_cell_6" region="12 -13" universe="2"/>
<cell fill="1" id="14" name="azimuthal_cell_7" region="13 -6" universe="2"/>
<cell id="15" material="2" name="moderator infinite" universe="3"/>
<cell fill="4" id="16" universe="5"/>
<cell fill="6" id="17" name="assembly" region="14 -15 16 -17" universe="7"/>
<lattice id="4">
<pitch>0.126 0.126</pitch>
<dimension>10 10</dimension>
<lower_left>-0.63 -0.63</lower_left>
<universes>
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3 </universes>
</lattice>
<lattice id="6">
<pitch>1.26 1.26</pitch>
<dimension>2 2</dimension>
<lower_left>-1.26 -1.26</lower_left>
<universes>
2 2
2 5 </universes>
</lattice>
<surface coeffs="0.0 0.0 0.54" id="1" name="Fuel OR" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.33" id="2" name="inner ring a" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.45" id="3" name="inner ring b" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.6" id="4" name="outer ring a" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.69" id="5" name="outer ring b" type="z-cylinder"/>
<surface coeffs="-0.0 1.0 0 0" id="6" type="plane"/>
<surface coeffs="-0.7071067811865475 0.7071067811865476 0 0" id="7" type="plane"/>
<surface coeffs="-1.0 6.123233995736766e-17 0 0" id="8" type="plane"/>
<surface coeffs="-0.7071067811865476 -0.7071067811865475 0 0" id="9" type="plane"/>
<surface coeffs="-1.2246467991473532e-16 -1.0 0 0" id="10" type="plane"/>
<surface coeffs="0.7071067811865475 -0.7071067811865477 0 0" id="11" type="plane"/>
<surface coeffs="1.0 -1.8369701987210297e-16 0 0" id="12" type="plane"/>
<surface coeffs="0.7071067811865477 0.7071067811865474 0 0" id="13" type="plane"/>
<surface boundary="reflective" coeffs="-1.26" id="14" name="minimum x" type="x-plane"/>
<surface boundary="reflective" coeffs="1.26" id="15" name="maximum x" type="x-plane"/>
<surface boundary="reflective" coeffs="-1.26" id="16" name="minimum y" type="y-plane"/>
<surface boundary="reflective" coeffs="1.26" id="17" name="maximum y" type="y-plane"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>10</batches>
<inactive>5</inactive>
<energy_mode>multi-group</energy_mode>
<random_ray>
<distance_active>100.0</distance_active>
<distance_inactive>20.0</distance_inactive>
<source particle="neutron" strength="1.0" type="independent">
<space type="box">
<parameters>-1.26 -1.26 -1 1.26 1.26 1</parameters>
</space>
</source>
</random_ray>
</settings>
<tallies>
<mesh id="1">
<dimension>2 2</dimension>
<lower_left>-1.26 -1.26</lower_left>
<upper_right>1.26 1.26</upper_right>
</mesh>
<filter id="1" type="mesh">
<bins>1</bins>
</filter>
<filter id="2" type="energy">
<bins>1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0</bins>
</filter>
<tally id="1" name="Mesh tally">
<filters>1 2</filters>
<scores>flux fission nu-fission</scores>
<estimator>analog</estimator>
</tally>
</tallies>
</model>

View file

@ -0,0 +1,171 @@
k-combined:
8.400322E-01 8.023349E-03
tally 1:
1.260220E+00
3.179889E-01
1.484289E-01
4.411066E-03
3.612463E-01
2.612843E-02
7.086707E-01
1.006119E-01
3.342483E-02
2.238499E-04
8.134936E-02
1.325949E-03
4.194328E-01
3.558669E-02
4.287776E-03
3.717447E-06
1.043559E-02
2.201986E-05
5.878720E-01
7.045887E-02
6.147757E-03
7.701173E-06
1.496241E-02
4.561699E-05
1.768113E+00
6.356917E-01
6.513486E-03
8.628535E-06
1.585272E-02
5.111136E-05
5.063704E+00
5.152401E+00
2.440293E-03
1.196869E-06
6.038334E-03
7.328193E-06
3.253717E+00
2.117655E+00
1.389120E-02
3.859385E-05
3.863767E-02
2.985798E-04
1.876994E+00
7.046366E-01
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
8.390875E-01
1.408791E-01
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
4.513839E-01
4.139640E-02
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
6.682186E-01
9.116003E-02
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
1.849034E+00
6.944337E-01
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
4.523425E+00
4.112118E+00
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
2.821432E+00
1.592568E+00
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
1.159618E+00
2.694138E-01
1.354028E-01
3.672094E-03
3.295432E-01
2.175122E-02
6.880334E-01
9.491215E-02
3.234611E-02
2.097782E-04
7.872396E-02
1.242596E-03
4.184841E-01
3.536436E-02
4.274305E-03
3.687209E-06
1.040280E-02
2.184074E-05
5.810180E-01
6.872944E-02
6.060273E-03
7.476500E-06
1.474949E-02
4.428617E-05
1.782580E+00
6.457892E-01
6.552384E-03
8.730345E-06
1.594739E-02
5.171444E-05
5.278155E+00
5.596601E+00
2.546878E-03
1.303010E-06
6.302072E-03
7.978072E-06
3.420419E+00
2.340454E+00
1.465798E-02
4.299061E-05
4.077042E-02
3.325951E-04
1.279417E+00
3.278133E-01
1.509073E-01
4.561836E-03
3.672782E-01
2.702150E-02
7.212777E-01
1.042487E-01
3.411552E-02
2.332877E-04
8.303035E-02
1.381852E-03
4.269473E-01
3.685202E-02
4.378540E-03
3.872997E-06
1.065649E-02
2.294124E-05
5.973530E-01
7.266946E-02
6.260881E-03
7.976490E-06
1.523773E-02
4.724780E-05
1.795373E+00
6.547440E-01
6.635941E-03
8.945067E-06
1.615075E-02
5.298634E-05
5.161876E+00
5.353441E+00
2.505311E-03
1.261399E-06
6.199218E-03
7.723296E-06
3.344042E+00
2.236603E+00
1.443089E-02
4.166228E-05
4.013879E-02
3.223186E-04

View file

@ -0,0 +1,232 @@
import os
import numpy as np
import openmc
from tests.testing_harness import TolerantPyAPITestHarness
class MGXSTestHarness(TolerantPyAPITestHarness):
def _cleanup(self):
super()._cleanup()
f = 'mgxs.h5'
if os.path.exists(f):
os.remove(f)
def random_ray_model() -> openmc.Model:
###############################################################################
# Create multigroup data
# Instantiate the energy group data
group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6]
groups = openmc.mgxs.EnergyGroups(group_edges)
# Instantiate the 7-group (C5G7) cross section data
uo2_xsdata = openmc.XSdata('UO2', groups)
uo2_xsdata.order = 0
uo2_xsdata.set_total(
[0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678,
0.5644058])
uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02,
3.0020e-02, 1.1126e-01, 2.8278e-01])
scatter_matrix = np.array(
[[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]])
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
uo2_xsdata.set_scatter_matrix(scatter_matrix)
uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03,
1.85648e-02, 1.78084e-02, 8.30348e-02,
2.16004e-01])
uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02,
4.518301e-02, 4.334208e-02, 2.020901e-01,
5.257105e-01])
uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00,
0.0000e+00, 0.0000e+00])
h2o_xsdata = openmc.XSdata('LWTR', groups)
h2o_xsdata.order = 0
h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435,
0.718, 1.2544497, 2.650379])
h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04,
1.9406e-03, 5.7416e-03, 1.5001e-02,
3.7239e-02])
scatter_matrix = np.array(
[[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000],
[0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010],
[0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034],
[0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390],
[0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]])
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
h2o_xsdata.set_scatter_matrix(scatter_matrix)
mg_cross_sections = openmc.MGXSLibrary(groups)
mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata])
mg_cross_sections.export_to_hdf5()
###############################################################################
# Create materials for the problem
# Instantiate some Materials and register the appropriate macroscopic data
uo2 = openmc.Material(name='UO2 fuel')
uo2.set_density('macro', 1.0)
uo2.add_macroscopic('UO2')
water = openmc.Material(name='Water')
water.set_density('macro', 1.0)
water.add_macroscopic('LWTR')
# Instantiate a Materials collection and export to XML
materials = openmc.Materials([uo2, water])
materials.cross_sections = "mgxs.h5"
###############################################################################
# Define problem geometry
########################################
# Define an unbounded pincell universe
pitch = 1.26
# Create a surface for the fuel outer radius
fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR')
inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a')
inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b')
outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a')
outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b')
# Instantiate Cells
fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a')
fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b')
fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c')
moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a')
moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b')
moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c')
# Create pincell universe
pincell_base = openmc.Universe()
# Register Cells with Universe
pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c])
# Create planes for azimuthal sectors
azimuthal_planes = []
for i in range(8):
angle = 2 * i * openmc.pi / 8
normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0)
azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0))
# Create a cell for each azimuthal sector
azimuthal_cells = []
for i in range(8):
azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}')
azimuthal_cell.fill = pincell_base
azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8]
azimuthal_cells.append(azimuthal_cell)
# Create a geometry with the azimuthal universes
pincell = openmc.Universe(cells=azimuthal_cells)
########################################
# Define a moderator lattice universe
moderator_infinite = openmc.Cell(fill=water, name='moderator infinite')
mu = openmc.Universe()
mu.add_cells([moderator_infinite])
lattice = openmc.RectLattice()
lattice.lower_left = [-pitch/2.0, -pitch/2.0]
lattice.pitch = [pitch/10.0, pitch/10.0]
lattice.universes = np.full((10, 10), mu)
mod_lattice_cell = openmc.Cell(fill=lattice)
mod_lattice_uni = openmc.Universe()
mod_lattice_uni.add_cells([mod_lattice_cell])
########################################
# Define 2x2 outer lattice
lattice2x2 = openmc.RectLattice()
lattice2x2.lower_left = (-pitch, -pitch)
lattice2x2.pitch = (pitch, pitch)
lattice2x2.universes = [
[pincell, pincell],
[pincell, mod_lattice_uni]
]
########################################
# Define cell containing lattice and other stuff
box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective')
assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly')
# Create a geometry with the top-level cell
geometry = openmc.Geometry([assembly])
###############################################################################
# Define problem settings
# Instantiate a Settings object, set all runtime parameters, and export to XML
settings = openmc.Settings()
settings.energy_mode = "multi-group"
settings.batches = 10
settings.inactive = 5
settings.particles = 100
# Create an initial uniform spatial source distribution over fissionable zones
lower_left = (-pitch, -pitch, -1)
upper_right = (pitch, pitch, 1)
uniform_dist = openmc.stats.Box(lower_left, upper_right)
rr_source = openmc.IndependentSource(space=uniform_dist)
settings.random_ray['distance_active'] = 100.0
settings.random_ray['distance_inactive'] = 20.0
settings.random_ray['ray_source'] = rr_source
###############################################################################
# Define tallies
# Create a mesh that will be used for tallying
mesh = openmc.RegularMesh()
mesh.dimension = (2, 2)
mesh.lower_left = (-pitch, -pitch)
mesh.upper_right = (pitch, pitch)
# Create a mesh filter that can be used in a tally
mesh_filter = openmc.MeshFilter(mesh)
# Create an energy group filter as well
energy_filter = openmc.EnergyFilter(group_edges)
# Now use the mesh filter in a tally and indicate what scores are desired
tally = openmc.Tally(name="Mesh tally")
tally.filters = [mesh_filter, energy_filter]
tally.scores = ['flux', 'fission', 'nu-fission']
tally.estimator = 'analog'
# Instantiate a Tallies collection and export to XML
tallies = openmc.Tallies([tally])
###############################################################################
# Exporting to OpenMC model
###############################################################################
model = openmc.Model()
model.geometry = geometry
model.materials = materials
model.settings = settings
model.tallies = tallies
return model
def test_random_ray_basic():
harness = MGXSTestHarness('statepoint.10.h5', random_ray_model())
harness.main()

View file

@ -0,0 +1,108 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<cross_sections>mgxs.h5</cross_sections>
<material id="1" name="UO2 fuel">
<density units="macro" value="1.0"/>
<macroscopic name="UO2"/>
</material>
<material id="2" name="Water">
<density units="macro" value="1.0"/>
<macroscopic name="LWTR"/>
</material>
</materials>
<geometry>
<cell id="1" material="1" name="fuel inner a" region="-2" universe="1"/>
<cell id="2" material="1" name="fuel inner b" region="2 -3" universe="1"/>
<cell id="3" material="1" name="fuel inner c" region="3 -1" universe="1"/>
<cell id="4" material="2" name="moderator inner a" region="1 -4" universe="1"/>
<cell id="5" material="2" name="moderator outer b" region="4 -5" universe="1"/>
<cell id="6" material="2" name="moderator outer c" region="5" universe="1"/>
<cell fill="1" id="7" name="azimuthal_cell_0" region="6 -7" universe="2"/>
<cell fill="1" id="8" name="azimuthal_cell_1" region="7 -8" universe="2"/>
<cell fill="1" id="9" name="azimuthal_cell_2" region="8 -9" universe="2"/>
<cell fill="1" id="10" name="azimuthal_cell_3" region="9 -10" universe="2"/>
<cell fill="1" id="11" name="azimuthal_cell_4" region="10 -11" universe="2"/>
<cell fill="1" id="12" name="azimuthal_cell_5" region="11 -12" universe="2"/>
<cell fill="1" id="13" name="azimuthal_cell_6" region="12 -13" universe="2"/>
<cell fill="1" id="14" name="azimuthal_cell_7" region="13 -6" universe="2"/>
<cell id="15" material="2" name="moderator infinite" universe="3"/>
<cell fill="4" id="16" universe="5"/>
<cell fill="6" id="17" name="assembly" region="14 -15 16 -17" universe="7"/>
<lattice id="4">
<pitch>0.126 0.126</pitch>
<dimension>10 10</dimension>
<lower_left>-0.63 -0.63</lower_left>
<universes>
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3 </universes>
</lattice>
<lattice id="6">
<pitch>1.26 1.26</pitch>
<dimension>2 2</dimension>
<lower_left>-1.26 -1.26</lower_left>
<universes>
2 2
2 5 </universes>
</lattice>
<surface coeffs="0.0 0.0 0.54" id="1" name="Fuel OR" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.33" id="2" name="inner ring a" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.45" id="3" name="inner ring b" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.6" id="4" name="outer ring a" type="z-cylinder"/>
<surface coeffs="0.0 0.0 0.69" id="5" name="outer ring b" type="z-cylinder"/>
<surface coeffs="-0.0 1.0 0 0" id="6" type="plane"/>
<surface coeffs="-0.7071067811865475 0.7071067811865476 0 0" id="7" type="plane"/>
<surface coeffs="-1.0 6.123233995736766e-17 0 0" id="8" type="plane"/>
<surface coeffs="-0.7071067811865476 -0.7071067811865475 0 0" id="9" type="plane"/>
<surface coeffs="-1.2246467991473532e-16 -1.0 0 0" id="10" type="plane"/>
<surface coeffs="0.7071067811865475 -0.7071067811865477 0 0" id="11" type="plane"/>
<surface coeffs="1.0 -1.8369701987210297e-16 0 0" id="12" type="plane"/>
<surface coeffs="0.7071067811865477 0.7071067811865474 0 0" id="13" type="plane"/>
<surface boundary="vacuum" coeffs="-1.26" id="14" name="minimum x" type="x-plane"/>
<surface boundary="vacuum" coeffs="1.26" id="15" name="maximum x" type="x-plane"/>
<surface boundary="vacuum" coeffs="-1.26" id="16" name="minimum y" type="y-plane"/>
<surface boundary="vacuum" coeffs="1.26" id="17" name="maximum y" type="y-plane"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>10</batches>
<inactive>5</inactive>
<energy_mode>multi-group</energy_mode>
<random_ray>
<distance_active>100.0</distance_active>
<distance_inactive>20.0</distance_inactive>
<source particle="neutron" strength="1.0" type="independent">
<space type="box">
<parameters>-1.26 -1.26 -1 1.26 1.26 1</parameters>
</space>
</source>
</random_ray>
</settings>
<tallies>
<mesh id="1">
<dimension>2 2</dimension>
<lower_left>-1.26 -1.26</lower_left>
<upper_right>1.26 1.26</upper_right>
</mesh>
<filter id="1" type="mesh">
<bins>1</bins>
</filter>
<filter id="2" type="energy">
<bins>1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0</bins>
</filter>
<tally id="1" name="Mesh tally">
<filters>1 2</filters>
<scores>flux fission nu-fission</scores>
<estimator>analog</estimator>
</tally>
</tallies>
</model>

View file

@ -0,0 +1,171 @@
k-combined:
1.010455E-01 1.585558E-02
tally 1:
1.849176E-01
7.634332E-03
2.181815E-02
1.062861E-04
5.310100E-02
6.295730E-04
4.048251E-02
3.851890E-04
1.893676E-03
8.448769E-07
4.608828E-03
5.004529E-06
4.063643E-03
4.022442E-06
4.112970E-05
4.186661E-10
1.001015E-04
2.479919E-09
7.467029E-03
1.178864E-05
7.688748E-05
1.266903E-09
1.871288E-04
7.504350E-09
3.870644E-02
3.010745E-04
1.375240E-04
3.807356E-09
3.347099E-04
2.255298E-08
4.524967E-01
4.098857E-02
2.437418E-04
1.190325E-08
6.031220E-04
7.288126E-08
4.989226E-01
4.993728E-02
2.374296E-03
1.135824E-06
6.603983E-03
8.787258E-06
3.899991E-01
3.308783E-02
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
7.108982E-02
1.144390E-03
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
5.295259E-03
6.352159E-06
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
9.852001E-03
1.984406E-05
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
4.414391E-02
3.905201E-04
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
2.571668E-01
1.323140E-02
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
2.752932E-01
1.517930E-02
0.000000E+00
0.000000E+00
0.000000E+00
0.000000E+00
1.465446E-01
4.901884E-03
1.700791E-02
6.587674E-05
4.139385E-02
3.902131E-04
3.424032E-02
2.841338E-04
1.598985E-03
6.216312E-07
3.891610E-03
3.682159E-06
4.067582E-03
4.209468E-06
4.152829E-05
4.494649E-10
1.010715E-04
2.662352E-09
7.526712E-03
1.225032E-05
7.769969E-05
1.328443E-09
1.891055E-04
7.868877E-09
4.008649E-02
3.246821E-04
1.417944E-04
4.070719E-09
3.451035E-04
2.411301E-08
4.859902E-01
4.747592E-02
2.606214E-04
1.369749E-08
6.448895E-04
8.386705E-08
5.475198E-01
6.061269E-02
2.625477E-03
1.405458E-06
7.302631E-03
1.087327E-05
1.909660E-01
8.147906E-03
2.269063E-02
1.149570E-04
5.522446E-02
6.809342E-04
4.196583E-02
4.141620E-04
1.980406E-03
9.227119E-07
4.819913E-03
5.465576E-06
4.247004E-03
4.420116E-06
4.341806E-05
4.691518E-10
1.056709E-04
2.778965E-09
7.742814E-03
1.272112E-05
8.039606E-05
1.389209E-09
1.956679E-04
8.228817E-09
3.982370E-02
3.190931E-04
1.427171E-04
4.103942E-09
3.473492E-04
2.430981E-08
4.849535E-01
4.707014E-02
2.678327E-04
1.438540E-08
6.627333E-04
8.807897E-08
5.493457E-01
6.069440E-02
2.717400E-03
1.501450E-06
7.558312E-03
1.161591E-05

View file

@ -0,0 +1,235 @@
import os
import numpy as np
import openmc
from tests.testing_harness import TolerantPyAPITestHarness
class MGXSTestHarness(TolerantPyAPITestHarness):
def _cleanup(self):
super()._cleanup()
f = 'mgxs.h5'
if os.path.exists(f):
os.remove(f)
def random_ray_model() -> openmc.Model:
###############################################################################
# Create multigroup data
# Instantiate the energy group data
group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6]
groups = openmc.mgxs.EnergyGroups(group_edges)
# Instantiate the 7-group (C5G7) cross section data
uo2_xsdata = openmc.XSdata('UO2', groups)
uo2_xsdata.order = 0
uo2_xsdata.set_total(
[0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678,
0.5644058])
uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02,
3.0020e-02, 1.1126e-01, 2.8278e-01])
scatter_matrix = np.array(
[[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]])
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
uo2_xsdata.set_scatter_matrix(scatter_matrix)
uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03,
1.85648e-02, 1.78084e-02, 8.30348e-02,
2.16004e-01])
uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02,
4.518301e-02, 4.334208e-02, 2.020901e-01,
5.257105e-01])
uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00,
0.0000e+00, 0.0000e+00])
h2o_xsdata = openmc.XSdata('LWTR', groups)
h2o_xsdata.order = 0
h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435,
0.718, 1.2544497, 2.650379])
h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04,
1.9406e-03, 5.7416e-03, 1.5001e-02,
3.7239e-02])
scatter_matrix = np.array(
[[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000],
[0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010],
[0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034],
[0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390],
[0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200],
[0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]])
scatter_matrix = np.rollaxis(scatter_matrix, 0, 3)
h2o_xsdata.set_scatter_matrix(scatter_matrix)
mg_cross_sections = openmc.MGXSLibrary(groups)
mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata])
mg_cross_sections.export_to_hdf5()
###############################################################################
# Create materials for the problem
# Instantiate some Materials and register the appropriate Macroscopic objects
uo2 = openmc.Material(name='UO2 fuel')
uo2.set_density('macro', 1.0)
uo2.add_macroscopic('UO2')
water = openmc.Material(name='Water')
water.set_density('macro', 1.0)
water.add_macroscopic('LWTR')
# Instantiate a Materials collection and export to XML
materials = openmc.Materials([uo2, water])
materials.cross_sections = "mgxs.h5"
###############################################################################
# Define problem geometry
########################################
# Define an unbounded pincell universe
pitch = 1.26
# Create a surface for the fuel outer radius
fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR')
inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a')
inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b')
outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a')
outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b')
# Instantiate Cells
fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a')
fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b')
fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c')
moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a')
moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b')
moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c')
# Create pincell universe
pincell_base = openmc.Universe()
# Register Cells with Universe
pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c])
# Create planes for azimuthal sectors
azimuthal_planes = []
for i in range(8):
angle = 2 * i * openmc.pi / 8
normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0)
azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0))
# Create a cell for each azimuthal sector
azimuthal_cells = []
for i in range(8):
azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}')
azimuthal_cell.fill = pincell_base
azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8]
azimuthal_cells.append(azimuthal_cell)
# Create a geometry with the azimuthal universes
pincell = openmc.Universe(cells=azimuthal_cells)
########################################
# Define a moderator lattice universe
moderator_infinite = openmc.Cell(fill=water, name='moderator infinite')
mu = openmc.Universe()
mu.add_cells([moderator_infinite])
lattice = openmc.RectLattice()
lattice.lower_left = [-pitch/2.0, -pitch/2.0]
lattice.pitch = [pitch/10.0, pitch/10.0]
lattice.universes = np.full((10, 10), mu)
mod_lattice_cell = openmc.Cell(fill=lattice)
mod_lattice_uni = openmc.Universe()
mod_lattice_uni.add_cells([mod_lattice_cell])
########################################
# Define 2x2 outer lattice
lattice2x2 = openmc.RectLattice()
lattice2x2.lower_left = [-pitch, -pitch]
lattice2x2.pitch = [pitch, pitch]
lattice2x2.universes = [
[pincell, pincell],
[pincell, mod_lattice_uni]
]
########################################
# Define cell containing lattice and other stuff
box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='vacuum')
assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly')
root = openmc.Universe(name='root universe')
root.add_cell(assembly)
# Create a geometry with the two cells and export to XML
geometry = openmc.Geometry(root)
###############################################################################
# Define problem settings
# Instantiate a Settings object, set all runtime parameters, and export to XML
settings = openmc.Settings()
settings.energy_mode = "multi-group"
settings.batches = 10
settings.inactive = 5
settings.particles = 100
# Create an initial uniform spatial source distribution over fissionable zones
lower_left = (-pitch, -pitch, -1)
upper_right = (pitch, pitch, 1)
uniform_dist = openmc.stats.Box(lower_left, upper_right)
rr_source = openmc.IndependentSource(space=uniform_dist)
settings.random_ray['distance_active'] = 100.0
settings.random_ray['distance_inactive'] = 20.0
settings.random_ray['ray_source'] = rr_source
###############################################################################
# Define tallies
# Create a mesh that will be used for tallying
mesh = openmc.RegularMesh()
mesh.dimension = (2, 2)
mesh.lower_left = (-pitch, -pitch)
mesh.upper_right = (pitch, pitch)
# Create a mesh filter that can be used in a tally
mesh_filter = openmc.MeshFilter(mesh)
# Create an energy group filter as well
energy_filter = openmc.EnergyFilter(group_edges)
# Now use the mesh filter in a tally and indicate what scores are desired
tally = openmc.Tally(name="Mesh tally")
tally.filters = [mesh_filter, energy_filter]
tally.scores = ['flux', 'fission', 'nu-fission']
tally.estimator = 'analog'
# Instantiate a Tallies collection and export to XML
tallies = openmc.Tallies([tally])
###############################################################################
# Exporting to OpenMC model
###############################################################################
model = openmc.Model()
model.geometry = geometry
model.materials = materials
model.settings = settings
model.tallies = tallies
return model
def test_random_ray_vacuum():
harness = MGXSTestHarness('statepoint.10.h5', random_ray_model())
harness.main()

View file

@ -384,6 +384,63 @@ class HashedPyAPITestHarness(PyAPITestHarness):
return super()._get_results(True)
class TolerantPyAPITestHarness(PyAPITestHarness):
"""Specialized harness for running tests that involve significant levels
of floating point non-associativity when using shared memory parallelism
due to single precision usage (e.g., as in the random ray solver).
"""
def _are_files_equal(self, actual_path, expected_path, tolerance):
def isfloat(value):
try:
float(value)
return True
except ValueError:
return False
def tokenize(line):
return line.strip().split()
def compare_tokens(token1, token2):
if isfloat(token1) and isfloat(token2):
float1, float2 = float(token1), float(token2)
return abs(float1 - float2) <= tolerance * max(abs(float1), abs(float2))
else:
return token1 == token2
expected = open(expected_path).readlines()
actual = open(actual_path).readlines()
if len(expected) != len(actual):
return False
for line1, line2 in zip(expected, actual):
tokens1 = tokenize(line1)
tokens2 = tokenize(line2)
if len(tokens1) != len(tokens2):
return False
for token1, token2 in zip(tokens1, tokens2):
if not compare_tokens(token1, token2):
return False
return True
def _compare_results(self):
"""Make sure the current results agree with the reference."""
compare = self._are_files_equal('results_test.dat', 'results_true.dat', 1e-6)
if not compare:
expected = open('results_true.dat').readlines()
actual = open('results_test.dat').readlines()
diff = unified_diff(expected, actual, 'results_true.dat',
'results_test.dat')
print('Result differences:')
print(''.join(colorize(diff)))
os.rename('results_test.dat', 'results_error.dat')
assert compare, 'Results do not agree'
class PlotTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC plotting tests."""
def __init__(self, plot_names, voxel_convert_checks=[]):

View file

@ -58,6 +58,13 @@ def test_export_to_xml(run_in_tmpdir):
s.electron_treatment = 'led'
s.write_initial_source = True
s.weight_window_checkpoints = {'surface': True, 'collision': False}
s.random_ray = {
'distance_inactive': 10.0,
'distance_active': 100.0,
'ray_source': openmc.IndependentSource(
space=openmc.stats.Box((-1., -1., -1.), (1., 1., 1.))
)
}
s.max_particle_events = 100
@ -131,3 +138,7 @@ def test_export_to_xml(run_in_tmpdir):
assert vol.upper_right == (10., 10., 10.)
assert s.weight_window_checkpoints == {'surface': True, 'collision': False}
assert s.max_particle_events == 100
assert s.random_ray['distance_inactive'] == 10.0
assert s.random_ray['distance_active'] == 100.0
assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.]
assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.]