mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 06:25:30 -04:00
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:
parent
d53155d234
commit
5111aa2621
46 changed files with 4512 additions and 69 deletions
|
|
@ -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
|
||||
|
|
|
|||
BIN
docs/source/_images/2x2_fsrs.jpeg
Normal file
BIN
docs/source/_images/2x2_fsrs.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
BIN
docs/source/_images/2x2_materials.jpeg
Normal file
BIN
docs/source/_images/2x2_materials.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
|
|
@ -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
|
||||
----------------------------------
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ Theory and Methodology
|
|||
energy_deposition
|
||||
parallelization
|
||||
cmfd
|
||||
random_ray
|
||||
804
docs/source/methods/random_ray.rst
Normal file
804
docs/source/methods/random_ray.rst
Normal 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).
|
||||
|
|
@ -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
|
||||
-------------------------
|
||||
|
|
|
|||
|
|
@ -25,4 +25,6 @@ essential aspects of using OpenMC to perform simulations.
|
|||
processing
|
||||
parallel
|
||||
volume
|
||||
random_ray
|
||||
troubleshoot
|
||||
|
||||
480
docs/source/usersguide/random_ray.rst
Normal file
480
docs/source/usersguide/random_ray.rst
Normal 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.
|
||||
203
examples/pincell_random_ray/build_xml.py
Normal file
203
examples/pincell_random_ray/build_xml.py
Normal 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()
|
||||
|
|
@ -10,6 +10,7 @@ namespace openmc {
|
|||
|
||||
// Forward declare some types used in function arguments.
|
||||
class Particle;
|
||||
class RandomRay;
|
||||
class Surface;
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -340,6 +340,8 @@ enum class RunMode {
|
|||
VOLUME
|
||||
};
|
||||
|
||||
enum class SolverType { MONTE_CARLO, RANDOM_RAY };
|
||||
|
||||
//==============================================================================
|
||||
// Geometry Constants
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_; }
|
||||
|
||||
|
|
|
|||
141
include/openmc/random_ray/flat_source_domain.h
Normal file
141
include/openmc/random_ray/flat_source_domain.h
Normal 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
|
||||
55
include/openmc/random_ray/random_ray.h
Normal file
55
include/openmc/random_ray/random_ray.h
Normal 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
|
||||
59
include/openmc/random_ray/random_ray_simulation.h
Normal file
59
include/openmc/random_ray/random_ray_simulation.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
11
src/main.cpp
11
src/main.cpp
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
686
src/random_ray/flat_source_domain.cpp
Normal file
686
src/random_ray/flat_source_domain.cpp
Normal 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
|
||||
289
src/random_ray/random_ray.cpp
Normal file
289
src/random_ray/random_ray.cpp
Normal 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
|
||||
397
src/random_ray/random_ray_simulation.cpp
Normal file
397
src/random_ray/random_ray_simulation.cpp
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
0
tests/regression_tests/random_ray_basic/__init__.py
Normal file
0
tests/regression_tests/random_ray_basic/__init__.py
Normal file
108
tests/regression_tests/random_ray_basic/inputs_true.dat
Normal file
108
tests/regression_tests/random_ray_basic/inputs_true.dat
Normal 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>
|
||||
171
tests/regression_tests/random_ray_basic/results_true.dat
Normal file
171
tests/regression_tests/random_ray_basic/results_true.dat
Normal 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
|
||||
232
tests/regression_tests/random_ray_basic/test.py
Normal file
232
tests/regression_tests/random_ray_basic/test.py
Normal 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()
|
||||
0
tests/regression_tests/random_ray_vacuum/__init__.py
Normal file
0
tests/regression_tests/random_ray_vacuum/__init__.py
Normal file
108
tests/regression_tests/random_ray_vacuum/inputs_true.dat
Normal file
108
tests/regression_tests/random_ray_vacuum/inputs_true.dat
Normal 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>
|
||||
171
tests/regression_tests/random_ray_vacuum/results_true.dat
Normal file
171
tests/regression_tests/random_ray_vacuum/results_true.dat
Normal 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
|
||||
235
tests/regression_tests/random_ray_vacuum/test.py
Normal file
235
tests/regression_tests/random_ray_vacuum/test.py
Normal 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()
|
||||
|
|
@ -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=[]):
|
||||
|
|
|
|||
|
|
@ -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.]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue