Merge branch 'develop' into photonuclear-physics-pt1

This commit is contained in:
GuySten 2025-11-20 13:30:27 +02:00
commit 48f7ddbcd0
145 changed files with 6930 additions and 749 deletions

View file

@ -338,6 +338,7 @@ list(APPEND libopenmc_SOURCES
src/cell.cpp
src/chain.cpp
src/cmfd_solver.cpp
src/collision_track.cpp
src/cross_sections.cpp
src/dagmc.cpp
src/distribution.cpp

View file

@ -0,0 +1,46 @@
.. _io_collision_track:
===========================
Collision Track File Format
===========================
When collision tracking is enabled with ``mcpl=false`` (the default), OpenMC
writes binary data to an HDF5 file named ``collision_track.h5``. The same data
may also be written after each batch when multiple files are requested
(``collision_track.N.h5``) or when the run is performed in parallel. The file
contains the information needed to reconstruct each recorded collision.
The current revision of the collision track file format is 1.0.
**/**
:Attributes:
- **filetype** (*char[]*) -- String indicating the type of file.
For collision-track files the value is ``"collision_track"``.
:Datasets:
- **collision_track_bank** (Compound type) -- Collision information
for each stored event. Each entry in the dataset corresponds to one
collision and contains the following fields:
- ``r`` (*double[3]*) -- Position of the collision in [cm].
- ``u`` (*double[3]*) -- Direction unit vector immediately after the collision.
- ``E`` (*double*) -- Incident particle energy before the collision in [eV].
- ``dE`` (*double*) -- Energy loss over the collision (:math:`E_\text{before} - E_\text{after}`) in [eV].
- ``time`` (*double*) -- Time of the collision in [s].
- ``wgt`` (*double*) -- Particle weight at the collision.
- ``event_mt`` (*int*) -- ENDF MT number identifying the reaction.
- ``delayed_group`` (*int*) -- Delayed neutron group index (non-zero for delayed events).
- ``cell_id`` (*int*) -- ID of the cell in which the collision occurred.
- ``nuclide_id`` (*int*) -- ZA identifier of the nuclide (ZZZAAAM format).
- ``material_id`` (*int*) -- ID of the material containing the collision site.
- ``universe_id`` (*int*) -- ID of the universe containing the collision site.
- ``n_collision`` (*int*) -- Collision counter for the particle history.
- ``particle`` (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, 3=positron).
- ``parent_id`` (*int64*) -- Unique ID of the parent particle.
- ``progeny_id`` (*int64*) -- Progeny ID of the particle.
In an MPI run, OpenMC writes the combined dataset by gathering collision-track
entries from all ranks before flushing them to disk, so the final file appears
as though it were produced serially.

View file

@ -4,7 +4,7 @@
Depletion Results File Format
=============================
The current version of the depletion results file format is 1.1.
The current version of the depletion results file format is 1.2.
**/**
@ -12,22 +12,20 @@ The current version of the depletion results file format is 1.1.
- **version** (*int[2]*) -- Major and minor version of the
statepoint file format.
:Datasets: - **eigenvalues** (*double[][][2]*) -- k-eigenvalues at each
time/stage. This array has shape (number of timesteps, number of
stages, value). The last axis contains the eigenvalue and the
associated uncertainty
- **number** (*double[][][][]*) -- Total number of atoms. This array
has shape (number of timesteps, number of stages, number of
:Datasets: - **eigenvalues** (*double[][2]*) -- k-eigenvalues at each timestep.
This array has shape (number of timesteps, 2). The second axis
contains the eigenvalue and its associated uncertainty.
- **number** (*double[][][]*) -- Total number of atoms at each
timestep. This array has shape (number of timesteps, number of
materials, number of nuclides).
- **reaction rates** (*double[][][][][]*) -- Reaction rates used to
build depletion matrices. This array has shape (number of
timesteps, number of stages, number of materials, number of
nuclides, number of reactions).
- **reaction rates** (*double[][][][]*) -- Reaction rates at each
timestep. This array has shape (number of timesteps, number of
materials, number of nuclides, number of reactions). Only stored if
write_rates=True.
- **time** (*double[][2]*) -- Time in [s] at beginning/end of each
step.
- **source_rate** (*double[][]*) -- Power in [W] or source rate in
[neutron/sec]. This array has shape (number of timesteps, number
of stages).
- **source_rate** (*double[]*) -- Power in [W] or source rate in
[neutron/sec] for each timestep.
- **depletion time** (*double[]*) -- Average process time in [s]
spent depleting a material across all burnable materials and,
if applicable, MPI processes.

View file

@ -44,6 +44,7 @@ Output Files
statepoint
source
collision_track
summary
properties
depletion_results

View file

@ -20,6 +20,85 @@ source neutrons.
*Default*: None
-----------------------------
``<collision_track>`` Element
-----------------------------
The ``<collision_track>`` element indicates to track information about particle
collisions based on a set of criteria and store these events in a file named
``collision_track.h5``. This file records details such as the position of the
interaction, direction of the incoming particle, incident energy and deposited
energy, weight, time of the interaction, and the delayed neutron group (0 for
prompt neutrons). Additional information such as the cell ID, material ID,
universe ID, nuclide ZAID, particle type, and event MT number are also stored.
Users can specify one or more criterion to filter collisions. If no criteria are
specified, it defaults to tracking all collisions across the model.
.. warning::
Storing all collisions can be very memory intensive. For more targeted
tracking, users can employ a variety of parameters such as ``cell_ids``,
``reactions``, ``universe_ids``, ``material_ids``, ``nuclides``, and
``deposited_E_threshold`` to refine the selection of particle interactions
to be banked.
This element can contain one or more of the following attributes or
sub-elements:
:max_collisions:
An integer indicating the maximum number of collisions to be banked per file.
*Default*: 1000
:max_collision_track_files:
An integer indicating the number of collision_track files to be used.
*Default*: 1
:mcpl:
An optional boolean to enable MCPL_-format instead of the native HDF5-based
format. If activated, the output file name and type is changed to
``collision_track.mcpl``.
*Default*: false
.. _MCPL: https://mctools.github.io/mcpl/mcpl.pdf
:cell_ids:
A list of integers representing cell IDs to define specific cells in which
collisions are to be banked.
*Default*: None
:universe_ids:
A list of integers representing the universe IDs to define specific
universes in which collisions are to be banked.
*Default*: None
:material_ids:
A list of integers representing the material IDs to define specific
materials in which collisions are to be banked.
*Default*: None
:nuclides:
A list of strings representing the nuclide, to define specific
define specific target nuclide collisions to be banked.
*Default*: None
:reactions:
A list of integers representing the ENDF-6 format MT numbers or strings
(e.g. (n,fission)) to define specific reaction types to be banked.
*Default*: None
:deposited_E_threshold:
A float defining the minimum deposited energy per collision (in eV) to
trigger banking.
*Default*: 0.0
----------------------------------
``<confidence_intervals>`` Element
----------------------------------

View file

@ -149,6 +149,8 @@ The current version of the statepoint file format is 18.1.
tallies will have a value of 0 unless otherwise instructed.
- **multiply_density** (*int*) -- Flag indicating whether reaction
rates should be multiplied by atom density (1) or not (0).
- **higher_moments** (*int*) -- Flag indicating whether
higher-order tally moments are enabled (1) or not (0).
:Datasets: - **n_realizations** (*int*) -- Number of realizations.
- **n_filters** (*int*) -- Number of filters used.

View file

@ -387,6 +387,101 @@ of this is that the longer you run a simulation, the better you know your
results. Therefore, by running a simulation long enough, it is possible to
reduce the stochastic uncertainty to arbitrarily low levels.
Skewness
++++++++
The `skewness`_ of a population quantifies the asymmetry of the probability
distribution around its mean. Positive and negative skewness indicate a
longer/heavier right and left tail respectively. Let :math:`x_1,\ldots,x_n` be
the per-realization values for a bin, with sample mean :math:`\bar{x}` and
sample central moments:
.. math::
m_k \;=\; \frac{1}{n}\sum_{i=1}^{n}\bigl(x_i-\bar{x}\bigr)^k.
OpenMC reports the *adjusted Fisher-Pearson skewness* (defined for :math:`n \ge
3`), which is commonly used in many statistical packages:
.. math::
G_1 \;=\; \frac{\sqrt{n \cdot (n-1)}}{\,n-2\,}\cdot\frac{m_3}{m_2^{3/2}}.
where :math:`m_2` and :math:`m_3` correspond to the biased sample second and
third central moment respectively.
Kurtosis
++++++++
The `kurtosis`_ of a population quantifies tail weight (also called tailedness)
of the probability distribution relative to a normal distribution. Positive
excess kurtosis indicates *heavier tails* whereas negative excess kurtosis
indicates *lighter tails*. Kurtosis is especially useful for identifying bins
where occasional extreme scores dominate uncertainty. OpenMC reports the
*adjusted excess kurtosis* (defined for :math:`n \ge 4`):
.. math::
G_2 \;=\; \frac{(n-1)}{(n-2)(n-3)}
\left[(n+1)\,\frac{m_4}{m_2^{2}} \;-\; 3(n-1)\right].
where :math:`m_2` and :math:`m_4` correspond to the biased sample second and
fourth central moment respectively. For a perfectly normal distribution, the
excess kurtosis is :math:`0`.
Variance of Variance
++++++++++++++++++++
The variance of the variance (also known as the coefficient of variation
squared) measures *stability of the sample variance* :math:`s^2` and, by
extension, the reliability of reported relative errors. High VOV means that
error bars themselves are noisy—often due to heavy tails, skewness, or too few
realizations.
.. math::
VOV = \frac{s^2(s_{\bar{X}}^2)}{s_{\bar{X}}^4 } = \frac{m_4}{m_2^2} - \frac{1}{n}
where :math:`s_{\bar{X}}^2` is the estimated variance of the mean and
:math:`s^2(s_{\bar{X}}^2)` is the estimated variance in :math:`s_{\bar{X}}^2`.
The MCNP manual suggests a hard threshold such that :math:`VOV < 0.1` to improve
the probability of forming a reliable confidence interval. However, OpenMC does
not enforce an universal cut-off because the suitability of any single threshold
depends strongly on problem specifics (estimator choice, variance-reduction
settings, tally binning, or even effective sample size).
Normality Tests (D'Agostino-Pearson)
++++++++++++++++++++++++++++++++++++
These normality test verify the hypothesis that fluctuations are *approximately
normal*, a working assumption behind many Monte Carlo diagnostics and
`confidence-interval heuristics`_. Tests are provided for: (i) skewness-only,
(ii) kurtosis-only, and (iii) the *omnibus* combination. OpenMC uses the
finite-sample-adjusted skewness :math:`G_1` and excess kurtosis :math:`G_2`
above to construct standardized normal scores :math:`Z_1` (from :math:`G_1`) and
:math:`Z_2` (from :math:`G_2`) via the D'Agostino-Pearson transformations. The
omnibus statistic is
.. math::
K^2 \;=\; Z_1^{\,2} \;+\; Z_2^{\,2}
\;\sim\; \chi^2_{(2)} \quad \text{under } H_0:\ \text{normality}.
OpenMC reports :math:`Z_1`, :math:`Z_2`, :math:`K^2`, and their p-values when
prerequisites are met (skewness for :math:`n\ge 3`, kurtosis and omnibus for
:math:`n\ge 4`). Given a user-chosen significance level :math:`\alpha` (default
is :math:`0.05`), reject :math:`H_0` if :math:`\text{p-value}<\alpha`; otherwise
fail to reject. OpenMC leaves the interpretation to the user, who should
consider VOV together with skewness, kurtosis, and normality tests results when
judging whether reported confidence intervals are credible for their application
[#norm-tests]_.
.. [#norm-tests]
Higher-moments accumulation must be enabled with ``higher_moments = True``
for running these diagnostics including the skewness, kurtosis, and normality
tests.
Figure of Merit
+++++++++++++++
@ -405,14 +500,16 @@ defined as
.. math::
:label: relative_error
r = \frac{s_\bar{X}}{\bar{x}}.
r = \frac{s_{\bar{X}}}{\bar{x}}.
Based on this definition, one can see that a higher FOM is desirable. The FOM is
useful as a comparative tool. For example, if a variance reduction technique is
being applied to a simulation, the FOM with variance reduction can be compared
to the FOM without variance reduction to ascertain whether the reduction in
variance outweighs the potential increase in execution time (e.g., due to
particle splitting).
particle splitting). It is important to note that MCNP reports the FOM using CPU
time (wall-clock time multiplied by the number of threads/cores), whereas OpenMC
reports the FOM using only the wall-clock time :math:`t`.
Confidence Intervals
++++++++++++++++++++
@ -521,6 +618,8 @@ improve the estimate of the percentile.
.. rubric:: References
.. _confidence-interval heuristics: https://doi.org/10.1080/00031305.1990.10475751
.. _following approximation: https://doi.org/10.1080/03610918708812641
.. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel's_correction
@ -541,6 +640,10 @@ improve the estimate of the percentile.
.. _converges in distribution: https://en.wikipedia.org/wiki/Convergence_of_random_variables#Convergence_in_distribution
.. _skewness: https://en.wikipedia.org/wiki/Skewness
.. _kurtosis: https://en.wikipedia.org/wiki/Kurtosis
.. _confidence intervals: https://en.wikipedia.org/wiki/Confidence_interval
.. _Student's t-distribution: https://en.wikipedia.org/wiki/Student%27s_t-distribution

View file

@ -216,6 +216,9 @@ Post-processing
:nosignatures:
:template: myfunction.rst
openmc.read_collision_track_file
openmc.read_collision_track_hdf5
openmc.read_collision_track_mcpl
openmc.voxel_to_vtk
The following classes and functions are used for functional expansion reconstruction.

View file

@ -287,6 +287,16 @@ the following abstract base classes:
abc.SIIntegrator
abc.DepSystemSolver
R2S Automation
--------------
.. autosummary::
:toctree: generated
:nosignatures:
:template: myclass.rst
R2SManager
D1S Functions
-------------

View file

@ -6,42 +6,189 @@ Decay Sources
Through the :ref:`depletion <usersguide_depletion>` capabilities in OpenMC, it
is possible to simulate radiation emitted from the decay of activated materials.
For fusion energy systems, this is commonly done using what is known as the
`rigorous 2-step <https://doi.org/10.1016/S0920-3796(02)00144-8>`_ (R2S) method.
In this method, a neutron transport calculation is used to determine the neutron
flux and reaction rates over a cell- or mesh-based spatial discretization of the
model. Then, the neutron flux in each discrete region is used to predict the
activated material composition using a depletion solver. Finally, a photon
transport calculation with a source based on the activity and energy spectrum of
the activated materials is used to determine a desired physical response (e.g.,
a dose rate) at one or more locations of interest.
For fusion energy systems, this is commonly done using either the `rigorous
2-step <https://doi.org/10.1016/S0920-3796(02)00144-8>`_ (R2S) method or the
`direct 1-step <https://doi.org/10.1016/S0920-3796(01)00188-0>`_ (D1S) method.
In the R2S method, a neutron transport calculation is used to determine the
neutron flux and reaction rates over a cell- or mesh-based spatial
discretization of the model. Then, the neutron flux in each discrete region is
used to predict the activated material composition using a depletion solver.
Finally, a photon transport calculation with a source based on the activity and
energy spectrum of the activated materials is used to determine a desired
physical response (e.g., a dose rate) at one or more locations of interest.
OpenMC includes automation for both the R2S and D1S methods as described in the
following sections.
Once a depletion simulation has been completed in OpenMC, the intrinsic decay
source can be determined as follows. First the activated material composition
can be determined using the :class:`openmc.deplete.Results` object. Indexing an
instance of this class with the timestep index returns a
:class:`~openmc.deplete.StepResult` object, which itself has a
:meth:`~openmc.deplete.StepResult.get_material` method. Once the activated
:class:`~openmc.Material` has been obtained, the
:meth:`~openmc.Material.get_decay_photon_energy` method will give the energy
spectrum of the decay photon source. The integral of the spectrum also indicates
the intensity of the source in units of [Bq]. Altogether, the workflow looks as
follows::
Rigorous 2-Step (R2S) Calculations
==================================
OpenMC includes an :class:`openmc.deplete.R2SManager` class that fully automates
cell- and mesh-based R2S calculations. Before we describe this class, it is
useful to understand the basic mechanics of how an R2S calculation works.
Generally, it involves the following steps:
1. The :meth:`openmc.deplete.get_microxs_and_flux` function is called to run a
neutron transport calculation that determines fluxes and microscopic cross
sections in each activation region.
2. The :class:`openmc.deplete.IndependentOperator` and
:class:`openmc.deplete.PredictorIntegrator` classes are used to carry out a
depletion (activation) calculation in order to determine predicted material
compositions based on a set of timesteps and source rates.
3. The activated material composition is determined using the
:class:`openmc.deplete.Results` class. Indexing an instance of this class
with the timestep index returns a :class:`~openmc.deplete.StepResult` object,
which itself has a :meth:`~openmc.deplete.StepResult.get_material` method
returning an activated material.
4. The :meth:`openmc.Material.get_decay_photon_energy` method is used to obtain
the energy spectrum of the decay photon source. The integral of the spectrum
also indicates the intensity of the source in units of [Bq].
5. A new photon source is defined using one of OpenMC's source classes with the
energy distribution set equal to the object returned by the
:meth:`openmc.Material.get_decay_photon_energy` method. The source is then
assigned to a photon :class:`~openmc.Model`.
6. A photon transport calculation is run with ``model.run()``.
Altogether, the workflow looks as follows::
# Run neutron transport calculation
fluxes, micros = openmc.deplete.get_microxs_and_flux(model, domains)
# Run activation calculation
op = openmc.deplete.IndependentOperator(mats, fluxes, micros)
timesteps = ...
source_rates = ...
integrator = openmc.deplete.Integrator(op, timesteps, source_rates)
integrator.integrate()
# Get decay photon source at last timestep
results = openmc.deplete.Results("depletion_results.h5")
# Get results at last timestep
step = results[-1]
# Get activated material composition for ID=1
activated_mat = step.get_material('1')
# Determine photon source
photon_energy = activated_mat.get_decay_photon_energy()
photon_source = openmc.IndependentSource(
space=...,
energy=photon_energy,
particle='photon',
strength=photon_energy.integral()
)
By default, the :meth:`~openmc.Material.get_decay_photon_energy` method will
eliminate spectral lines with very low intensity, but this behavior can be
configured with the ``clip_tolerance`` argument.
# Run photon transport calculation
model.settings.source = photon_source
model.run()
Note that by default, the :meth:`~openmc.Material.get_decay_photon_energy`
method will eliminate spectral lines with very low intensity, but this behavior
can be configured with the ``clip_tolerance`` argument.
Cell-based R2S
--------------
In practice, users do not need to manually go through each of the steps in an R2S
calculation described above. The :class:`~openmc.deplete.R2SManager` fully
automates the execution of neutron transport, depletion, decay source
generation, and photon transport. For a cell-based R2S calculation, once you
have a :class:`~openmc.Model` that has been defined, simply create an instance
of :class:`~openmc.deplete.R2SManager` by passing the model and a list of cells
to activate::
r2s = openmc.deplete.R2SManager(model, [cell1, cell2, cell3])
Note that the ``volume`` attribute must be set for any cell that is to be
activated. The :class:`~openmc.deplete.R2SManager` class allows you to
optionally specify a separate photon model; if not given as an argument, it will
create a shallow copy of the original neutron model (available as the
``neutron_model`` attribute) and store it in the ``photon_model`` attribute. We
can use this to define tallies specific to the photon model::
dose_tally = openmc.Tally()
...
r2s.photon_model.tallies = [dose_tally]
Next, define the timesteps and source rates for the activation calculation::
timesteps = [(3.0, 'd'), (5.0, 'h')]
source_rates = [1e12, 0.0]
In this case, the model is irradiated for 3 days with a source rate of
:math:`10^{12}` neutron/sec and then the source is turned off and the activated
materials are allowed to decay for 5 hours. These parameters should be passed to
the :meth:`~openmc.deplete.R2SManager.run` method to execute the full R2S
calculation. Before we can do that though, for a cell-based calculation, the one
other piece of information that is needed is bounding boxes of the activated
cells::
bounding_boxes = {
cell1.id: cell1.bounding_box,
cell2.id: cell2.bounding_box,
cell3.id: cell3.bounding_box
}
Note that calling the ``bounding_box`` attribute may not work for all
constructive solid geometry regions (for example, a cell that uses a
non-axis-aligned plane). In these cases, the bounding box will need to be
specified manually. Once you have a set of bounding boxes, the R2S calculation
can be run::
r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes)
If not specified otherwise, a photon transport calculation is run at each time
in the depletion schedule. That means in the case above, we would see three
photon transport calculations. To specify specific times at which photon
transport calculations should be run, pass the ``photon_time_indices`` argument.
For example, if we wanted to run a photon transport calculation only on the last
time (after the 5 hour decay), we would run::
r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes,
photon_time_indices=[2])
After an R2S calculation has been run, the :class:`~openmc.deplete.R2SManager`
instance will have a ``results`` dictionary that allows you to directly access
results from each of the steps. It will also write out all the output files into
a directory that is named "r2s_<timestamp>/". The ``output_dir`` argument to the
:meth:`~openmc.deplete.R2SManager.run` method enables you to override the
default output directory name if desired.
The :meth:`~openmc.deplete.R2SManager.run` method actually runs three
lower-level methods under the hood::
r2s.step1_neutron_transport(...)
r2s.step2_activation(...)
r2s.step3_photon_transport(...)
For users looking for more control over the calculation, these lower-level
methods can be used in lieu of the :meth:`openmc.deplete.R2SManager.run` method.
Mesh-based R2S
--------------
Executing a mesh-based R2S calculation looks nearly identical to the cell-based
R2S workflow described above. The only difference is that instead of passing a
list of cells to the ``domains`` argument of
:class:`~openmc.deplete.R2SManager`, you need to define a mesh object and pass
that instead. This might look like the following::
# Define a regular Cartesian mesh
mesh = openmc.RegularMesh()
mesh.lower_left = (-50., -50., 0.)
mesh.upper_right = (50., 50., 75.)
mesh.dimension = (10, 10, 5)
r2s = openmc.deplete.R2SManager(model, mesh)
Executing the R2S calculation is then performed by adding photon tallies and
calling the :meth:`~openmc.deplete.R2SManager.run` method with the appropriate
timesteps and source rates. Note that in this case we do not need to define cell
volumes or bounding boxes as is required for a cell-based R2S calculation.
Instead, during the neutron transport step, OpenMC will run a raytracing
calculation to determine material volume fractions within each mesh element
using the :meth:`openmc.MeshBase.material_volumes` method. Arguments to this
method can be customized via the ``mat_vol_kwargs`` argument to the
:meth:`~openmc.deplete.R2SManager.run` method. Most often, this would involve
customizing the number of rays traced to obtain better estimates of volumes. As
an example, if we wanted to run the raytracing calculation with 10 million rays,
we would run::
r2s.run(timesteps, source_rates, mat_vol_kwargs={'n_samples': 10_000_000})
Direct 1-Step (D1S) Calculations
================================

View file

@ -68,17 +68,17 @@ generation, and particle number of the desired particle. For example, to create
a track file for particle 4 of batch 1 and generation 2::
settings = openmc.Settings()
settings.track = (1, 2, 4)
settings.track = [(1, 2, 4)]
To specify multiple particles, the length of the iterable should be a multiple
of three, e.g., if we wanted particles 3 and 4 from batch 1 and generation 2::
To specify multiple particles, specify a list of tuples, e.g., if we wanted
particles 3 and 4 from batch 1 and generation 2::
settings.track = (1, 2, 3, 1, 2, 4)
settings.track = [(1, 2, 3), (1, 2, 4)]
After running OpenMC, the working directory will contain a file of the form
"track_(batch #)_(generation #)_(particle #).h5" for each particle tracked.
These track files can be converted into VTK poly data files with the
:class:`openmc.Tracks` class.
After running OpenMC (now, without the ``-t`` argument), the working directory
will contain a file named `tracks.h5`, which contains a collection of particle
tracks. These track files can be converted into VTK poly data files or
matplotlib plots with the :class:`openmc.Tracks` class.
----------------------
Source Site Processing

View file

@ -765,7 +765,7 @@ energy decomposition::
# Create a "tallies.xml" file for the MGXS Library
tallies = openmc.Tallies()
mgxs_lib.add_to_tallies_file(tallies, merge=True)
mgxs_lib.add_to_tallies(tallies, merge=True)
# Export
tallies.export_to_xml()

View file

@ -756,6 +756,62 @@ instance, whereas the :meth:`openmc.Track.filter` method returns a new
track_files = [f"tracks_p{rank}.h5" for rank in range(32)]
openmc.Tracks.combine(track_files, "tracks.h5")
Collision Track File
---------------------
OpenMC can generate a collision track file that contains detailed collision
information (position, direction, energy, deposited energy, time, weight, cell
ID, material ID, universe ID, nuclide ZAID, particle type, particle delayed
group and particle ID) for each particle collision depending on user-defined
parameters. To invoke this feature, set the
:attr:`~openmc.Settings.collision_track` attribute as shown in this example::
settings.collision_track = {
"max_collisions": 300,
"reactions": ["(n,fission)", "(n,2n)"],
"material_ids": [1,2],
"nuclides": ["U238", "O16"],
"cell_ids": [5, 12]
}
In this example, collision track information is written to the
collision_track.h5 file at the end of the simulation. The file contains
300 recorded collisions that occurred in materials with IDs 1 or 2, involving
fission or (n,2n) reactions on the nuclides U-238 or O-16, within cells
with IDs 5 and 12.
The file can be read using :func:`openmc.read_collision_track_file`.
The example below shows how to extract the data from the collision_track
feature and displays the fields stored in the file:
>>> data = openmc.read_collision_track_file('collision_track.h5')
>>> data.dtype
dtype([('r', [('x', '<f8'), ('y', '<f8'), ('z', '<f8')]),
('u', [('x', '<f8'), ('y', '<f8'), ('z', '<f8')]), ('E', '<f8'),
('dE', '<f8'), ('time', '<f8'), ('wgt', '<f8'), ('event_mt', '<i4'),
('delayed_group', '<i4'), ('cell_id', '<i4'), ('nuclide_id', '<i4'),
('material_id', '<i4'), ('universe_id', '<i4'), ('n_collision', '<i4'),
('particle', '<i4'), ('parent_id', '<i8'), ('progeny_id', '<i8')])
The full list of fields is as follows:
:r: Position (each direction in [cm])
:u: Direction
:E: Energy in [eV]
:dE: Energy deposited during collision in [eV]
:time: Time in [s]
:wgt: Weight of the particle
:event_mt: Reaction MT number
:delayed_group: Delayed group of the particle
:cell_id: Cell ID
:nuclide_id: Nuclide ID (10000×Z + 10×A + M)
:material_id: Material ID
:universe_id: Universe ID
:n_collision: Number of collision suffered by the particle
:particle: Particle type
:parent_id: Source particle ID
:progeny_id: Progeny ID
-----------------------
Restarting a Simulation
-----------------------

View file

@ -133,8 +133,7 @@ random ray mode can be found in the :ref:`Random Ray User Guide <random_ray>`.
# we used for source region decomposition
wwg = openmc.WeightWindowGenerator(
method='fw_cadis',
mesh=mesh,
max_realizations=settings.batches
mesh=mesh
)
# Add generator to openmc.settings object

View file

@ -20,6 +20,8 @@ extern vector<SourceSite> source_bank;
extern SharedArray<SourceSite> surf_source_bank;
extern SharedArray<CollisionTrackSite> collision_track_bank;
extern SharedArray<SourceSite> fission_bank;
extern vector<vector<int>> ifp_source_delayed_group_bank;

103
include/openmc/bank_io.h Normal file
View file

@ -0,0 +1,103 @@
#ifndef OPENMC_BANK_IO_H
#define OPENMC_BANK_IO_H
#include "hdf5.h"
#include "openmc/message_passing.h"
#include "openmc/span.h"
#include "openmc/vector.h"
#include <algorithm>
#ifdef OPENMC_MPI
#include <mpi.h>
#endif
namespace openmc {
template<typename SiteType>
void write_bank_dataset(const char* dataset_name, hid_t group_id,
span<SiteType> bank, const vector<int64_t>& bank_index, hid_t banktype
#ifdef OPENMC_MPI
,
MPI_Datatype mpi_dtype
#endif
)
{
int64_t dims_size = bank_index.back();
int64_t count_size = bank_index[mpi::rank + 1] - bank_index[mpi::rank];
#ifdef PHDF5
hsize_t dims[] {static_cast<hsize_t>(dims_size)};
hid_t dspace = H5Screate_simple(1, dims, nullptr);
hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, H5P_DEFAULT,
H5P_DEFAULT, H5P_DEFAULT);
hsize_t count[] {static_cast<hsize_t>(count_size)};
hid_t memspace = H5Screate_simple(1, count, nullptr);
hsize_t start[] {static_cast<hsize_t>(bank_index[mpi::rank])};
H5Sselect_hyperslab(dspace, H5S_SELECT_SET, start, nullptr, count, nullptr);
hid_t plist = H5Pcreate(H5P_DATASET_XFER);
H5Pset_dxpl_mpio(plist, H5FD_MPIO_COLLECTIVE);
H5Dwrite(dset, banktype, memspace, dspace, plist, bank.data());
H5Sclose(dspace);
H5Sclose(memspace);
H5Dclose(dset);
H5Pclose(plist);
#else
if (mpi::master) {
hsize_t dims[] {static_cast<hsize_t>(dims_size)};
hid_t dspace = H5Screate_simple(1, dims, nullptr);
hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace,
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
#ifdef OPENMC_MPI
vector<SiteType> temp_bank {bank.begin(), bank.end()};
#endif
for (int i = 0; i < mpi::n_procs; ++i) {
hsize_t count[] {static_cast<hsize_t>(bank_index[i + 1] - bank_index[i])};
hid_t memspace = H5Screate_simple(1, count, nullptr);
#ifdef OPENMC_MPI
if (i > 0) {
MPI_Recv(bank.data(), count[0], mpi_dtype, i, i, mpi::intracomm,
MPI_STATUS_IGNORE);
}
#endif
hid_t dspace_rank = H5Dget_space(dset);
hsize_t start[] {static_cast<hsize_t>(bank_index[i])};
H5Sselect_hyperslab(
dspace_rank, H5S_SELECT_SET, start, nullptr, count, nullptr);
H5Dwrite(dset, banktype, memspace, dspace_rank, H5P_DEFAULT, bank.data());
H5Sclose(memspace);
H5Sclose(dspace_rank);
}
H5Dclose(dset);
#ifdef OPENMC_MPI
std::copy(temp_bank.begin(), temp_bank.end(), bank.begin());
#endif
}
#ifdef OPENMC_MPI
else {
if (!bank.empty()) {
MPI_Send(
bank.data(), bank.size(), mpi_dtype, 0, mpi::rank, mpi::intracomm);
}
}
#endif
#endif
}
} // namespace openmc
#endif // OPENMC_BANK_IO_H

View file

@ -0,0 +1,23 @@
#ifndef OPENMC_COLLISION_TRACK_H
#define OPENMC_COLLISION_TRACK_H
#include <string>
namespace openmc {
class Particle;
//! Reserve space in the collision track bank according to user settings.
void collision_track_reserve_bank();
//! Write collision track data to disk when the bank is full or the batch ends.
void collision_track_flush_bank();
//! Record the current particle as a collision-track entry when applicable.
//!
//! \param particle Particle whose collision should be recorded if eligible
void collision_track_record(Particle& particle);
} // namespace openmc
#endif // OPENMC_COLLISION_TRACK_H

View file

@ -34,6 +34,7 @@ constexpr array<int, 2> VERSION_VOXEL {2, 0};
constexpr array<int, 2> VERSION_MGXS_LIBRARY {1, 0};
constexpr array<int, 2> VERSION_PROPERTIES {1, 1};
constexpr array<int, 2> VERSION_WEIGHT_WINDOWS {1, 0};
constexpr array<int, 2> VERSION_COLLISION_TRACK {1, 0};
// ============================================================================
// ADJUSTABLE PARAMETERS
@ -291,7 +292,7 @@ enum class MgxsType {
// ============================================================================
// TALLY-RELATED CONSTANTS
enum class TallyResult { VALUE, SUM, SUM_SQ, SIZE };
enum class TallyResult { VALUE, SUM, SUM_SQ, SUM_THIRD, SUM_FOURTH };
enum class TallyType { VOLUME, MESH_SURFACE, SURFACE, PULSE_HEIGHT };

View file

@ -51,6 +51,8 @@ public:
Distribution* phi() const { return phi_.get(); }
private:
Direction v_ref_ {1.0, 0.0, 0.0}; //!< reference direction
Direction w_ref_;
UPtrDist mu_; //!< Distribution of polar angle
UPtrDist phi_; //!< Distribution of azimuthal angle
};

View file

@ -100,8 +100,8 @@ void read_llong(hid_t obj_id, const char* name, long long* buffer, bool indep);
void read_string(
hid_t obj_id, const char* name, size_t slen, char* buffer, bool indep);
void read_tally_results(
hid_t group_id, hsize_t n_filter, hsize_t n_score, double* results);
void read_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score,
hsize_t n_results, double* results);
void write_attr_double(hid_t obj_id, int ndim, const hsize_t* dims,
const char* name, const double* buffer);
void write_attr_int(hid_t obj_id, int ndim, const hsize_t* dims,
@ -114,9 +114,9 @@ void write_int(hid_t group_id, int ndim, const hsize_t* dims, const char* name,
void write_llong(hid_t group_id, int ndim, const hsize_t* dims,
const char* name, const long long* buffer, bool indep);
void write_string(hid_t group_id, int ndim, const hsize_t* dims, size_t slen,
const char* name, char const* buffer, bool indep);
void write_tally_results(
hid_t group_id, hsize_t n_filter, hsize_t n_score, const double* results);
const char* name, const char* buffer, bool indep);
void write_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score,
hsize_t n_results, const double* results);
} // extern "C"
//==============================================================================

View file

@ -38,6 +38,21 @@ vector<SourceSite> mcpl_source_sites(std::string path);
void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
const vector<int64_t>& bank_index);
//! Write an MCPL collision track file
//!
//! This function writes collision track data to an MCPL file. Additional
//! collision-specific metadata (such as energy deposition, material info, etc.)
//! is stored in the file header as blob data.
//!
//! \param[in] filename Path to MCPL file
//! \param[in] collision_track_bank Vector of CollisionTrackSites to write to
//! file for this MPI rank.
//! \param[in] bank_index Pointer to vector of site index ranges over all
//! MPI ranks.
void write_mcpl_collision_track(const char* filename,
span<CollisionTrackSite> collision_track_bank,
const vector<int64_t>& bank_index);
//! Check if MCPL functionality is available
bool is_mcpl_interface_available();

View file

@ -132,8 +132,14 @@ public:
// Constructors and destructor
Mesh() = default;
Mesh(pugi::xml_node node);
Mesh(hid_t group);
virtual ~Mesh() = default;
// Factory method for creating meshes from either an XML node or HDF5 group
template<typename T>
static const std::unique_ptr<Mesh>& create(
T dataset, const std::string& mesh_type, const std::string& mesh_library);
// Methods
//! Perform any preparation needed to support point location within the mesh
virtual void prepare_for_point_location() {};
@ -258,6 +264,7 @@ class StructuredMesh : public Mesh {
public:
StructuredMesh() = default;
StructuredMesh(pugi::xml_node node) : Mesh {node} {};
StructuredMesh(hid_t group) : Mesh {group} {};
virtual ~StructuredMesh() = default;
using MeshIndex = std::array<int, 3>;
@ -423,6 +430,7 @@ class PeriodicStructuredMesh : public StructuredMesh {
public:
PeriodicStructuredMesh() = default;
PeriodicStructuredMesh(pugi::xml_node node) : StructuredMesh {node} {};
PeriodicStructuredMesh(hid_t group) : StructuredMesh {group} {};
Position local_coords(const Position& r) const override
{
@ -442,6 +450,7 @@ public:
// Constructors
RegularMesh() = default;
RegularMesh(pugi::xml_node node);
RegularMesh(hid_t group);
// Overridden methods
int get_index_in_direction(double r, int i) const override;
@ -481,6 +490,8 @@ public:
//! Return the volume for a given mesh index
double volume(const MeshIndex& ijk) const override;
int set_grid();
// Data members
double volume_frac_; //!< Volume fraction of each mesh element
double element_volume_; //!< Volume of each mesh element
@ -492,6 +503,7 @@ public:
// Constructors
RectilinearMesh() = default;
RectilinearMesh(pugi::xml_node node);
RectilinearMesh(hid_t group);
// Overridden methods
int get_index_in_direction(double r, int i) const override;
@ -534,6 +546,7 @@ public:
// Constructors
CylindricalMesh() = default;
CylindricalMesh(pugi::xml_node node);
CylindricalMesh(hid_t group);
// Overridden methods
virtual MeshIndex get_indices(Position r, bool& in_mesh) const override;
@ -598,6 +611,7 @@ public:
// Constructors
SphericalMesh() = default;
SphericalMesh(pugi::xml_node node);
SphericalMesh(hid_t group);
// Overridden methods
virtual MeshIndex get_indices(Position r, bool& in_mesh) const override;
@ -668,6 +682,7 @@ public:
// Constructors
UnstructuredMesh() { n_dimension_ = 3; };
UnstructuredMesh(pugi::xml_node node);
UnstructuredMesh(hid_t group);
static const std::string mesh_type;
virtual std::string get_mesh_type() const override;
@ -774,6 +789,7 @@ public:
// Constructors
MOABMesh() = default;
MOABMesh(pugi::xml_node);
MOABMesh(hid_t group);
MOABMesh(const std::string& filename, double length_multiplier = 1.0);
MOABMesh(std::shared_ptr<moab::Interface> external_mbi);
@ -943,6 +959,7 @@ class LibMesh : public UnstructuredMesh {
public:
// Constructors
LibMesh(pugi::xml_node node);
LibMesh(hid_t group);
LibMesh(const std::string& filename, double length_multiplier = 1.0);
LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier = 1.0);
@ -1069,6 +1086,11 @@ private:
//! \param[in] root XML node
void read_meshes(pugi::xml_node root);
//! Read meshes from an HDF5 file
//
//! \param[in] group HDF5 group ("meshes" group)
void read_meshes(hid_t group);
//! Write mesh data to an HDF5 group
//
//! \param[in] group HDF5 group

View file

@ -18,6 +18,7 @@ extern bool master;
#ifdef OPENMC_MPI
extern MPI_Datatype source_site;
extern MPI_Datatype collision_track_site;
extern MPI_Comm intracomm;
#endif

View file

@ -56,6 +56,25 @@ struct SourceSite {
int64_t progeny_id;
};
struct CollisionTrackSite {
Position r;
Direction u;
double E;
double dE;
double time {0.0};
double wgt {1.0};
int event_mt {0};
int delayed_group {0};
int cell_id {0};
int nuclide_id;
int material_id {0};
int universe_id {0};
int n_collision {0};
ParticleType particle;
int64_t parent_id;
int64_t progeny_id;
};
//! State of a particle used for particle track files
struct TrackState {
Position r; //!< Position in [cm]

View file

@ -32,6 +32,24 @@ enum class IFPParameter {
GenerationTime,
};
struct CollisionTrackConfig {
bool mcpl_write {false}; //!< Write collision tracks using MCPL?
std::unordered_set<int>
cell_ids; //!< Cell ids where collisions will be written
std::unordered_set<int>
mt_numbers; //!< MT Numbers where collisions will be written
std::unordered_set<int>
universe_ids; //!< Universe IDs where collisions will be written
std::unordered_set<int>
material_ids; //!< Material IDs where collisions will be written
std::unordered_set<std::string>
nuclides; //!< Nuclides where collisions will be written
double deposited_energy_threshold {0.0}; //!< Minimum deposited energy [eV]
int64_t max_collisions {
1000}; //!< Maximum events recorded per collision track file
int64_t max_files {1}; //!< Maximum number of collision track files
};
//==============================================================================
// Global variable declarations
//==============================================================================
@ -41,6 +59,7 @@ namespace settings {
// Boolean flags
extern bool assume_separate; //!< assume tallies are spatially separate?
extern bool check_overlaps; //!< check overlaps in geometry?
extern bool collision_track; //!< flag to use collision track feature?
extern bool confidence_intervals; //!< use confidence intervals for results?
extern bool
create_fission_neutrons; //!< create fission neutrons (fixed source)?
@ -145,6 +164,7 @@ extern std::unordered_set<int>
statepoint_batch; //!< Batches when state should be written
extern std::unordered_set<int>
source_write_surf_id; //!< Surface ids where sources will be written
extern CollisionTrackConfig collision_track_config;
extern double source_rejection_fraction; //!< Minimum fraction of source sites
//!< that must be accepted
extern double free_gas_threshold; //!< Threshold multiplier for free gas

View file

@ -22,6 +22,7 @@ constexpr int STATUS_EXIT_ON_TRIGGER {2};
namespace simulation {
extern int ct_current_file; //!< current collision track file index
extern "C" int current_batch; //!< current batch
extern "C" int current_gen; //!< current fission generation
extern "C" bool initialized; //!< has simulation been initialized?

View file

@ -106,6 +106,8 @@ public:
bool writable() const { return writable_; }
bool higher_moments() const { return higher_moments_; }
//----------------------------------------------------------------------------
// Other methods.
@ -190,6 +192,9 @@ private:
//! Whether to multiply by atom density for reaction rates
bool multiply_density_ {true};
//! Whether to accumulate higher moments (third and fourth)
bool higher_moments_ {false};
int64_t index_;
};

View file

@ -27,10 +27,10 @@ public:
double heating;
};
Interpolation interp_; //!< interpolation type
int inelastic_flag_; //!< inelastic competition flag
int absorption_flag_; //!< other absorption flag
bool multiply_smooth_; //!< multiply by smooth cross section?
Interpolation interp_; //!< interpolation type
int inelastic_flag_; //!< inelastic competition flag
int absorption_flag_; //!< other absorption flag
bool multiply_smooth_; //!< multiply by smooth cross section?
vector<double> energy_; //!< incident energies
auto n_energy() const { return energy_.size(); }

View file

@ -37,7 +37,7 @@ def check_type(name, value, expected_type, expected_iter_type=None, *, none_ok=F
[t.__name__ for t in expected_type]))
else:
msg = (f'Unable to set "{name}" to "{value}" which is not of type "'
f'{expected_type.__name__}"')
f'{expected_type}"')
raise TypeError(msg)
if expected_iter_type:

View file

@ -17,6 +17,7 @@ from .stepresult import *
from .results import *
from .integrators import *
from .transfer_rates import *
from .r2s import *
from . import abc
from . import cram
from . import helpers

View file

@ -631,17 +631,7 @@ class Integrator(ABC):
solver: str = "cram48",
continue_timesteps: bool = False,
):
# Check number of stages previously used
if operator.prev_res is not None:
res = operator.prev_res[-1]
if res.data.shape[0] != self._num_stages:
raise ValueError(
"{} incompatible with previous restart calculation. "
"Previous scheme used {} intermediate solutions, while "
"this uses {}".format(
self.__class__.__name__, res.data.shape[0],
self._num_stages))
elif continue_timesteps:
if continue_timesteps and operator.prev_res is None:
raise ValueError("Continuation run requires passing prev_results.")
self.operator = operator
self.chain = operator.chain
@ -775,12 +765,8 @@ class Integrator(ABC):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulations
n_end : list of numpy.ndarray
Concentrations at end of timestep
"""
@property
@ -811,9 +797,9 @@ class Integrator(ABC):
"""Get beginning of step concentrations, reaction rates from restart"""
res = self.operator.prev_res[-1]
# Depletion methods expect list of arrays
bos_conc = list(res.data[0])
rates = res.rates[0]
k = ufloat(res.k[0, 0], res.k[0, 1])
bos_conc = list(res.data)
rates = res.rates
k = ufloat(res.k[0], res.k[1])
if res.source_rate != 0.0:
# Scale reaction rates by ratio of source rates
@ -855,7 +841,8 @@ class Integrator(ABC):
self,
final_step: bool = True,
output: bool = True,
path: PathLike = 'depletion_results.h5'
path: PathLike = 'depletion_results.h5',
write_rates: bool = False
):
"""Perform the entire depletion process across all steps
@ -874,6 +861,11 @@ class Integrator(ABC):
Path to file to write. Defaults to 'depletion_results.h5'.
.. versionadded:: 0.15.0
write_rates : bool, optional
Whether reaction rates should be written to the results file for
each step. Defaults to ``False`` to reduce file size.
.. versionadded:: 0.15.3
"""
with change_directory(self.operator.output_dir):
n = self.operator.initial_condition()
@ -890,18 +882,22 @@ class Integrator(ABC):
n, res = self._get_bos_data_from_restart(source_rate, n)
# Solve Bateman equations over time interval
proc_time, n_list, res_list = self(n, res.rates, dt, source_rate, i)
proc_time, n_end = self(n, res.rates, dt, source_rate, i)
# Insert BOS concentration, transport results
n_list.insert(0, n)
res_list.insert(0, res)
# Remove actual EOS concentration for next step
n = n_list.pop()
StepResult.save(self.operator, n_list, res_list, [t, t + dt],
source_rate, self._i_res + i, proc_time, path)
StepResult.save(
self.operator,
n,
res,
[t, t + dt],
source_rate,
self._i_res + i,
proc_time,
write_rates=write_rates,
path=path
)
# Update for next step
n = n_end
t += dt
# Final simulation -- in the case that final_step is False, a zero
@ -910,9 +906,18 @@ class Integrator(ABC):
# solve)
if output and final_step and comm.rank == 0:
print(f"[openmc.deplete] t={t} (final operator evaluation)")
res_list = [self.operator(n, source_rate if final_step else 0.0)]
StepResult.save(self.operator, [n], res_list, [t, t],
source_rate, self._i_res + len(self), proc_time, path)
res_final = self.operator(n, source_rate if final_step else 0.0)
StepResult.save(
self.operator,
n,
res_final,
[t, t],
source_rate,
self._i_res + len(self),
proc_time,
write_rates=write_rates,
path=path
)
self.operator.write_bos_data(len(self) + self._i_res)
self.operator.finalize()
@ -1013,6 +1018,36 @@ class Integrator(ABC):
material, composition, rate, rate_units, timesteps)
def add_redox(self, material, buffer, oxidation_states, timesteps=None):
"""Add redox control to depletable material.
Parameters
----------
material : openmc.Material or str or int
Depletable material
buffer : dict
Dictionary of buffer nuclides used to maintain redox balance. Keys
are nuclide names (strings) and values are their respective
fractions (float) that collectively sum to 1.
oxidation_states : dict
User-defined oxidation states for elements. Keys are element symbols
(e.g., 'H', 'He'), and values are their corresponding oxidation
states as integers (e.g., +1, 0).
timesteps : list of int, optional
List of timestep indices where to set external source rates.
Defaults to None, which means the external source rate is set for
all timesteps.
"""
if self.transfer_rates is None:
if hasattr(self.operator, 'model'):
materials = self.operator.model.materials
elif hasattr(self.operator, 'materials'):
materials = self.operator.materials
self.transfer_rates = TransferRates(
self.operator, materials, len(self.timesteps))
self.transfer_rates.set_redox(material, buffer, oxidation_states, timesteps)
@add_params
class SIIntegrator(Integrator):
r"""Abstract class for the Stochastic Implicit Euler integrators
@ -1141,10 +1176,40 @@ class SIIntegrator(Integrator):
self.operator.settings.particles //= self.n_steps
return inherited
@abstractmethod
def __call__(self, n, rates, dt, source_rate, i):
"""Perform the integration across one time step
Parameters
----------
n : list of numpy.ndarray
List of atom number arrays for each material. Each array has
shape ``(n_nucs,)`` where ``n_nucs`` is the number of nuclides
rates : openmc.deplete.ReactionRates
Reaction rates (from transport operator)
dt : float
Time step in [s]
source_rate : float
Power in [W] or source rate in [neutron/sec]
i : int
Current time step index
Returns
-------
proc_time : float
Time spent in transport simulation
n_end : list of numpy.ndarray
Updated atom number densities for each material
op_result : OperatorResult
Eigenvalue and reaction rates resulting from transport simulation
"""
def integrate(
self,
output: bool = True,
path: PathLike = "depletion_results.h5"
path: PathLike = "depletion_results.h5",
write_rates: bool = False
):
"""Perform the entire depletion process across all steps
@ -1156,11 +1221,17 @@ class SIIntegrator(Integrator):
Path to file to write. Defaults to 'depletion_results.h5'.
.. versionadded:: 0.15.0
write_rates : bool, optional
Whether reaction rates should be written to the results file for
each step. Defaults to ``False`` to reduce file size.
.. versionadded:: 0.15.3
"""
with change_directory(self.operator.output_dir):
n = self.operator.initial_condition()
t, self._i_res = self._get_start_data()
res_end = None # Will be set in first iteration
for i, (dt, p) in enumerate(self):
if output:
print(f"[openmc.deplete] t={t} s, dt={dt} s, source={p}")
@ -1170,28 +1241,38 @@ class SIIntegrator(Integrator):
n, res = self._get_bos_data_from_operator(i, p, n)
else:
n, res = self._get_bos_data_from_restart(p, n)
else:
# Pull rates, k from previous iteration w/o
# re-running transport
res = res_list[-1] # defined in previous i iteration
proc_time, n_list, res_list = self(n, res.rates, dt, p, i)
proc_time, n_end, res_end = self(n, res.rates, dt, p, i)
# Insert BOS concentration, transport results
n_list.insert(0, n)
res_list.insert(0, res)
# Remove actual EOS concentration for next step
n = n_list.pop()
StepResult.save(self.operator, n_list, res_list, [t, t + dt],
p, self._i_res + i, proc_time, path)
StepResult.save(
self.operator,
n,
res,
[t, t + dt],
p,
self._i_res + i,
proc_time,
write_rates=write_rates,
path=path
)
# Update for next step
n = n_end
res = res_end
t += dt
# No final simulation for SIE, use last iteration results
StepResult.save(self.operator, [n], [res_list[-1]], [t, t],
p, self._i_res + len(self), proc_time, path)
StepResult.save(
self.operator,
n,
res_end,
[t, t],
p,
self._i_res + len(self),
proc_time,
write_rates=write_rates,
path=path
)
self.operator.write_bos_data(self._i_res + len(self))
self.operator.finalize()

View file

@ -7,6 +7,7 @@ loaded from an .xml file and all the nuclides are linked together.
from io import StringIO
from itertools import chain
import math
import numpy as np
import re
from collections import defaultdict, namedtuple
from collections.abc import Mapping, Iterable
@ -629,7 +630,7 @@ class Chain:
n = len(self)
# we accumulate indices and value entries for everything and create the matrix
# we accumulate indices and value entries for everything and create the matrix
# in one step at the end to avoid expensive index checks scipy otherwise does.
rows, cols, vals = [], [], []
def setval(i, j, val):
@ -714,6 +715,62 @@ class Chain:
# Return CSC representation instead of DOK
return sp.csc_matrix((vals, (rows, cols)), shape=(n, n))
def add_redox_term(self, matrix, buffer, oxidation_states):
r"""Adds a redox term to the depletion matrix from data contained in
the matrix itself and a few user-inputs.
The redox term to add to the buffer nuclide :math:`N_j` can be written
as:
.. math::
\frac{dN_j(t)}{dt} = \cdots - \frac{1}{OS_j}\sum_i N_i a_{ij}
\cdot OS_i
where :math:`OS` is the oxidation states vector and :math:`a_{ij}` the
corresponding term in the Bateman matrix.
Parameters
----------
matrix : scipy.sparse.csc_matrix
Sparse matrix representing depletion
buffer : dict
Dictionary of buffer nuclides used to maintain anoins net balance.
Keys are nuclide names (strings) and values are their respective
fractions (float) that collectively sum to 1.
oxidation_states : dict
User-defined oxidation states for elements. Keys are element symbols
(e.g., 'H', 'He'), and values are their corresponding oxidation
states as integers (e.g., +1, 0).
Returns
-------
matrix : scipy.sparse.csc_matrix
Sparse matrix with redox term added
"""
# Elements list with the same size as self.nuclides
elements = [re.split(r'\d+', nuc.name)[0] for nuc in self.nuclides]
# Match oxidation states with all elements and add 0 if not data
os = np.array([oxidation_states[elm] if elm in oxidation_states else 0
for elm in elements])
# Buffer idx with nuclide index as value
buffer_idx = {nuc: self.nuclide_dict[nuc] for nuc in buffer}
array = matrix.toarray()
redox_change = np.array([])
# calculate the redox array
for i in range(len(self)):
# Net redox impact of reaction: multiply the i-th column of the
# depletion matrix by the oxidation states
redox_change = np.append(redox_change, sum(array[:, i]*os))
# Subtract redox vector to the buffer nuclides in the matrix scaling by
# their respective oxidation states
for nuc, idx in buffer_idx.items():
array[idx] -= redox_change * buffer[nuc] / os[idx]
return sp.csc_matrix(array)
def form_rr_term(self, tr_rates, current_timestep, mats):
"""Function to form the transfer rate term matrices.

View file

@ -5,7 +5,7 @@ shutdown dose rate calculations.
"""
from copy import deepcopy
from copy import copy
from typing import Sequence
from math import log, prod
@ -164,8 +164,12 @@ def apply_time_correction(
radionuclides = [str(x) for x in tally.filters[i_filter].bins]
tcf = np.array([time_correction_factors[x][index] for x in radionuclides])
# Create copy of tally
new_tally = deepcopy(tally)
# Force tally results to be read and std_dev to be computed
tally.std_dev
# Create shallow copy of tally
new_tally = copy(tally)
new_tally._filters = copy(tally._filters)
# Determine number of bins in other filters
n_bins_before = prod([f.num_bins for f in tally.filters[:i_filter]])
@ -177,32 +181,33 @@ def apply_time_correction(
shape = (n_bins_before, n_radionuclides, n_bins_after, n_nuclides, n_scores)
tally_sum = new_tally.sum.reshape(shape)
tally_sum_sq = new_tally.sum_sq.reshape(shape)
tally_mean = new_tally.mean.reshape(shape)
tally_std_dev = new_tally.std_dev.reshape(shape)
# Apply TCF, broadcasting to the correct dimensions
tcf.shape = (1, -1, 1, 1, 1)
new_tally._sum = tally_sum * tcf
new_tally._sum_sq = tally_sum_sq * (tcf*tcf)
new_tally._mean = None
new_tally._std_dev = None
new_tally._mean = tally_mean * tcf
new_tally._std_dev = tally_std_dev * tcf
shape = (-1, n_nuclides, n_scores)
if sum_nuclides:
# Query the mean and standard deviation
mean = new_tally.mean
std_dev = new_tally.std_dev
# Sum over parent nuclides (note that when combining different bins for
# parent nuclide, we can't work directly on sum_sq)
new_tally._mean = mean.sum(axis=1).reshape(shape)
new_tally._std_dev = np.linalg.norm(std_dev, axis=1).reshape(shape)
new_tally._mean = new_tally.mean.sum(axis=1).reshape(shape)
new_tally._std_dev = np.linalg.norm(new_tally.std_dev, axis=1).reshape(shape)
new_tally._derived = True
# Remove ParentNuclideFilter
new_tally.filters.pop(i_filter)
else:
# Change shape back to (filter combinations, nuclides, scores)
new_tally._sum.shape = shape
new_tally._sum_sq.shape = shape
new_tally._mean.shape = shape
new_tally._std_dev.shape = shape
return new_tally

View file

@ -46,15 +46,12 @@ class PredictorIntegrator(Integrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
n_end : list of numpy.ndarray
Concentrations at end of interval
op_results : empty list
Kept for consistency with API. No intermediate calls to operator
with predictor
"""
proc_time, n_end = self._timed_deplete(n, rates, dt, _i)
return proc_time, [n_end], []
return proc_time, n_end
@add_params
@ -98,11 +95,8 @@ class CECMIntegrator(Integrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
Eigenvalue and reaction rates from transport simulations
n_end : list of numpy.ndarray
Concentrations at end of interval
"""
# deplete across first half of interval
time0, n_middle = self._timed_deplete(n, rates, dt / 2, _i)
@ -112,7 +106,7 @@ class CECMIntegrator(Integrator):
# MOS reaction rates
time1, n_end = self._timed_deplete(n, res_middle.rates, dt, _i)
return time0 + time1, [n_middle, n_end], [res_middle]
return time0 + time1, n_end
@add_params
@ -162,12 +156,8 @@ class CF4Integrator(Integrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulations
n_end : list of numpy.ndarray
Concentrations at end of interval
"""
# Step 1: deplete with matrix 1/2*A(y0)
time1, n_eos1 = self._timed_deplete(
@ -192,9 +182,7 @@ class CF4Integrator(Integrator):
time5, n_eos5 = self._timed_deplete(
n_inter, list_rates, dt, _i, matrix_func=cf4_f4)
return (time1 + time2 + time3 + time4 + time5,
[n_eos1, n_eos2, n_eos3, n_eos5],
[res1, res2, res3])
return time1 + time2 + time3 + time4 + time5, n_eos5
@add_params
@ -240,12 +228,8 @@ class CELIIntegrator(Integrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulation
n_end : list of numpy.ndarray
Concentrations at end of interval
"""
# deplete to end using BOS rates
proc_time, n_ce = self._timed_deplete(n_bos, rates, dt, _i)
@ -260,7 +244,7 @@ class CELIIntegrator(Integrator):
time_le2, n_end = self._timed_deplete(
n_inter, list_rates, dt, _i, matrix_func=celi_f2)
return proc_time + time_le1 + time_le1, [n_ce, n_end], [res_ce]
return proc_time + time_le1 + time_le2, n_end
@add_params
@ -306,12 +290,8 @@ class EPCRK4Integrator(Integrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulations
n_end : list of numpy.ndarray
Concentrations at end of interval
"""
# Step 1: deplete with matrix A(y0) / 2
@ -330,7 +310,7 @@ class EPCRK4Integrator(Integrator):
list_rates = list(zip(rates, res1.rates, res2.rates, res3.rates))
time4, n4 = self._timed_deplete(n, list_rates, dt, _i, matrix_func=rk4_f4)
return (time1 + time2 + time3 + time4, [n1, n2, n3, n4], [res1, res2, res3])
return time1 + time2 + time3 + time4, n4
@add_params
@ -388,12 +368,8 @@ class LEQIIntegrator(Integrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulation
n_end : list of numpy.ndarray
Concentrations at end of interval
"""
if i == 0:
if self._i_res < 1: # need at least previous transport solution
@ -402,7 +378,7 @@ class LEQIIntegrator(Integrator):
self, n_bos, bos_rates, dt, source_rate, i)
prev_res = self.operator.prev_res[-2]
prev_dt = self.timesteps[i] - prev_res.time[0]
self._prev_rates = prev_res.rates[0]
self._prev_rates = prev_res.rates
else:
prev_dt = self.timesteps[i - 1]
@ -431,9 +407,7 @@ class LEQIIntegrator(Integrator):
# store updated rates
self._prev_rates = copy.deepcopy(bos_res.rates)
return (
time1 + time2 + time3 + time4, [n_eos0, n_eos1],
[bos_res, res_inter])
return time1 + time2 + time3 + time4, n_eos1
@add_params
@ -470,10 +444,9 @@ class SICELIIntegrator(SIIntegrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_bos_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
n_end : list of numpy.ndarray
Concentrations at end of interval
op_result : openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulations
"""
@ -499,7 +472,7 @@ class SICELIIntegrator(SIIntegrator):
proc_time += time1 + time2
# end iteration
return proc_time, [n_eos, n_inter], [res_bar]
return proc_time, n_inter, res_bar
@add_params
@ -536,10 +509,9 @@ class SILEQIIntegrator(SIIntegrator):
-------
proc_time : float
Time spent in CRAM routines for all materials in [s]
n_list : list of list of numpy.ndarray
Concentrations at each of the intermediate points with
the final concentration as the last element
op_results : list of openmc.deplete.OperatorResult
n_end : list of numpy.ndarray
Concentrations at end of interval
op_result : openmc.deplete.OperatorResult
Eigenvalue and reaction rates from intermediate transport
simulation
"""
@ -551,7 +523,7 @@ class SILEQIIntegrator(SIIntegrator):
self, n_bos, bos_rates, dt, source_rate, i)
prev_res = self.operator.prev_res[-2]
prev_dt = self.timesteps[i] - prev_res.time[0]
self._prev_rates = prev_res.rates[0]
self._prev_rates = prev_res.rates
else:
prev_dt = self.timesteps[i - 1]
@ -584,7 +556,10 @@ class SILEQIIntegrator(SIIntegrator):
n_inter, inputs, dt, i, matrix_func=leqi_f4)
proc_time += time1 + time2
return proc_time, [n_eos, n_inter], [res_bar]
# Store updated rates for next step
self._prev_rates = copy.deepcopy(bos_rates)
return proc_time, n_inter, res_bar
integrator_by_name = {

View file

@ -8,8 +8,9 @@ from __future__ import annotations
from collections.abc import Sequence
import shutil
from tempfile import TemporaryDirectory
from typing import Union, TypeAlias
from typing import Union, TypeAlias, Self
import h5py
import pandas as pd
import numpy as np
@ -20,6 +21,7 @@ from openmc.data import REACTION_MT
import openmc
from .chain import Chain, REACTIONS, _get_chain
from .coupled_operator import _find_cross_sections, _get_nuclides_with_data
from ..utility_funcs import h5py_file_or_group
import openmc.lib
from openmc.mpi import comm
@ -47,6 +49,7 @@ def get_microxs_and_flux(
reaction_rate_mode: str = 'direct',
chain_file: PathLike | Chain | None = None,
path_statepoint: PathLike | None = None,
path_input: PathLike | None = None,
run_kwargs=None
) -> tuple[list[np.ndarray], list[MicroXS]]:
"""Generate microscopic cross sections and fluxes for multiple domains.
@ -59,7 +62,7 @@ def get_microxs_and_flux(
.. versionadded:: 0.14.0
.. versionchanged:: 0.15.3
Added `reaction_rate_mode` and `path_statepoint` arguments.
Added `reaction_rate_mode`, `path_statepoint`, `path_input` arguments.
Parameters
----------
@ -90,6 +93,10 @@ def get_microxs_and_flux(
Path to write the statepoint file from the neutron transport solve to.
By default, The statepoint file is written to a temporary directory and
is not kept.
path_input : path-like, optional
Path to write the model XML file from the neutron transport solve to.
By default, the model XML file is written to a temporary directory and
not kept.
run_kwargs : dict, optional
Keyword arguments passed to :meth:`openmc.Model.run`
@ -108,7 +115,7 @@ def get_microxs_and_flux(
check_value('reaction_rate_mode', reaction_rate_mode, {'direct', 'flux'})
# Save any original tallies on the model
original_tallies = model.tallies
original_tallies = list(model.tallies)
# Determine what reactions and nuclides are available in chain
chain = _get_chain(chain_file)
@ -163,13 +170,14 @@ def get_microxs_and_flux(
# Reinitialize with tallies
openmc.lib.init(intracomm=comm)
# create temporary run
with TemporaryDirectory() as temp_dir:
if run_kwargs is None:
run_kwargs = {}
else:
run_kwargs = dict(run_kwargs)
run_kwargs.setdefault('cwd', temp_dir)
# Indicate to run in temporary directory unless being executed through
# openmc.lib, in which case we don't need to specify the cwd
run_kwargs = dict(run_kwargs) if run_kwargs else {}
if not openmc.lib.is_initialized:
run_kwargs.setdefault('cwd', temp_dir)
# Run transport simulation
statepoint_path = model.run(**run_kwargs)
if comm.rank == 0:
@ -178,15 +186,22 @@ def get_microxs_and_flux(
shutil.move(statepoint_path, path_statepoint)
statepoint_path = path_statepoint
with StatePoint(statepoint_path) as sp:
if reaction_rate_mode == 'direct':
rr_tally = sp.tallies[rr_tally.id]
rr_tally._read_results()
flux_tally = sp.tallies[flux_tally.id]
flux_tally._read_results()
# Export the model to path_input if provided
if path_input is not None:
model.export_to_model_xml(path_input)
# Broadcast updated statepoint path to all ranks
statepoint_path = comm.bcast(statepoint_path)
# Read in tally results (on all ranks)
with StatePoint(statepoint_path) as sp:
if reaction_rate_mode == 'direct':
rr_tally = sp.tallies[rr_tally.id]
rr_tally._read_results()
flux_tally = sp.tallies[flux_tally.id]
flux_tally._read_results()
# Get flux values and make energy groups last dimension
flux_tally = comm.bcast(flux_tally)
flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1)
flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups)
@ -195,7 +210,6 @@ def get_microxs_and_flux(
if reaction_rate_mode == 'direct':
# Get reaction rates
rr_tally = comm.bcast(rr_tally)
reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions)
# Make energy groups last dimension
@ -345,13 +359,17 @@ class MicroXS:
reactions = chain.reactions
mts = [REACTION_MT[name] for name in reactions]
# Normalize multigroup flux
multigroup_flux = np.array(multigroup_flux)
multigroup_flux /= multigroup_flux.sum()
# Create 3D array for microscopic cross sections
microxs_arr = np.zeros((len(nuclides), len(mts), 1))
# If flux is zero, safely return zero cross sections
multigroup_flux = np.array(multigroup_flux)
if (flux_sum := multigroup_flux.sum()) == 0.0:
return cls(microxs_arr, nuclides, reactions)
# Normalize multigroup flux
multigroup_flux /= flux_sum
# Compute microscopic cross sections within a temporary session
with openmc.lib.TemporarySession(**init_kwargs):
# For each nuclide and reaction, compute the flux-averaged xs
@ -383,8 +401,7 @@ class MicroXS:
MicroXS
"""
if 'float_precision' not in kwargs:
kwargs['float_precision'] = 'round_trip'
kwargs.setdefault('float_precision', 'round_trip')
df = pd.read_csv(csv_file, **kwargs)
df.set_index(['nuclides', 'reactions', 'groups'], inplace=True)
@ -419,3 +436,96 @@ class MicroXS:
)
df = pd.DataFrame({'xs': self.data.flatten()}, index=multi_index)
df.to_csv(*args, **kwargs)
def to_hdf5(self, group_or_filename: h5py.Group | PathLike, **kwargs):
"""Export microscopic cross section data to HDF5 format
Parameters
----------
group_or_filename : h5py.Group or path-like
HDF5 group or filename to write to
kwargs : dict, optional
Keyword arguments to pass to :meth:`h5py.Group.create_dataset`.
Defaults to {'compression': 'lzf'}.
"""
kwargs.setdefault('compression', 'lzf')
with h5py_file_or_group(group_or_filename, 'w') as group:
# Store cross section data as 3D dataset
group.create_dataset('data', data=self.data, **kwargs)
# Store metadata as datasets using string encoding
group.create_dataset('nuclides', data=np.array(self.nuclides, dtype='S'))
group.create_dataset('reactions', data=np.array(self.reactions, dtype='S'))
@classmethod
def from_hdf5(cls, group_or_filename: h5py.Group | PathLike) -> Self:
"""Load data from an HDF5 file
Parameters
----------
group_or_filename : h5py.Group or str or PathLike
HDF5 group or path to HDF5 file. If given as an h5py.Group, the
data is read from that group. If given as a string, it is assumed
to be the filename for the HDF5 file.
Returns
-------
MicroXS
"""
with h5py_file_or_group(group_or_filename, 'r') as group:
# Read data from HDF5 group
data = group['data'][:]
nuclides = [nuc.decode('utf-8') for nuc in group['nuclides'][:]]
reactions = [rxn.decode('utf-8') for rxn in group['reactions'][:]]
return cls(data, nuclides, reactions)
def write_microxs_hdf5(
micros: Sequence[MicroXS],
filename: PathLike,
names: Sequence[str] | None = None,
**kwargs
):
"""Write multiple MicroXS objects to an HDF5 file
Parameters
----------
micros : list of MicroXS
List of MicroXS objects
filename : PathLike
Output HDF5 filename
names : list of str, optional
Names for each MicroXS object. If None, uses 'domain_0', 'domain_1',
etc.
**kwargs
Additional keyword arguments passed to :meth:`h5py.Group.create_dataset`
"""
if names is None:
names = [f'domain_{i}' for i in range(len(micros))]
# Open file once and write all domains using group interface
with h5py.File(filename, 'w') as f:
for microxs, name in zip(micros, names):
group = f.create_group(name)
microxs.to_hdf5(group, **kwargs)
def read_microxs_hdf5(filename: PathLike) -> dict[str, MicroXS]:
"""Read multiple MicroXS objects from an HDF5 file
Parameters
----------
filename : path-like
HDF5 filename
Returns
-------
dict
Dictionary mapping domain names to MicroXS objects
"""
with h5py.File(filename, 'r') as f:
return {name: MicroXS.from_hdf5(group) for name, group in f.items()}

View file

@ -109,6 +109,13 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
matrices = [matrix - transfer for (matrix, transfer) in zip(matrices,
transfers)]
if transfer_rates.redox:
for mat_idx, mat_id in enumerate(transfer_rates.local_mats):
if mat_id in transfer_rates.redox:
matrices[mat_idx] = chain.add_redox_term(matrices[mat_idx],
transfer_rates.redox[mat_id][0],
transfer_rates.redox[mat_id][1])
if current_timestep in transfer_rates.index_transfer:
# Gather all on comm.rank 0
matrices = comm.gather(matrices)
@ -125,6 +132,12 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
transfer_matrix = chain.form_rr_term(transfer_rates,
current_timestep,
mat_pair)
# check if destination material has a redox control
if mat_pair[0] in transfer_rates.redox:
transfer_matrix = chain.add_redox_term(transfer_matrix,
transfer_rates.redox[mat_pair[0]][0],
transfer_rates.redox[mat_pair[0]][1])
transfer_pair[mat_pair] = transfer_matrix
# Combine all matrices together in a single matrix of matrices

692
openmc/deplete/r2s.py Normal file
View file

@ -0,0 +1,692 @@
from __future__ import annotations
from collections.abc import Sequence
import copy
from datetime import datetime
import json
from pathlib import Path
import numpy as np
import openmc
from . import IndependentOperator, PredictorIntegrator
from .microxs import get_microxs_and_flux, write_microxs_hdf5, read_microxs_hdf5
from .results import Results
from ..checkvalue import PathLike
from ..mpi import comm
from openmc.lib import TemporarySession
from openmc.utility_funcs import change_directory
def get_activation_materials(
model: openmc.Model, mmv: openmc.MeshMaterialVolumes
) -> openmc.Materials:
"""Get a list of activation materials for each mesh element/material.
When performing a mesh-based R2S calculation, a unique material is needed
for each activation region, which is a combination of a mesh element and a
material within that mesh element. This function generates a list of such
materials, each with a unique name and volume corresponding to the mesh
element and material.
Parameters
----------
model : openmc.Model
The full model containing the geometry and materials.
mmv : openmc.MeshMaterialVolumes
The mesh material volumes object containing the materials and their
volumes for each mesh element.
Returns
-------
openmc.Materials
A list of materials, each corresponding to a unique mesh element and
material combination.
"""
# Get the material ID, volume, and element index for each element-material
# combination
mat_ids = mmv._materials[mmv._materials > -1]
volumes = mmv._volumes[mmv._materials > -1]
elems, _ = np.where(mmv._materials > -1)
# Get all materials in the model
material_dict = model._get_all_materials()
# Create a new activation material for each element-material combination
materials = openmc.Materials()
for elem, mat_id, vol in zip(elems, mat_ids, volumes):
mat = material_dict[mat_id]
new_mat = mat.clone()
new_mat.depletable = True
new_mat.name = f'Element {elem}, Material {mat_id}'
new_mat.volume = vol
materials.append(new_mat)
return materials
class R2SManager:
"""Manager for Rigorous 2-Step (R2S) method calculations.
This class is responsible for managing the materials and sources needed for
mesh-based or cell-based R2S calculations. It provides methods to get
activation materials and decay photon sources based on the mesh/cells and
materials in the OpenMC model.
This class supports the use of a different models for the neutron and photon
transport calculation. However, for cell-based calculations, it assumes that
the only changes in the model are material assignments. For mesh-based
calculations, it checks material assignments in the photon model and any
element--material combinations that don't appear in the photon model are
skipped.
Parameters
----------
neutron_model : openmc.Model
The OpenMC model to use for neutron transport.
domains : openmc.MeshBase or Sequence[openmc.Cell]
The mesh or a sequence of cells that represent the spatial units over
which the R2S calculation will be performed.
photon_model : openmc.Model, optional
The OpenMC model to use for photon transport calculations. If None, a
shallow copy of the neutron_model will be created and used.
Attributes
----------
domains : openmc.MeshBase or Sequence[openmc.Cell]
The mesh or a sequence of cells that represent the spatial units over
which the R2S calculation will be performed.
neutron_model : openmc.Model
The OpenMC model used for neutron transport.
photon_model : openmc.Model
The OpenMC model used for photon transport calculations.
method : {'mesh-based', 'cell-based'}
Indicates whether the R2S calculation uses mesh elements ('mesh-based')
as the spatial discetization or a list of a cells ('cell-based').
results : dict
A dictionary that stores results from the R2S calculation.
"""
def __init__(
self,
neutron_model: openmc.Model,
domains: openmc.MeshBase | Sequence[openmc.Cell],
photon_model: openmc.Model | None = None,
):
self.neutron_model = neutron_model
if photon_model is None:
# Create a shallow copy of the neutron model for photon transport
self.photon_model = openmc.Model(
geometry=copy.copy(neutron_model.geometry),
materials=copy.copy(neutron_model.materials),
settings=copy.copy(neutron_model.settings),
tallies=copy.copy(neutron_model.tallies),
plots=copy.copy(neutron_model.plots),
)
else:
self.photon_model = photon_model
if isinstance(domains, openmc.MeshBase):
self.method = 'mesh-based'
else:
self.method = 'cell-based'
self.domains = domains
self.results = {}
def run(
self,
timesteps: Sequence[float] | Sequence[tuple[float, str]],
source_rates: float | Sequence[float],
timestep_units: str = 's',
photon_time_indices: Sequence[int] | None = None,
output_dir: PathLike | None = None,
bounding_boxes: dict[int, openmc.BoundingBox] | None = None,
chain_file: PathLike | None = None,
micro_kwargs: dict | None = None,
mat_vol_kwargs: dict | None = None,
run_kwargs: dict | None = None,
operator_kwargs: dict | None = None,
):
"""Run the R2S calculation.
Parameters
----------
timesteps : Sequence[float] or Sequence[tuple[float, str]]
Sequence of timesteps. Note that values are not cumulative. The
units are specified by the `timestep_units` argument when
`timesteps` is an iterable of float. Alternatively, units can be
specified for each step by passing an iterable of (value, unit)
tuples.
source_rates : float or Sequence[float]
Source rate in [neutron/sec] for each interval in `timesteps`.
timestep_units : {'s', 'min', 'h', 'd', 'a'}, optional
Units for values specified in the `timesteps` argument when passing
float values. 's' means seconds, 'min' means minutes, 'h' means
hours, 'd' means days, and 'a' means years (Julian).
photon_time_indices : Sequence[int], optional
Sequence of time indices at which photon transport should be run;
represented as indices into the array of times formed by the
timesteps. For example, if two timesteps are specified, the array of
times would contain three entries, and [2] would indicate computing
photon results at the last time. A value of None indicates to run
photon transport for each time.
output_dir : PathLike, optional
Path to directory where R2S calculation outputs will be saved. If
not provided, a timestamped directory 'r2s_YYYY-MM-DDTHH-MM-SS' is
created. Subdirectories will be created for the neutron transport,
activation, and photon transport steps.
bounding_boxes : dict[int, openmc.BoundingBox], optional
Dictionary mapping cell IDs to bounding boxes used for spatial
source sampling in cell-based R2S calculations. Required if method
is 'cell-based'.
chain_file : PathLike, optional
Path to the depletion chain XML file to use during activation. If
not provided, the default configured chain file will be used.
micro_kwargs : dict, optional
Additional keyword arguments passed to
:func:`openmc.deplete.get_microxs_and_flux` during the neutron
transport step.
mat_vol_kwargs : dict, optional
Additional keyword arguments passed to
:meth:`openmc.MeshBase.material_volumes`.
run_kwargs : dict, optional
Additional keyword arguments passed to :meth:`openmc.Model.run`
during the neutron and photon transport step. By default, output is
disabled.
operator_kwargs : dict, optional
Additional keyword arguments passed to
:class:`openmc.deplete.IndependentOperator`.
Returns
-------
Path
Path to the output directory containing all calculation results
"""
if output_dir is None:
# Create timestamped output directory and broadcast to all ranks for
# consistency (different ranks may have slightly different times)
stamp = datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
output_dir = Path(comm.bcast(f'r2s_{stamp}'))
# Set run_kwargs for the neutron transport step
if micro_kwargs is None:
micro_kwargs = {}
if run_kwargs is None:
run_kwargs = {}
if operator_kwargs is None:
operator_kwargs = {}
run_kwargs.setdefault('output', False)
micro_kwargs.setdefault('run_kwargs', run_kwargs)
# If a chain file is provided, prefer it for steps 1 and 2
if chain_file is not None:
micro_kwargs.setdefault('chain_file', chain_file)
operator_kwargs.setdefault('chain_file', chain_file)
self.step1_neutron_transport(
output_dir / 'neutron_transport', mat_vol_kwargs, micro_kwargs
)
self.step2_activation(
timesteps, source_rates, timestep_units, output_dir / 'activation',
operator_kwargs=operator_kwargs
)
self.step3_photon_transport(
photon_time_indices, bounding_boxes, output_dir / 'photon_transport',
mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs
)
return output_dir
def step1_neutron_transport(
self,
output_dir: PathLike = "neutron_transport",
mat_vol_kwargs: dict | None = None,
micro_kwargs: dict | None = None
):
"""Run the neutron transport step.
This step computes the material volume fractions on the mesh, creates a
mesh-material filter, and retrieves the fluxes and microscopic cross
sections for each mesh/material combination. This step will populate the
'fluxes' and 'micros' keys in the results dictionary. For a mesh-based
calculation, it will also populate the 'mesh_material_volumes' key.
Parameters
----------
output_dir : PathLike, optional
The directory where the results will be saved.
mat_vol_kwargs : dict, optional
Additional keyword arguments based to
:meth:`openmc.MeshBase.material_volumes`.
micro_kwargs : dict, optional
Additional keyword arguments passed to
:func:`openmc.deplete.get_microxs_and_flux`.
"""
output_dir = Path(output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
if self.method == 'mesh-based':
# Compute material volume fractions on the mesh
if mat_vol_kwargs is None:
mat_vol_kwargs = {}
self.results['mesh_material_volumes'] = mmv = comm.bcast(
self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs))
# Save results to file
if comm.rank == 0:
mmv.save(output_dir / 'mesh_material_volumes.npz')
# Create mesh-material filter based on what combos were found
domains = openmc.MeshMaterialFilter.from_volumes(self.domains, mmv)
else:
domains: Sequence[openmc.Cell] = self.domains
# Check to make sure that each cell is filled with a material and
# that the volume has been set
# TODO: If volumes are not set, run volume calculation for cells
for cell in domains:
if cell.fill is None:
raise ValueError(
f"Cell {cell.id} is not filled with a materials. "
"Please set the fill material for each cell before "
"running the R2S calculation."
)
if cell.volume is None:
raise ValueError(
f"Cell {cell.id} does not have a volume set. "
"Please set the volume for each cell before running "
"the R2S calculation."
)
# Set default keyword arguments for microxs and flux calculation
if micro_kwargs is None:
micro_kwargs = {}
micro_kwargs.setdefault('path_statepoint', output_dir / 'statepoint.h5')
micro_kwargs.setdefault('path_input', output_dir / 'model.xml')
# Run neutron transport and get fluxes and micros. Run via openmc.lib to
# maintain a consistent parallelism strategy with the activation step.
with TemporarySession():
self.results['fluxes'], self.results['micros'] = get_microxs_and_flux(
self.neutron_model, domains, **micro_kwargs)
# Save flux and micros to file
if comm.rank == 0:
np.save(output_dir / 'fluxes.npy', self.results['fluxes'])
write_microxs_hdf5(self.results['micros'], output_dir / 'micros.h5')
def step2_activation(
self,
timesteps: Sequence[float] | Sequence[tuple[float, str]],
source_rates: float | Sequence[float],
timestep_units: str = 's',
output_dir: PathLike = 'activation',
operator_kwargs: dict | None = None,
):
"""Run the activation step.
This step creates a unique copy of each activation material based on the
mesh elements or cells, then solves the depletion equations for each
material using the fluxes and microscopic cross sections obtained in the
neutron transport step. This step will populate the 'depletion_results'
and 'activation_materials' keys in the results dictionary.
Parameters
----------
timesteps : Sequence[float] or Sequence[tuple[float, str]]
Sequence of timesteps. Note that values are not cumulative. The
units are specified by the `timestep_units` argument when
`timesteps` is an iterable of float. Alternatively, units can be
specified for each step by passing an iterable of (value, unit)
tuples.
source_rates : float | Sequence[float]
Source rate in [neutron/sec] for each interval in `timesteps`.
timestep_units : {'s', 'min', 'h', 'd', 'a'}, optional
Units for values specified in the `timesteps` argument when passing
float values. 's' means seconds, 'min' means minutes, 'h' means
hours, 'd' means days, and 'a' means years (Julian).
output_dir : PathLike, optional
Path to directory where activation calculation outputs will be
saved.
operator_kwargs : dict, optional
Additional keyword arguments passed to
:class:`openmc.deplete.IndependentOperator`.
"""
if self.method == 'mesh-based':
# Get unique material for each (mesh, material) combination
mmv = self.results['mesh_material_volumes']
self.results['activation_materials'] = get_activation_materials(self.neutron_model, mmv)
else:
# Create unique material for each cell
activation_mats = openmc.Materials()
for cell in self.domains:
mat = cell.fill.clone()
mat.name = f'Cell {cell.id}'
mat.depletable = True
mat.volume = cell.volume
activation_mats.append(mat)
self.results['activation_materials'] = activation_mats
# Save activation materials to file
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
self.results['activation_materials'].export_to_xml(
output_dir / 'materials.xml')
# Create depletion operator for the activation materials
if operator_kwargs is None:
operator_kwargs = {}
operator_kwargs.setdefault('normalization_mode', 'source-rate')
op = IndependentOperator(
self.results['activation_materials'],
self.results['fluxes'],
self.results['micros'],
**operator_kwargs
)
# Create time integrator and solve depletion equations
integrator = PredictorIntegrator(
op, timesteps, source_rates=source_rates, timestep_units=timestep_units
)
output_path = output_dir / 'depletion_results.h5'
integrator.integrate(final_step=False, path=output_path)
# Get depletion results
self.results['depletion_results'] = Results(output_path)
def step3_photon_transport(
self,
time_indices: Sequence[int] | None = None,
bounding_boxes: dict[int, openmc.BoundingBox] | None = None,
output_dir: PathLike = 'photon_transport',
mat_vol_kwargs: dict | None = None,
run_kwargs: dict | None = None,
):
"""Run the photon transport step.
This step performs photon transport calculations using decay photon
sources created from the activated materials. For each specified time,
it creates appropriate photon sources and runs a transport calculation.
In mesh-based mode, the sources are created using the mesh material
volumes, while in cell-based mode, they are created using bounding boxes
for each cell. This step will populate the 'photon_tallies' key in the
results dictionary.
Parameters
----------
time_indices : Sequence[int], optional
Sequence of time indices at which photon transport should be run;
represented as indices into the array of times formed by the
timesteps. For example, if two timesteps are specified, the array of
times would contain three entries, and [2] would indicate computing
photon results at the last time. A value of None indicates to run
photon transport for each time.
bounding_boxes : dict[int, openmc.BoundingBox], optional
Dictionary mapping cell IDs to bounding boxes used for spatial
source sampling in cell-based R2S calculations. Required if method
is 'cell-based'.
output_dir : PathLike, optional
Path to directory where photon transport outputs will be saved.
mat_vol_kwargs : dict, optional
Additional keyword arguments passed to
:meth:`openmc.MeshBase.material_volumes`.
run_kwargs : dict, optional
Additional keyword arguments passed to :meth:`openmc.Model.run`
during the photon transport step. By default, output is disabled.
"""
# TODO: Automatically determine bounding box for each cell
if bounding_boxes is None and self.method == 'cell-based':
raise ValueError("bounding_boxes must be provided for cell-based "
"R2S calculations.")
# Set default run arguments if not provided
if run_kwargs is None:
run_kwargs = {}
run_kwargs.setdefault('output', False)
# Write out JSON file with tally IDs that can be used for loading
# results
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Get default time indices if not provided
if time_indices is None:
n_steps = len(self.results['depletion_results'])
time_indices = list(range(n_steps))
# Check whether the photon model is different
neutron_univ = self.neutron_model.geometry.root_universe
photon_univ = self.photon_model.geometry.root_universe
different_photon_model = (neutron_univ != photon_univ)
# For mesh-based calculations, compute material volume fractions for the
# photon model if it is different from the neutron model to account for
# potential material changes
if self.method == 'mesh-based' and different_photon_model:
self.results['mesh_material_volumes_photon'] = photon_mmv = comm.bcast(
self.domains.material_volumes(self.photon_model, **mat_vol_kwargs))
# Save photon MMV results to file
if comm.rank == 0:
photon_mmv.save(output_dir / 'mesh_material_volumes.npz')
if comm.rank == 0:
tally_ids = [tally.id for tally in self.photon_model.tallies]
with open(output_dir / 'tally_ids.json', 'w') as f:
json.dump(tally_ids, f)
self.results['photon_tallies'] = {}
# Get dictionary of cells in the photon model
if different_photon_model:
photon_cells = self.photon_model.geometry.get_all_cells()
for time_index in time_indices:
# Create decay photon source
if self.method == 'mesh-based':
self.photon_model.settings.source = \
self.get_decay_photon_source_mesh(time_index)
else:
sources = []
results = self.results['depletion_results']
for cell, original_mat in zip(self.domains, self.results['activation_materials']):
# Skip if the cell is not in the photon model or the
# material has changed
if different_photon_model:
if cell.id not in photon_cells or \
cell.fill.id != photon_cells[cell.id].fill.id:
continue
# Get bounding box for the cell
bounding_box = bounding_boxes[cell.id]
# Get activated material composition
activated_mat = results[time_index].get_material(str(original_mat.id))
# Create decay photon source source
space = openmc.stats.Box(*bounding_box)
energy = activated_mat.get_decay_photon_energy()
strength = energy.integral() if energy is not None else 0.0
source = openmc.IndependentSource(
space=space,
energy=energy,
particle='photon',
strength=strength,
constraints={'domains': [cell]}
)
sources.append(source)
self.photon_model.settings.source = sources
# Convert time_index (which may be negative) to a normal index
if time_index < 0:
time_index = len(self.results['depletion_results']) + time_index
# Run photon transport calculation
photon_dir = Path(output_dir) / f'time_{time_index}'
with TemporarySession(self.photon_model, cwd=photon_dir):
statepoint_path = self.photon_model.run(**run_kwargs)
# Store tally results
with openmc.StatePoint(statepoint_path) as sp:
self.results['photon_tallies'][time_index] = [
sp.tallies[tally.id] for tally in self.photon_model.tallies
]
def get_decay_photon_source_mesh(
self,
time_index: int = -1
) -> list[openmc.MeshSource]:
"""Create decay photon source for a mesh-based calculation.
This function creates N :class:`MeshSource` objects where N is the
maximum number of unique materials that appears in a single mesh
element. For each mesh element-material combination, and
IndependentSource instance is created with a spatial constraint limited
the sampled decay photons to the correct region.
When the photon transport model is different from the neutron model, the
photon MeshMaterialVolumes is used to determine whether an (element,
material) combination exists in the photon model.
Parameters
----------
time_index : int, optional
Time index for the decay photon source. Default is -1 (last time).
Returns
-------
list of openmc.MeshSource
A list of MeshSource objects, each containing IndependentSource
instances for the decay photons in the corresponding mesh element.
"""
mat_dict = self.neutron_model._get_all_materials()
# Some MeshSource objects will have empty positions; create a "null source"
# that is used for this case
null_source = openmc.IndependentSource(particle='photon', strength=0.0)
# List to hold sources for each MeshSource (length = N)
source_lists = []
# Index in the overall list of activated materials
index_mat = 0
# Get various results from previous steps
mat_vols = self.results['mesh_material_volumes']
materials = self.results['activation_materials']
results = self.results['depletion_results']
photon_mat_vols = self.results.get('mesh_material_volumes_photon')
# Total number of mesh elements
n_elements = mat_vols.num_elements
for index_elem in range(n_elements):
# Determine which materials exist in the photon model for this element
if photon_mat_vols is not None:
photon_materials = {
mat_id
for mat_id, _ in photon_mat_vols.by_element(index_elem)
if mat_id is not None
}
for j, (mat_id, _) in enumerate(mat_vols.by_element(index_elem)):
# Skip void volume
if mat_id is None:
continue
# Skip if this material doesn't exist in photon model
if photon_mat_vols is not None and mat_id not in photon_materials:
index_mat += 1
continue
# Check whether a new MeshSource object is needed
if j >= len(source_lists):
source_lists.append([null_source]*n_elements)
# Get activated material composition
original_mat = materials[index_mat]
activated_mat = results[time_index].get_material(str(original_mat.id))
# Create decay photon source source
energy = activated_mat.get_decay_photon_energy()
if energy is not None:
strength = energy.integral()
source_lists[j][index_elem] = openmc.IndependentSource(
energy=energy,
particle='photon',
strength=strength,
constraints={'domains': [mat_dict[mat_id]]}
)
# Increment index of activated material
index_mat += 1
# Return list of mesh sources
return [openmc.MeshSource(self.domains, sources) for sources in source_lists]
def load_results(self, path: PathLike):
"""Load results from a previous R2S calculation.
Parameters
----------
path : PathLike
Path to the directory containing the R2S calculation results.
"""
path = Path(path)
# Load neutron transport results
neutron_dir = path / 'neutron_transport'
if self.method == 'mesh-based':
mmv_file = neutron_dir / 'mesh_material_volumes.npz'
if mmv_file.exists():
self.results['mesh_material_volumes'] = \
openmc.MeshMaterialVolumes.from_npz(mmv_file)
fluxes_file = neutron_dir / 'fluxes.npy'
if fluxes_file.exists():
self.results['fluxes'] = list(np.load(fluxes_file, allow_pickle=True))
micros_dict = read_microxs_hdf5(neutron_dir / 'micros.h5')
self.results['micros'] = [
micros_dict[f'domain_{i}'] for i in range(len(micros_dict))
]
# Load activation results
activation_dir = path / 'activation'
activation_results = activation_dir / 'depletion_results.h5'
if activation_results.exists():
self.results['depletion_results'] = Results(activation_results)
activation_mats_file = activation_dir / 'materials.xml'
if activation_mats_file.exists():
self.results['activation_materials'] = \
openmc.Materials.from_xml(activation_mats_file)
# Load photon transport results
photon_dir = path / 'photon_transport'
# Load photon mesh material volumes if they exist (for mesh-based calculations)
if self.method == 'mesh-based':
photon_mmv_file = photon_dir / 'mesh_material_volumes.npz'
if photon_mmv_file.exists():
self.results['mesh_material_volumes_photon'] = \
openmc.MeshMaterialVolumes.from_npz(photon_mmv_file)
# Load tally IDs from JSON file
tally_ids_path = photon_dir / 'tally_ids.json'
if tally_ids_path.exists():
with tally_ids_path.open('r') as f:
tally_ids = json.load(f)
self.results['photon_tallies'] = {}
# For each photon transport calc, load the statepoint and get the
# tally results based on tally_ids
for time_dir in photon_dir.glob('time_*'):
time_index = int(time_dir.name.split('_')[1])
for sp_path in time_dir.glob('statepoint.*.h5'):
with openmc.StatePoint(sp_path) as sp:
self.results['photon_tallies'][time_index] = [
sp.tallies[tally_id] for tally_id in tally_ids
]

View file

@ -203,7 +203,7 @@ class Results(list):
# Evaluate value in each region
for i, result in enumerate(self):
times[i] = result.time[0]
concentrations[i] = result[0, mat_id, nuc]
concentrations[i] = result[mat_id, nuc]
# Unit conversions
times = _get_time_as(times, time_units)
@ -363,7 +363,7 @@ class Results(list):
# Evaluate value in each region
for i, result in enumerate(self):
times[i] = result.time[0]
rates[i] = result.rates[0].get(mat_id, nuc, rx) * result[0, mat, nuc]
rates[i] = result.rates.get(mat_id, nuc, rx) * result[mat, nuc]
return times, rates
@ -397,7 +397,7 @@ class Results(list):
# Get time/eigenvalue at each point
for i, result in enumerate(self):
times[i] = result.time[0]
eigenvalues[i] = result.k[0]
eigenvalues[i] = result.k
# Convert time units if necessary
times = _get_time_as(times, time_units)
@ -630,7 +630,7 @@ class Results(list):
for nuc in result.index_nuc:
if nuc not in available_cross_sections:
continue
atoms = result[0, mat_id, nuc]
atoms = result[mat_id, nuc]
if atoms > 0.0:
atoms_per_barn_cm = 1e-24 * atoms / mat.volume
mat.remove_nuclide(nuc) # Replace if it's there

View file

@ -16,7 +16,7 @@ from openmc.mpi import comm, MPI
from openmc.checkvalue import PathLike
from .reaction_rates import ReactionRates
VERSION_RESULTS = (1, 1)
VERSION_RESULTS = (1, 2)
__all__ = ["StepResult"]
@ -30,8 +30,8 @@ class StepResult:
Attributes
----------
k : list of (float, float)
Eigenvalue and uncertainty for each substep.
k : tuple of (float, float)
Eigenvalue and uncertainty at end of step.
time : list of float
Time at beginning, end of step, in seconds.
source_rate : float
@ -40,8 +40,8 @@ class StepResult:
Number of mats.
n_nuc : int
Number of nuclides.
rates : list of ReactionRates
The reaction rates for each substep.
rates : ReactionRates
The reaction rates at end of step.
volume : dict of str to float
Dictionary mapping mat id to volume.
index_mat : dict of str to int
@ -52,10 +52,8 @@ class StepResult:
A dictionary mapping mat ID as string to global index.
n_hdf5_mats : int
Number of materials in entire geometry.
n_stages : int
Number of stages in simulation.
data : numpy.ndarray
Atom quantity, stored by stage, mat, then by nuclide.
Atom quantity, stored by mat, then by nuclide.
proc_time : int
Average time spent depleting a material across all
materials and processes
@ -86,17 +84,17 @@ class StepResult:
Parameters
----------
pos : tuple
A three-length tuple containing a stage index, mat index and a nuc
index. All can be integers or slices. The second two can be
A two-length tuple containing a mat index and a nuc
index. Both can be integers or slices, or can be
strings corresponding to their respective dictionary.
Returns
-------
float
The atoms for stage, mat, nuc
The atoms for mat, nuc
"""
stage, mat, nuc = pos
mat, nuc = pos
if isinstance(mat, openmc.Material):
mat = str(mat.id)
if isinstance(mat, str):
@ -104,7 +102,7 @@ class StepResult:
if isinstance(nuc, str):
nuc = self.index_nuc[nuc]
return self.data[stage, mat, nuc]
return self.data[mat, nuc]
def __setitem__(self, pos, val):
"""Sets an item from results.
@ -112,21 +110,21 @@ class StepResult:
Parameters
----------
pos : tuple
A three-length tuple containing a stage index, mat index and a nuc
index. All can be integers or slices. The second two can be
A two-length tuple containing a mat index and a nuc
index. Both can be integers or slices, or can be
strings corresponding to their respective dictionary.
val : float
The value to set data to.
"""
stage, mat, nuc = pos
mat, nuc = pos
if isinstance(mat, str):
mat = self.index_mat[mat]
if isinstance(nuc, str):
nuc = self.index_nuc[nuc]
self.data[stage, mat, nuc] = val
self.data[mat, nuc] = val
@property
def n_mat(self):
@ -140,11 +138,7 @@ class StepResult:
def n_hdf5_mats(self):
return len(self.mat_to_hdf5_ind)
@property
def n_stages(self):
return self.data.shape[0]
def allocate(self, volume, nuc_list, burn_list, full_burn_list, stages):
def allocate(self, volume, nuc_list, burn_list, full_burn_list):
"""Allocate memory for depletion step data
Parameters
@ -157,8 +151,6 @@ class StepResult:
A list of all mat IDs to be burned. Used for sorting the simulation.
full_burn_list : list of str
List of all burnable material IDs
stages : int
Number of stages in simulation.
"""
self.volume = copy.deepcopy(volume)
@ -167,7 +159,7 @@ class StepResult:
self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)}
# Create storage array
self.data = np.zeros((stages, self.n_mat, self.n_nuc))
self.data = np.zeros((self.n_mat, self.n_nuc))
def distribute(self, local_materials, ranges):
"""Create a new object containing data for distributed materials
@ -196,8 +188,8 @@ class StepResult:
for attr in direct_attrs:
setattr(new, attr, getattr(self, attr))
# Get applicable slice of data
new.data = self.data[:, ranges]
new.rates = [r[ranges] for r in self.rates]
new.data = self.data[ranges]
new.rates = self.rates[ranges]
return new
def get_material(self, mat_id):
@ -232,7 +224,7 @@ class StepResult:
f'values are {list(self.volume.keys())}'
) from e
for nuc, _ in sorted(self.index_nuc.items(), key=lambda x: x[1]):
atoms = self[0, mat_id, nuc]
atoms = self[mat_id, nuc]
if atoms <= 0.0:
continue
atom_per_bcm = atoms / vol * 1e-24
@ -240,7 +232,7 @@ class StepResult:
material.volume = vol
return material
def export_to_hdf5(self, filename, step):
def export_to_hdf5(self, filename, step, write_rates: bool = False):
"""Export results to an HDF5 file
Parameters
@ -249,6 +241,8 @@ class StepResult:
The filename to write to
step : int
What step is this?
write_rates : bool, optional
Whether to include reaction rate datasets in the results file.
"""
# Write new file if first time step, else add to existing file
@ -259,7 +253,8 @@ class StepResult:
kwargs['driver'] = 'mpio'
kwargs['comm'] = comm
with h5py.File(filename, **kwargs) as handle:
self._to_hdf5(handle, step, parallel=True)
self._to_hdf5(handle, step, parallel=True,
write_rates=write_rates)
else:
# Gather results at root process
all_results = comm.gather(self)
@ -268,15 +263,18 @@ class StepResult:
if comm.rank == 0:
with h5py.File(filename, **kwargs) as handle:
for res in all_results:
res._to_hdf5(handle, step, parallel=False)
res._to_hdf5(handle, step, parallel=False,
write_rates=write_rates)
def _write_hdf5_metadata(self, handle):
def _write_hdf5_metadata(self, handle, write_rates):
"""Writes result metadata in HDF5 file
Parameters
----------
handle : h5py.File or h5py.Group
An hdf5 file or group type to store this in.
write_rates : bool
Whether reaction rate datasets are being written.
"""
# Create and save the 5 dictionaries:
@ -284,8 +282,8 @@ class StepResult:
# self.index_mat -> self.volume (TODO: support for changing volumes)
# self.index_nuc
# reactions
# self.rates[0].index_nuc (can be different from above, above is superset)
# self.rates[0].index_rx
# self.rates.index_nuc (can be different from above, above is superset)
# self.rates.index_rx
# these are shared by every step of the simulation, and should be deduplicated.
# Store concentration mat and nuclide dictionaries (along with volumes)
@ -295,13 +293,19 @@ class StepResult:
mat_list = sorted(self.mat_to_hdf5_ind, key=int)
nuc_list = sorted(self.index_nuc)
rxn_list = sorted(self.rates[0].index_rx)
include_rates = (
write_rates
and self.rates is not None
and bool(self.rates.index_nuc)
and bool(self.rates.index_rx)
)
rxn_list = sorted(self.rates.index_rx) if include_rates else []
n_mats = self.n_hdf5_mats
n_nuc_number = len(nuc_list)
n_nuc_rxn = len(self.rates[0].index_nuc)
n_nuc_rxn = len(self.rates.index_nuc) if include_rates else 0
n_rxn = len(rxn_list)
n_stages = self.n_stages
mat_group = handle.create_group("materials")
@ -315,41 +319,44 @@ class StepResult:
for nuc in nuc_list:
nuc_single_group = nuc_group.create_group(nuc)
nuc_single_group.attrs["atom number index"] = self.index_nuc[nuc]
if nuc in self.rates[0].index_nuc:
nuc_single_group.attrs["reaction rate index"] = self.rates[0].index_nuc[nuc]
if include_rates and nuc in self.rates.index_nuc:
nuc_single_group.attrs["reaction rate index"] = (
self.rates.index_nuc[nuc])
rxn_group = handle.create_group("reactions")
if include_rates:
rxn_group = handle.create_group("reactions")
for rxn in rxn_list:
rxn_single_group = rxn_group.create_group(rxn)
rxn_single_group.attrs["index"] = self.rates[0].index_rx[rxn]
for rxn in rxn_list:
rxn_single_group = rxn_group.create_group(rxn)
rxn_single_group.attrs["index"] = (
self.rates.index_rx[rxn])
# Construct array storage
handle.create_dataset("number", (1, n_stages, n_mats, n_nuc_number),
maxshape=(None, n_stages, n_mats, n_nuc_number),
handle.create_dataset("number", (1, n_mats, n_nuc_number),
maxshape=(None, n_mats, n_nuc_number),
chunks=True,
dtype='float64')
if n_nuc_rxn > 0 and n_rxn > 0:
handle.create_dataset("reaction rates", (1, n_stages, n_mats, n_nuc_rxn, n_rxn),
maxshape=(None, n_stages, n_mats, n_nuc_rxn, n_rxn),
chunks=True,
dtype='float64')
if include_rates and n_nuc_rxn > 0 and n_rxn > 0:
handle.create_dataset(
"reaction rates", (1, n_mats, n_nuc_rxn, n_rxn),
maxshape=(None, n_mats, n_nuc_rxn, n_rxn),
chunks=True, dtype='float64')
handle.create_dataset("eigenvalues", (1, n_stages, 2),
maxshape=(None, n_stages, 2), dtype='float64')
handle.create_dataset("eigenvalues", (1, 2),
maxshape=(None, 2), dtype='float64')
handle.create_dataset("time", (1, 2), maxshape=(None, 2), dtype='float64')
handle.create_dataset("source_rate", (1, n_stages), maxshape=(None, n_stages),
handle.create_dataset("source_rate", (1,), maxshape=(None,),
dtype='float64')
handle.create_dataset(
"depletion time", (1,), maxshape=(None,),
dtype="float64")
def _to_hdf5(self, handle, index, parallel=False):
def _to_hdf5(self, handle, index, parallel=False, write_rates: bool = False):
"""Converts results object into an hdf5 object.
Parameters
@ -360,12 +367,14 @@ class StepResult:
What step is this?
parallel : bool
Being called with parallel HDF5?
write_rates : bool, optional
Whether reaction rate datasets are being written.
"""
if "/number" not in handle:
if parallel:
comm.barrier()
self._write_hdf5_metadata(handle)
self._write_hdf5_metadata(handle, write_rates)
if parallel:
comm.barrier()
@ -417,18 +426,14 @@ class StepResult:
return
# Add data
# Note, for the last step, self.n_stages = 1, even if n_stages != 1.
n_stages = self.n_stages
inds = [self.mat_to_hdf5_ind[mat] for mat in self.index_mat]
low = min(inds)
high = max(inds)
for i in range(n_stages):
number_dset[index, i, low:high+1] = self.data[i]
if has_reactions:
rxn_dset[index, i, low:high+1] = self.rates[i]
if comm.rank == 0:
eigenvalues_dset[index, i] = self.k[i]
number_dset[index, low:high+1] = self.data
if has_reactions:
rxn_dset[index, low:high+1] = self.rates
if comm.rank == 0:
eigenvalues_dset[index] = self.k
time_dset[index] = self.time
source_rate_dset[index] = self.source_rate
if self.proc_time is not None:
@ -459,10 +464,24 @@ class StepResult:
# Older versions used "power" instead of "source_rate"
source_rate_dset = handle["/power"]
results.data = number_dset[step, :, :, :]
results.k = eigenvalues_dset[step, :]
# Check if this is an old format file (with stages dimension) or new format
# Old format: number has shape (n_steps, n_stages, n_mats, n_nucs)
# New format: number has shape (n_steps, n_mats, n_nucs)
has_stages = len(number_dset.shape) == 4
if has_stages:
# Old format - extract data from first stage (index 0)
results.data = number_dset[step, 0, :, :]
results.k = eigenvalues_dset[step, 0, :]
# source_rate had shape (n_steps, n_stages) in old format
results.source_rate = source_rate_dset[step, 0]
else:
# New format - no stages dimension
results.data = number_dset[step, :, :]
results.k = eigenvalues_dset[step, :]
results.source_rate = source_rate_dset[step]
results.time = time_dset[step, :]
results.source_rate = source_rate_dset[step, 0]
if "depletion time" in handle:
proc_time_dset = handle["/depletion time"]
@ -493,33 +512,45 @@ class StepResult:
if "reaction rate index" in nuc_handle.attrs:
rxn_nuc_to_ind[nuc] = nuc_handle.attrs["reaction rate index"]
for rxn, rxn_handle in handle["/reactions"].items():
rxn_to_ind[rxn] = rxn_handle.attrs["index"]
if "reactions" in handle:
for rxn, rxn_handle in handle["/reactions"].items():
rxn_to_ind[rxn] = rxn_handle.attrs["index"]
results.rates = []
# Reconstruct reactions
for i in range(results.n_stages):
rate = ReactionRates(results.index_mat, rxn_nuc_to_ind, rxn_to_ind, True)
if "reaction rates" in handle:
rate[:] = handle["/reaction rates"][step, i, :, :, :]
results.rates.append(rate)
# Reconstruct reaction rates
rate = ReactionRates(results.index_mat, rxn_nuc_to_ind, rxn_to_ind, True)
if "reaction rates" in handle:
if has_stages:
# Old format: (n_steps, n_stages, n_mats, n_nucs, n_rxns)
rate[:] = handle["/reaction rates"][step, 0, :, :, :]
else:
# New format: (n_steps, n_mats, n_nucs, n_rxns)
rate[:] = handle["/reaction rates"][step, :, :, :]
results.rates = rate
return results
@staticmethod
def save(op, x, op_results, t, source_rate, step_ind, proc_time=None,
path: PathLike = "depletion_results.h5"):
def save(
op,
x,
op_results,
t,
source_rate,
step_ind,
proc_time=None,
write_rates: bool = False,
path: PathLike = "depletion_results.h5"
):
"""Creates and writes depletion results to disk
Parameters
----------
op : openmc.deplete.abc.TransportOperator
The operator used to generate these results.
x : list of list of numpy.array
The prior x vectors. Indexed [i][cell] using the above equation.
op_results : list of openmc.deplete.OperatorResult
Results of applying transport operator
x : numpy.array
End-of-step concentrations for each material
op_results : openmc.deplete.OperatorResult
Result of applying transport operator at end of step
t : list of float
Time indices.
source_rate : float
@ -530,7 +561,8 @@ class StepResult:
Total process time spent depleting materials. This may
be process-dependent and will be reduced across MPI
processes.
write_rates : bool, optional
Whether reaction rates should be written to the results file.
path : PathLike
Path to file to write. Defaults to 'depletion_results.h5'.
@ -539,26 +571,20 @@ class StepResult:
# Get indexing terms
vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info()
stages = len(x)
# Create results
results = StepResult()
results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, stages)
results.allocate(vol_dict, nuc_list, burn_list, full_burn_list)
n_mat = len(burn_list)
for i in range(stages):
for mat_i in range(n_mat):
results[i, mat_i, :] = x[i][mat_i]
for mat_i in range(n_mat):
results[mat_i, :] = x[mat_i]
ks = []
for r in op_results:
if isinstance(r.k, type(None)):
ks += [(None, None)]
else:
ks += [(r.k.nominal_value, r.k.std_dev)]
results.k = ks
results.rates = [r.rates for r in op_results]
if isinstance(op_results.k, type(None)):
results.k = (None, None)
else:
results.k = (op_results.k.nominal_value, op_results.k.std_dev)
results.rates = op_results.rates
results.time = t
results.source_rate = source_rate
results.proc_time = proc_time
@ -567,7 +593,7 @@ class StepResult:
if not Path(path).is_file():
Path(path).parent.mkdir(parents=True, exist_ok=True)
results.export_to_hdf5(path, step_ind)
results.export_to_hdf5(path, step_ind, write_rates)
def transfer_volumes(self, model):
"""Transfers volumes from depletion results to geometry

View file

@ -49,9 +49,10 @@ class ExternalRates:
self.local_mats = operator.local_mats
self.number_of_timesteps = number_of_timesteps
# initialize transfer rates container dict
#initialize transfer rates container dict
self.external_rates = {mat: defaultdict(list) for mat in self.burnable_mats}
self.external_timesteps = []
self.redox = {}
def _get_material_id(self, val):
"""Helper method for getting material id from Material obj or name.
@ -300,6 +301,46 @@ class TransferRates(ExternalRates):
self.external_timesteps = np.unique(np.concatenate(
[self.external_timesteps, timesteps]))
def set_redox(self, material, buffer, oxidation_states, timesteps=None):
"""Add redox control to depletable material.
Parameters
----------
material : openmc.Material or str or int
Depletable material
buffer : dict
Dictionary of buffer nuclides used to maintain redox balance.
Keys are nuclide names (strings) and values are their respective
fractions (float) that collectively sum to 1.
oxidation_states : dict
User-defined oxidation states for elements.
Keys are element symbols (e.g., 'H', 'He'), and values are their
corresponding oxidation states as integers (e.g., +1, 0).
timesteps : list of int, optional
List of timestep indices where to set external source rates.
Defaults to None, which means the external source rate is set for
all timesteps.
"""
material_id = self._get_material_id(material)
if timesteps is not None:
for timestep in timesteps:
check_value('timestep', timestep, range(self.number_of_timesteps))
timesteps = np.array(timesteps)
else:
timesteps = np.arange(self.number_of_timesteps)
#Check nuclides in buffer exist
for nuc in buffer:
if nuc not in self.chain_nuclides:
raise ValueError(f'{nuc} is not a valid nuclide.')
# Checks element in oxidation states exist
for elm in oxidation_states:
if elm not in ELEMENT_SYMBOL.values():
raise ValueError(f'{elm} is not a valid element.')
self.redox[material_id] = (buffer, oxidation_states)
self.external_timesteps = np.unique(np.concatenate(
[self.external_timesteps, timesteps]))
class ExternalSourceRates(ExternalRates):
"""Class for defining external source rates.

View file

@ -13,6 +13,7 @@ from numpy.ctypeslib import as_array
from . import _dll
from .error import _error_handler
from ..mpi import comm
from openmc.checkvalue import PathLike
import openmc.lib
import openmc
@ -632,6 +633,9 @@ class TemporarySession:
model : openmc.Model, optional
OpenMC model to use for the session. If None, a minimal working model is
created.
cwd : PathLike, optional
Working directory in which to run OpenMC. If None, a temporary directory
is created and deleted automatically.
**init_kwargs
Keyword arguments to pass to :func:`openmc.lib.init`.
@ -639,10 +643,13 @@ class TemporarySession:
----------
model : openmc.Model
The OpenMC model used for the session.
comm : mpi4py.MPI.Intracomm
The MPI intracommunicator used for the session.
"""
def __init__(self, model=None, **init_kwargs):
self.init_kwargs = init_kwargs
def __init__(self, model=None, cwd=None, **init_kwargs):
self.init_kwargs = dict(init_kwargs)
self.cwd = cwd
if model is None:
surf = openmc.Sphere(boundary_type="vacuum")
cell = openmc.Cell(region=-surf)
@ -652,6 +659,10 @@ class TemporarySession:
particles=1, batches=1, output={'summary': False})
self.model = model
# Determine MPI intercommunicator
self.init_kwargs.setdefault('intracomm', comm)
self.comm = self.init_kwargs['intracomm']
def __enter__(self):
"""Initialize the OpenMC library in a temporary directory."""
# If already initialized, the context manager is a no-op
@ -662,14 +673,24 @@ class TemporarySession:
# Store original working directory
self.orig_dir = Path.cwd()
# Set up temporary directory
self.tmp_dir = TemporaryDirectory()
working_dir = Path(self.tmp_dir.name)
working_dir.mkdir(parents=True, exist_ok=True)
os.chdir(working_dir)
if self.cwd is None:
# Set up temporary directory on rank 0
if self.comm.rank == 0:
self._tmp_dir = TemporaryDirectory()
self.cwd = self._tmp_dir.name
# Export model and initialize OpenMC
self.model.export_to_model_xml()
# Broadcast the path so that all ranks use the same directory
self.cwd = self.comm.bcast(self.cwd)
# Create and change to specified directory
self.cwd = Path(self.cwd)
self.cwd.mkdir(parents=True, exist_ok=True)
os.chdir(self.cwd)
# Export model on first rank and initialize OpenMC
if self.comm.rank == 0:
self.model.export_to_model_xml()
self.comm.barrier()
openmc.lib.init(**self.init_kwargs)
return self
@ -683,7 +704,11 @@ class TemporarySession:
finalize()
finally:
os.chdir(self.orig_dir)
self.tmp_dir.cleanup()
# Make sure all ranks have finalized before deleting temporary dir
self.comm.barrier()
if hasattr(self, '_tmp_dir'):
self._tmp_dir.cleanup()
class _DLLGlobal:

View file

@ -290,11 +290,13 @@ class Material(IDManagerMixin):
return self.get_decay_photon_energy(0.0)
def get_decay_photon_energy(
self,
clip_tolerance: float = 1e-6,
units: str = 'Bq',
volume: float | None = None
) -> Univariate | None:
self,
clip_tolerance: float = 1e-6,
units: str = 'Bq',
volume: float | None = None,
exclude_nuclides: list[str] | None = None,
include_nuclides: list[str] | None = None
) -> Univariate | None:
r"""Return energy distribution of decay photons from unstable nuclides.
.. versionadded:: 0.14.0
@ -302,22 +304,31 @@ class Material(IDManagerMixin):
Parameters
----------
clip_tolerance : float
Maximum fraction of :math:`\sum_i x_i p_i` for discrete
distributions that will be discarded.
Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions
that will be discarded.
units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}
Specifies the units on the integral of the distribution.
volume : float, optional
Volume of the material. If not passed, defaults to using the
:attr:`Material.volume` attribute.
exclude_nuclides : list of str, optional
Nuclides to exclude from the photon source calculation.
include_nuclides : list of str, optional
Nuclides to include in the photon source calculation. If specified,
only these nuclides are used.
Returns
-------
Univariate or None
Decay photon energy distribution. The integral of this distribution
is the total intensity of the photon source in the requested units.
Decay photon energy distribution. The integral of this distribution is
the total intensity of the photon source in the requested units.
"""
cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'})
if exclude_nuclides is not None and include_nuclides is not None:
raise ValueError("Cannot specify both exclude_nuclides and include_nuclides")
if units == 'Bq':
multiplier = volume if volume is not None else self.volume
if multiplier is None:
@ -332,6 +343,11 @@ class Material(IDManagerMixin):
dists = []
probs = []
for nuc, atoms_per_bcm in self.get_nuclide_atom_densities().items():
if exclude_nuclides is not None and nuc in exclude_nuclides:
continue
if include_nuclides is not None and nuc not in include_nuclides:
continue
source_per_atom = openmc.data.decay_photon_energy(nuc)
if source_per_atom is not None and atoms_per_bcm > 0.0:
dists.append(source_per_atom)

View file

@ -5,6 +5,7 @@ from collections.abc import Iterable, Sequence, Mapping
from functools import wraps
from math import pi, sqrt, atan2
from numbers import Integral, Real
from pathlib import Path
from typing import Protocol
import h5py
@ -325,20 +326,10 @@ class MeshBase(IDManagerMixin, ABC):
vols = material_volumes
mat_volume_by_element = [vols.by_element(i) for i in range(vols.num_elements)]
# Get dictionary of all materials
materials = model._get_all_materials()
# Create homogenized material for each element
materials = model.geometry.get_all_materials()
# Account for materials in DAGMC universes
# TODO: This should really get incorporated in lower-level calls to
# get_all_materials, but right now it requires information from the
# Model object
for cell in model.geometry.get_all_cells().values():
if isinstance(cell.fill, openmc.DAGMCUniverse):
names = cell.fill.material_names
materials.update({
mat.id: mat for mat in model.materials if mat.name in names
})
homogenized_materials = []
for mat_volume_list in mat_volume_by_element:
material_ids, volumes = [list(x) for x in zip(*mat_volume_list)]
@ -410,7 +401,7 @@ class MeshBase(IDManagerMixin, ABC):
# In order to get mesh into model, we temporarily replace the
# tallies with a single mesh tally using the current mesh
original_tallies = model.tallies
original_tallies = list(model.tallies)
new_tally = openmc.Tally()
new_tally.filters = [openmc.MeshFilter(self)]
new_tally.scores = ['flux']
@ -2453,6 +2444,7 @@ class UnstructuredMesh(MeshBase):
_UNSUPPORTED_ELEM = -1
_LINEAR_TET = 0
_LINEAR_HEX = 1
_VTK_TETRA = 10
def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None,
name: str = '', length_multiplier: float = 1.0,
@ -2662,7 +2654,8 @@ class UnstructuredMesh(MeshBase):
warnings.warn(
"The 'UnstructuredMesh.write_vtk_mesh' method has been renamed "
"to 'write_data_to_vtk' and will be removed in a future version "
" of OpenMC.", FutureWarning
" of OpenMC.",
FutureWarning,
)
self.write_data_to_vtk(**kwargs)
@ -2680,9 +2673,10 @@ class UnstructuredMesh(MeshBase):
Parameters
----------
filename : str or pathlib.Path
Name of the VTK file to write. If the filename ends in '.vtu' then a
binary VTU format file will be written, if the filename ends in
'.vtk' then a legacy VTK file will be written.
Name of the VTK file to write. If the filename ends in '.vtkhdf'
then a VTKHDF format file will be written. If the filename ends in
'.vtu' then a binary VTU format file will be written. If the
filename ends in '.vtk' then a legacy VTK file will be written.
datasets : dict
Dictionary whose keys are the data labels and values are numpy
appropriately sized arrays of the data
@ -2690,6 +2684,35 @@ class UnstructuredMesh(MeshBase):
Whether or not to normalize the data by the volume of the mesh
elements
"""
if Path(filename).suffix == ".vtkhdf":
self._write_data_to_vtk_hdf5_format(
filename=filename,
datasets=datasets,
volume_normalization=volume_normalization,
)
elif Path(filename).suffix == ".vtk" or Path(filename).suffix == ".vtu":
self._write_data_to_vtk_ascii_format(
filename=filename,
datasets=datasets,
volume_normalization=volume_normalization,
)
else:
raise ValueError(
"Unsupported file extension, The filename must end with "
"'.vtkhdf', '.vtu' or '.vtk'"
)
def _write_data_to_vtk_ascii_format(
self,
filename: PathLike | None = None,
datasets: dict | None = None,
volume_normalization: bool = True,
):
from vtkmodules.util import numpy_support
from vtkmodules import vtkCommonCore
from vtkmodules import vtkCommonDataModel
@ -2697,9 +2720,7 @@ class UnstructuredMesh(MeshBase):
from vtkmodules import vtkIOXML
if self.connectivity is None or self.vertices is None:
raise RuntimeError(
"This mesh has not been loaded from a statepoint file."
)
raise RuntimeError("This mesh has not been loaded from a statepoint file.")
if filename is None:
filename = f"mesh_{self.id}.vtk"
@ -2781,29 +2802,128 @@ class UnstructuredMesh(MeshBase):
writer.Write()
def _write_data_to_vtk_hdf5_format(
self,
filename: PathLike | None = None,
datasets: dict | None = None,
volume_normalization: bool = True,
):
def append_dataset(dset, array):
"""Convenience function to append data to an HDF5 dataset"""
origLen = dset.shape[0]
dset.resize(origLen + array.shape[0], axis=0)
dset[origLen:] = array
if self.library != "moab":
raise NotImplementedError("VTKHDF output is only supported for MOAB meshes")
# the self.connectivity contains arrays of length 8 to support hex
# elements as well, in the case of tetrahedra mesh elements, the
# last 4 values are -1 and are removed
trimmed_connectivity = []
for cell in self.connectivity:
# Find the index of the first -1 value, if any
first_negative_index = np.where(cell == -1)[0]
if first_negative_index.size > 0:
# Slice the array up to the first -1 value
trimmed_connectivity.append(cell[: first_negative_index[0]])
else:
# No -1 values, append the whole cell
trimmed_connectivity.append(cell)
trimmed_connectivity = np.array(trimmed_connectivity, dtype="int32").flatten()
# MOAB meshes supports tet elements only so we know it has 4 points per cell
points_per_cell = 4
# offsets are the indices of the first point of each cell in the array of points
offsets = np.arange(0, self.n_elements * points_per_cell + 1, points_per_cell)
for name, data in datasets.items():
if data.shape != self.dimension:
raise ValueError(
f'Cannot apply dataset "{name}" with '
f"shape {data.shape} to mesh {self.id} "
f"with dimensions {self.dimension}"
)
with h5py.File(filename, "w") as f:
root = f.create_group("VTKHDF")
vtk_file_format_version = (2, 1)
root.attrs["Version"] = vtk_file_format_version
ascii_type = "UnstructuredGrid".encode("ascii")
root.attrs.create(
"Type",
ascii_type,
dtype=h5py.string_dtype("ascii", len(ascii_type)),
)
# create hdf5 file structure
root.create_dataset("NumberOfPoints", (0,), maxshape=(None,), dtype="i8")
root.create_dataset("Types", (0,), maxshape=(None,), dtype="uint8")
root.create_dataset("Points", (0, 3), maxshape=(None, 3), dtype="f")
root.create_dataset(
"NumberOfConnectivityIds", (0,), maxshape=(None,), dtype="i8"
)
root.create_dataset("NumberOfCells", (0,), maxshape=(None,), dtype="i8")
root.create_dataset("Offsets", (0,), maxshape=(None,), dtype="i8")
root.create_dataset("Connectivity", (0,), maxshape=(None,), dtype="i8")
append_dataset(root["NumberOfPoints"], np.array([len(self.vertices)]))
append_dataset(root["Points"], self.vertices)
append_dataset(
root["NumberOfConnectivityIds"],
np.array([len(trimmed_connectivity)]),
)
append_dataset(root["Connectivity"], trimmed_connectivity)
append_dataset(root["NumberOfCells"], np.array([self.n_elements]))
append_dataset(root["Offsets"], offsets)
append_dataset(
root["Types"], np.full(self.n_elements, self._VTK_TETRA, dtype="uint8")
)
cell_data_group = root.create_group("CellData")
for name, data in datasets.items():
cell_data_group.create_dataset(
name, (0,), maxshape=(None,), dtype="float64", chunks=True
)
if volume_normalization:
data /= self.volumes
append_dataset(cell_data_group[name], data)
@classmethod
def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str):
filename = group['filename'][()].decode()
library = group['library'][()].decode()
if 'options' in group.attrs:
filename = group["filename"][()].decode()
library = group["library"][()].decode()
if "options" in group.attrs:
options = group.attrs['options'].decode()
else:
options = None
mesh = cls(filename=filename, library=library, mesh_id=mesh_id, name=name, options=options)
mesh = cls(
filename=filename,
library=library,
mesh_id=mesh_id,
name=name,
options=options,
)
mesh._has_statepoint_data = True
vol_data = group['volumes'][()]
vol_data = group["volumes"][()]
mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],))
mesh.n_elements = mesh.volumes.size
vertices = group['vertices'][()]
vertices = group["vertices"][()]
mesh._vertices = vertices.reshape((-1, 3))
connectivity = group['connectivity'][()]
connectivity = group["connectivity"][()]
mesh._connectivity = connectivity.reshape((-1, 8))
mesh._element_types = group['element_types'][()]
mesh._element_types = group["element_types"][()]
if 'length_multiplier' in group:
mesh.length_multiplier = group['length_multiplier'][()]
if "length_multiplier" in group:
mesh.length_multiplier = group["length_multiplier"][()]
return mesh
@ -2822,7 +2942,7 @@ class UnstructuredMesh(MeshBase):
element.set("library", self._library)
if self.options is not None:
element.set('options', self.options)
element.set("options", self.options)
subelement = ET.SubElement(element, "filename")
subelement.text = str(self.filename)

View file

@ -13,8 +13,8 @@ GROUP_STRUCTURES = {}
- "XMAS-172_" designed for LWR analysis ([SAR1990]_, [SAN2004]_)
- "SHEM-361_" designed for LWR analysis to eliminate self-shielding calculations
of thermal resonances ([HFA2005]_, [SAN2007]_, [HEB2008]_)
- "SCALE-X" (where X is 44 which is designed for criticality analysis
and 252 is designed for thermal reactors) for the SCALE code suite
- "SCALE-X" (where X is 44 which is designed for criticality analysis, 252 is designed
for thermal reactors and 999 for multipurpose activation) for the SCALE code suite
([ZAL1999]_ and [REARDEN2013]_)
- "MPACT-X" (where X is 51 (PWR), 60 (BWR), 69 (Magnox)) from the MPACT_ reactor
physics code ([KIM2019]_ and [KIM2020]_)
@ -29,6 +29,7 @@ GROUP_STRUCTURES = {}
.. _SCALE44: https://www-nds.iaea.org/publications/indc/indc-czr-0001.pdf
.. _ECCO-33: https://serpent.vtt.fi/mediawiki/index.php/ECCO_33-group_structure
.. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf
.. _SCALE999: https://info.ornl.gov/sites/publications/Files/Pub67728.pdf, https://www.nrc.gov/docs/ML1218/ML12184A002.pdf
.. _MPACT: https://vera.ornl.gov/mpact/
.. _XMAS-172: https://www-nds.iaea.org/wimsd/energy.htm
.. _SHEM-361: http://merlin.polymtl.ca/downloads/FP214.pdf
@ -593,7 +594,208 @@ GROUP_STRUCTURES['CCFE-709'] = np.array([
2.4000e8, 2.8000e8, 3.2000e8, 3.6000e8, 4.0000e8,
4.4000e8, 4.8000e8, 5.2000e8, 5.6000e8, 6.0000e8,
6.4000e8, 6.8000e8, 7.2000e8, 7.6000e8, 8.0000e8,
8.4000e8, 8.8000e8, 9.2000e8, 9.6000e8, 1.0000e9,])
8.4000e8, 8.8000e8, 9.2000e8, 9.6000e8, 1.0000e9])
GROUP_STRUCTURES['SCALE-999'] = np.array([
1.000e-5, 1.000e-4, 5.000e-4, 7.500e-4, 1.000e-3,
1.200e-3, 1.500e-3, 2.000e-3, 2.500e-3, 3.000e-3,
4.000e-3, 5.000e-3, 7.500e-3, 1.000e-2, 1.450e-2,
1.850e-2, 2.100e-2, 2.530e-2, 3.000e-2, 4.000e-2,
5.000e-2, 6.000e-2, 7.000e-2, 8.000e-2, 9.000e-2,
1.000e-1, 1.250e-1, 1.500e-1, 1.750e-1, 1.840e-1,
2.000e-1, 2.250e-1, 2.500e-1, 2.750e-1, 3.000e-1,
3.250e-1, 3.500e-1, 3.668e-1, 3.750e-1, 4.000e-1,
4.140e-1, 4.500e-1, 5.000e-1, 5.316e-1, 5.500e-1,
6.000e-1, 6.250e-1, 6.500e-1, 6.826e-1, 7.000e-1,
7.500e-1, 8.000e-1, 8.500e-1, 8.764e-1, 9.000e-1,
9.250e-1, 9.500e-1, 9.750e-1, 1.000e+0, 1.010e+0,
1.020e+0, 1.030e+0, 1.040e+0, 1.050e+0, 1.060e+0,
1.070e+0, 1.080e+0, 1.090e+0, 1.100e+0, 1.110e+0,
1.120e+0, 1.130e+0, 1.140e+0, 1.150e+0, 1.175e+0,
1.200e+0, 1.225e+0, 1.250e+0, 1.300e+0, 1.350e+0,
1.400e+0, 1.450e+0, 1.500e+0, 1.545e+0, 1.590e+0,
1.635e+0, 1.680e+0, 1.725e+0, 1.770e+0, 1.815e+0,
1.860e+0, 1.900e+0, 1.940e+0, 1.970e+0, 2.000e+0,
2.060e+0, 2.120e+0, 2.165e+0, 2.210e+0, 2.255e+0,
2.300e+0, 2.340e+0, 2.380e+0, 2.425e+0, 2.470e+0,
2.520e+0, 2.570e+0, 2.620e+0, 2.670e+0, 2.720e+0,
2.770e+0, 2.820e+0, 2.870e+0, 2.920e+0, 2.970e+0,
3.000e+0, 3.100e+0, 3.200e+0, 3.300e+0, 3.500e+0,
3.620e+0, 3.730e+0, 3.830e+0, 3.928e+0, 4.000e+0,
4.100e+0, 4.300e+0, 4.500e+0, 4.750e+0, 4.875e+0,
5.000e+0, 5.044e+0, 5.250e+0, 5.400e+0, 5.550e+0,
5.700e+0, 5.850e+0, 6.000e+0, 6.250e+0, 6.375e+0,
6.500e+0, 6.625e+0, 6.750e+0, 6.875e+0, 7.000e+0,
7.075e+0, 7.150e+0, 7.625e+0, 8.100e+0, 8.208e+0,
8.315e+0, 8.708e+0, 9.100e+0, 9.550e+0, 1.000e+1,
1.034e+1, 1.068e+1, 1.109e+1, 1.150e+1, 1.170e+1,
1.190e+1, 1.240e+1, 1.290e+1, 1.333e+1, 1.375e+1,
1.408e+1, 1.440e+1, 1.475e+1, 1.510e+1, 1.555e+1,
1.600e+1, 1.650e+1, 1.700e+1, 1.730e+1, 1.760e+1,
1.805e+1, 1.850e+1, 1.875e+1, 1.900e+1, 1.940e+1,
2.000e+1, 2.050e+1, 2.100e+1, 2.175e+1, 2.250e+1,
2.375e+1, 2.500e+1, 2.625e+1, 2.750e+1, 2.826e+1,
2.902e+1, 2.951e+1, 3.000e+1, 3.063e+1, 3.125e+1,
3.150e+1, 3.175e+1, 3.250e+1, 3.325e+1, 3.350e+1,
3.375e+1, 3.418e+1, 3.460e+1, 3.500e+1, 3.550e+1,
3.600e+1, 3.700e+1, 3.713e+1, 3.727e+1, 3.763e+1,
3.800e+1, 3.855e+1, 3.910e+1, 3.935e+1, 3.960e+1,
4.030e+1, 4.100e+1, 4.170e+1, 4.240e+1, 4.320e+1,
4.400e+1, 4.460e+1, 4.520e+1, 4.610e+1, 4.700e+1,
4.743e+1, 4.785e+1, 4.808e+1, 4.830e+1, 4.875e+1,
4.920e+1, 4.990e+1, 5.060e+1, 5.130e+1, 5.200e+1,
5.270e+1, 5.340e+1, 5.620e+1, 5.800e+1, 6.000e+1,
6.100e+1, 6.122e+1, 6.144e+1, 6.300e+1, 6.500e+1,
6.625e+1, 6.750e+1, 6.975e+1, 7.200e+1, 7.400e+1,
7.600e+1, 7.745e+1, 7.889e+1, 7.945e+1, 8.000e+1,
8.170e+1, 8.200e+1, 8.400e+1, 8.600e+1, 8.800e+1,
9.000e+1, 9.250e+1, 9.500e+1, 9.700e+1, 1.000e+2,
1.012e+2, 1.038e+2, 1.050e+2, 1.080e+2, 1.105e+2,
1.130e+2, 1.160e+2, 1.175e+2, 1.190e+2, 1.205e+2,
1.220e+2, 1.240e+2, 1.260e+2, 1.280e+2, 1.301e+2,
1.325e+2, 1.350e+2, 1.375e+2, 1.400e+2, 1.430e+2,
1.450e+2, 1.475e+2, 1.500e+2, 1.525e+2, 1.550e+2,
1.575e+2, 1.600e+2, 1.625e+2, 1.650e+2, 1.670e+2,
1.700e+2, 1.716e+2, 1.739e+2, 1.763e+2, 1.786e+2,
1.800e+2, 1.877e+2, 1.885e+2, 1.915e+2, 1.930e+2,
1.962e+2, 1.999e+2, 2.020e+2, 2.074e+2, 2.088e+2,
2.095e+2, 2.122e+2, 2.145e+2, 2.175e+2, 2.200e+2,
2.237e+2, 2.269e+2, 2.301e+2, 2.333e+2, 2.367e+2,
2.400e+2, 2.442e+2, 2.484e+2, 2.527e+2, 2.571e+2,
2.615e+2, 2.661e+2, 2.707e+2, 2.754e+2, 2.801e+2,
2.850e+2, 2.899e+2, 2.948e+2, 2.999e+2, 3.050e+2,
3.107e+2, 3.165e+2, 3.224e+2, 3.284e+2, 3.345e+2,
3.408e+2, 3.471e+2, 3.536e+2, 3.591e+2, 3.648e+2,
3.705e+2, 3.764e+2, 3.823e+2, 3.883e+2, 3.944e+2,
4.007e+2, 4.070e+2, 4.134e+2, 4.199e+2, 4.265e+2,
4.332e+2, 4.400e+2, 4.470e+2, 4.540e+2, 4.595e+2,
4.650e+2, 4.706e+2, 4.763e+2, 4.821e+2, 4.879e+2,
4.937e+2, 4.997e+2, 5.057e+2, 5.118e+2, 5.180e+2,
5.243e+2, 5.306e+2, 5.370e+2, 5.435e+2, 5.500e+2,
5.581e+2, 5.662e+2, 5.745e+2, 5.830e+2, 5.932e+2,
6.036e+2, 6.142e+2, 6.250e+2, 6.359e+2, 6.471e+2,
6.585e+2, 6.700e+2, 6.765e+2, 6.830e+2, 6.909e+2,
6.988e+2, 7.069e+2, 7.150e+2, 7.232e+2, 7.316e+2,
7.400e+2, 7.485e+2, 7.598e+2, 7.712e+2, 7.827e+2,
7.945e+2, 8.064e+2, 8.185e+2, 8.308e+2, 8.433e+2,
8.559e+2, 8.688e+2, 8.818e+2, 8.950e+2, 9.085e+2,
9.221e+2, 9.360e+2, 9.500e+2, 9.555e+2, 9.611e+2,
9.720e+2, 9.829e+2, 9.940e+2, 1.005e+3, 1.017e+3,
1.028e+3, 1.040e+3, 1.051e+3, 1.063e+3, 1.075e+3,
1.087e+3, 1.100e+3, 1.112e+3, 1.125e+3, 1.137e+3,
1.150e+3, 1.171e+3, 1.191e+3, 1.213e+3, 1.234e+3,
1.249e+3, 1.265e+3, 1.280e+3, 1.296e+3, 1.312e+3,
1.328e+3, 1.344e+3, 1.361e+3, 1.377e+3, 1.394e+3,
1.411e+3, 1.429e+3, 1.446e+3, 1.464e+3, 1.482e+3,
1.500e+3, 1.525e+3, 1.550e+3, 1.585e+3, 1.610e+3,
1.636e+3, 1.662e+3, 1.689e+3, 1.716e+3, 1.744e+3,
1.772e+3, 1.800e+3, 1.828e+3, 1.856e+3, 1.885e+3,
1.914e+3, 1.943e+3, 1.973e+3, 2.004e+3, 2.035e+3,
2.075e+3, 2.116e+3, 2.158e+3, 2.200e+3, 2.250e+3,
2.290e+3, 2.337e+3, 2.386e+3, 2.435e+3, 2.500e+3,
2.532e+3, 2.580e+3, 2.613e+3, 2.679e+3, 2.747e+3,
2.808e+3, 2.871e+3, 2.935e+3, 3.000e+3, 3.035e+3,
3.112e+3, 3.191e+3, 3.272e+3, 3.355e+3, 3.440e+3,
3.527e+3, 3.616e+3, 3.707e+3, 3.740e+3, 3.819e+3,
3.900e+3, 3.998e+3, 4.099e+3, 4.202e+3, 4.307e+3,
4.400e+3, 4.500e+3, 4.620e+3, 4.740e+3, 4.850e+3,
4.960e+3, 5.100e+3, 5.250e+3, 5.400e+3, 5.531e+3,
5.645e+3, 5.700e+3, 5.879e+3, 6.000e+3, 6.128e+3,
6.258e+3, 6.392e+3, 6.528e+3, 6.667e+3, 6.809e+3,
6.954e+3, 7.102e+3, 7.212e+3, 7.323e+3, 7.437e+3,
7.552e+3, 7.669e+3, 7.787e+3, 7.908e+3, 8.030e+3,
8.159e+3, 8.289e+3, 8.422e+3, 8.557e+3, 8.694e+3,
8.834e+3, 8.975e+3, 9.119e+3, 9.307e+3, 9.500e+3,
9.763e+3, 1.003e+4, 1.031e+4, 1.060e+4, 1.086e+4,
1.114e+4, 1.142e+4, 1.171e+4, 1.202e+4, 1.234e+4,
1.266e+4, 1.300e+4, 1.324e+4, 1.348e+4, 1.373e+4,
1.398e+4, 1.424e+4, 1.450e+4, 1.476e+4, 1.503e+4,
1.527e+4, 1.550e+4, 1.574e+4, 1.599e+4, 1.623e+4,
1.649e+4, 1.674e+4, 1.700e+4, 1.727e+4, 1.755e+4,
1.783e+4, 1.812e+4, 1.841e+4, 1.870e+4, 1.900e+4,
1.931e+4, 1.965e+4, 2.000e+4, 2.045e+4, 2.092e+4,
2.139e+4, 2.188e+4, 2.229e+4, 2.271e+4, 2.314e+4,
2.358e+4, 2.388e+4, 2.418e+4, 2.450e+4, 2.479e+4,
2.500e+4, 2.520e+4, 2.552e+4, 2.580e+4, 2.606e+4,
2.653e+4, 2.700e+4, 2.737e+4, 2.774e+4, 2.812e+4,
2.850e+4, 2.887e+4, 2.924e+4, 2.962e+4, 3.000e+4,
3.045e+4, 3.090e+4, 3.136e+4, 3.183e+4, 3.243e+4,
3.304e+4, 3.367e+4, 3.431e+4, 3.468e+4, 3.507e+4,
3.545e+4, 3.584e+4, 3.624e+4, 3.663e+4, 3.704e+4,
3.744e+4, 3.786e+4, 3.827e+4, 3.869e+4, 3.912e+4,
3.955e+4, 3.998e+4, 4.042e+4, 4.087e+4, 4.136e+4,
4.186e+4, 4.237e+4, 4.288e+4, 4.340e+4, 4.393e+4,
4.446e+4, 4.500e+4, 4.565e+4, 4.631e+4, 4.721e+4,
4.812e+4, 4.905e+4, 5.000e+4, 5.099e+4, 5.200e+4,
5.248e+4, 5.347e+4, 5.448e+4, 5.551e+4, 5.656e+4,
5.740e+4, 5.826e+4, 5.912e+4, 6.000e+4, 6.088e+4,
6.177e+4, 6.267e+4, 6.358e+4, 6.451e+4, 6.545e+4,
6.641e+4, 6.738e+4, 6.851e+4, 6.965e+4, 7.081e+4,
7.200e+4, 7.300e+4, 7.399e+4, 7.500e+4, 7.610e+4,
7.722e+4, 7.835e+4, 7.950e+4, 8.074e+4, 8.200e+4,
8.250e+4, 8.374e+4, 8.500e+4, 8.652e+4, 8.788e+4,
8.926e+4, 9.067e+4, 9.210e+4, 9.355e+4, 9.502e+4,
9.652e+4, 9.804e+4, 1.000e+5, 1.013e+5, 1.027e+5,
1.040e+5, 1.054e+5, 1.068e+5, 1.082e+5, 1.096e+5,
1.111e+5, 1.125e+5, 1.139e+5, 1.153e+5, 1.168e+5,
1.183e+5, 1.197e+5, 1.213e+5, 1.228e+5, 1.241e+5,
1.255e+5, 1.269e+5, 1.283e+5, 1.291e+5, 1.307e+5,
1.323e+5, 1.340e+5, 1.357e+5, 1.374e+5, 1.391e+5,
1.409e+5, 1.426e+5, 1.445e+5, 1.463e+5, 1.481e+5,
1.490e+5, 1.519e+5, 1.538e+5, 1.557e+5, 1.576e+5,
1.596e+5, 1.616e+5, 1.637e+5, 1.657e+5, 1.678e+5,
1.699e+5, 1.721e+5, 1.742e+5, 1.764e+5, 1.786e+5,
1.809e+5, 1.832e+5, 1.855e+5, 1.878e+5, 1.902e+5,
1.926e+5, 1.962e+5, 2.000e+5, 2.024e+5, 2.050e+5,
2.076e+5, 2.102e+5, 2.128e+5, 2.155e+5, 2.182e+5,
2.209e+5, 2.237e+5, 2.265e+5, 2.294e+5, 2.323e+5,
2.352e+5, 2.381e+5, 2.411e+5, 2.442e+5, 2.472e+5,
2.500e+5, 2.527e+5, 2.555e+5, 2.584e+5, 2.612e+5,
2.641e+5, 2.670e+5, 2.700e+5, 2.732e+5, 2.767e+5,
2.802e+5, 2.837e+5, 2.873e+5, 2.909e+5, 2.945e+5,
2.972e+5, 2.985e+5, 3.020e+5, 3.053e+5, 3.088e+5,
3.122e+5, 3.157e+5, 3.192e+5, 3.228e+5, 3.264e+5,
3.300e+5, 3.337e+5, 3.379e+5, 3.422e+5, 3.465e+5,
3.508e+5, 3.553e+5, 3.597e+5, 3.643e+5, 3.688e+5,
3.735e+5, 3.782e+5, 3.829e+5, 3.877e+5, 3.938e+5,
4.000e+5, 4.076e+5, 4.138e+5, 4.200e+5, 4.249e+5,
4.299e+5, 4.349e+5, 4.400e+5, 4.452e+5, 4.505e+5,
4.553e+5, 4.601e+5, 4.650e+5, 4.700e+5, 4.772e+5,
4.845e+5, 4.920e+5, 4.995e+5, 5.054e+5, 5.113e+5,
5.173e+5, 5.234e+5, 5.299e+5, 5.365e+5, 5.432e+5,
5.500e+5, 5.557e+5, 5.614e+5, 5.672e+5, 5.730e+5,
5.784e+5, 5.891e+5, 6.000e+5, 6.081e+5, 6.158e+5,
6.235e+5, 6.313e+5, 6.393e+5, 6.468e+5, 6.545e+5,
6.622e+5, 6.700e+5, 6.790e+5, 6.926e+5, 7.065e+5,
7.154e+5, 7.244e+5, 7.335e+5, 7.427e+5, 7.500e+5,
7.576e+5, 7.653e+5, 7.730e+5, 7.808e+5, 7.904e+5,
8.002e+5, 8.100e+5, 8.200e+5, 8.301e+5, 8.403e+5,
8.506e+5, 8.611e+5, 8.750e+5, 8.874e+5, 9.000e+5,
9.072e+5, 9.200e+5, 9.400e+5, 9.616e+5, 9.800e+5,
1.003e+6, 1.010e+6, 1.040e+6, 1.070e+6, 1.100e+6,
1.108e+6, 1.136e+6, 1.165e+6, 1.200e+6, 1.225e+6,
1.250e+6, 1.287e+6, 1.317e+6, 1.356e+6, 1.400e+6,
1.423e+6, 1.461e+6, 1.500e+6, 1.536e+6, 1.572e+6,
1.612e+6, 1.653e+6, 1.695e+6, 1.738e+6, 1.782e+6,
1.827e+6, 1.850e+6, 1.921e+6, 1.969e+6, 2.019e+6,
2.070e+6, 2.123e+6, 2.176e+6, 2.231e+6, 2.307e+6,
2.354e+6, 2.365e+6, 2.385e+6, 2.466e+6, 2.479e+6,
2.535e+6, 2.592e+6, 2.658e+6, 2.725e+6, 2.794e+6,
2.865e+6, 2.932e+6, 3.000e+6, 3.080e+6, 3.166e+6,
3.247e+6, 3.329e+6, 3.413e+6, 3.499e+6, 3.588e+6,
3.679e+6, 3.772e+6, 3.867e+6, 3.965e+6, 4.066e+6,
4.183e+6, 4.304e+6, 4.398e+6, 4.493e+6, 4.607e+6,
4.724e+6, 4.800e+6, 4.882e+6, 4.966e+6, 5.092e+6,
5.221e+6, 5.353e+6, 5.488e+6, 5.627e+6, 5.770e+6,
5.916e+6, 6.065e+6, 6.219e+6, 6.376e+6, 6.434e+6,
6.592e+6, 6.703e+6, 6.873e+6, 7.047e+6, 7.225e+6,
7.408e+6, 7.596e+6, 7.788e+6, 7.985e+6, 8.187e+6,
8.395e+6, 8.607e+6, 8.825e+6, 9.048e+6, 9.278e+6,
9.512e+6, 9.753e+6, 1.000e+7, 1.025e+7, 1.051e+7,
1.078e+7, 1.105e+7, 1.133e+7, 1.162e+7, 1.191e+7,
1.221e+7, 1.252e+7, 1.284e+7, 1.317e+7, 1.350e+7,
1.384e+7, 1.419e+7, 1.455e+7, 1.492e+7, 1.530e+7,
1.568e+7, 1.608e+7, 1.649e+7, 1.691e+7, 1.733e+7,
1.790e+7, 1.845e+7, 1.900e+7, 1.964e+7, 2.000e+7])
GROUP_STRUCTURES['UKAEA-1102'] = np.array([
1.0000e-5, 1.0471e-5, 1.0965e-5, 1.1482e-5, 1.2023e-5,
1.2589e-5, 1.3183e-5, 1.3804e-5, 1.4454e-5, 1.5136e-5,

View file

@ -10,6 +10,7 @@ import numpy as np
import openmc
import openmc.mgxs
import openmc.checkvalue as cv
from openmc.checkvalue import PathLike
from ..tallies import ESTIMATOR_TYPES
@ -555,14 +556,14 @@ class Library:
self.all_mgxs[domain.id][mgxs_type] = mgxs
def add_to_tallies_file(self, tallies_file, merge=True):
"""Add all tallies from all MGXS objects to a tallies file.
def add_to_tallies(self, tallies, merge=True):
"""Add tallies from all MGXS objects to a tallies object.
NOTE: This assumes that :meth:`Library.build_library` has been called
Parameters
----------
tallies_file : openmc.Tallies
tallies : openmc.Tallies
A Tallies collection to add each MGXS' tallies to generate a
'tallies.xml' input file for OpenMC
merge : bool
@ -571,7 +572,7 @@ class Library:
"""
cv.check_type('tallies_file', tallies_file, openmc.Tallies)
cv.check_type('tallies', tallies, openmc.Tallies)
# Add tallies from each MGXS for each domain and mgxs type
for domain in self.domains:
@ -586,7 +587,15 @@ class Library:
= list(range(1, self.num_delayed_groups + 1))
for tally in mgxs.tallies.values():
tallies_file.append(tally, merge=merge)
tallies.append(tally, merge=merge)
def add_to_tallies_file(self, tallies_file, merge=True):
warn(
"The Library.add_to_tallies_file(...) method has been renamed to"
"add_to_tallies(...) and will be removed in a future version of "
"OpenMC.", FutureWarning
)
self.add_to_tallies(tallies_file, merge=merge)
def load_from_statepoint(self, statepoint):
"""Extracts tallies in an OpenMC StatePoint with the data needed to
@ -851,7 +860,7 @@ class Library:
'since a statepoint has not yet been loaded'
raise ValueError(msg)
cv.check_type('filename', filename, str)
cv.check_type('filename', filename, (str, PathLike))
cv.check_type('directory', directory, str)
import h5py
@ -894,7 +903,7 @@ class Library:
"""
cv.check_type('filename', filename, str)
cv.check_type('filename', filename, (str, PathLike))
cv.check_type('directory', directory, str)
# Make directory if it does not exist
@ -930,7 +939,7 @@ class Library:
"""
cv.check_type('filename', filename, str)
cv.check_type('filename', filename, (str, PathLike))
cv.check_type('directory', directory, str)
# Make directory if it does not exist

View file

@ -7,6 +7,7 @@ import numpy as np
import openmc
import openmc.checkvalue as cv
from openmc.checkvalue import PathLike
from openmc.mgxs import MGXS
from .mgxs import _DOMAIN_TO_FILTER
@ -722,7 +723,7 @@ class MDGXS(MGXS):
"""
cv.check_type('filename', filename, str)
cv.check_type('filename', filename, (str, PathLike))
cv.check_type('directory', directory, str)
cv.check_value('format', format, ['csv', 'excel', 'pickle', 'latex'])
cv.check_value('xs_type', xs_type, ['macro', 'micro'])

View file

@ -10,6 +10,7 @@ import numpy as np
import openmc
from openmc.data import REACTION_MT, REACTION_NAME, FISSION_MTS
import openmc.checkvalue as cv
from openmc.checkvalue import PathLike
from ..tallies import ESTIMATOR_TYPES
from . import EnergyGroups
@ -1982,7 +1983,7 @@ class MGXS:
"""
cv.check_type('filename', filename, str)
cv.check_type('filename', filename, (str, PathLike))
cv.check_type('directory', directory, str)
cv.check_value('format', format, ['csv', 'excel', 'pickle', 'latex'])
cv.check_value('xs_type', xs_type, ['macro', 'micro'])
@ -2126,8 +2127,8 @@ class MGXS:
df['std. dev.'] /= np.tile(densities, tile_factor)
# Replace NaNs by zeros (happens if nuclide density is zero)
df['mean'].replace(np.nan, 0.0, inplace=True)
df['std. dev.'].replace(np.nan, 0.0, inplace=True)
df['mean'] = df['mean'].replace(np.nan, 0.0)
df['std. dev.'] = df['std. dev.'].replace(np.nan, 0.0)
# Sort the dataframe by domain type id (e.g., distribcell id) and
# energy groups such that data is from fast to thermal

View file

@ -11,7 +11,7 @@ import openmc
import openmc.mgxs
from openmc.mgxs import SCATTER_TABULAR, SCATTER_LEGENDRE, SCATTER_HISTOGRAM
from .checkvalue import check_type, check_value, check_greater_than, \
check_iterable_type, check_less_than, check_filetype_version
check_iterable_type, check_less_than, check_filetype_version, PathLike
ROOM_TEMPERATURE_KELVIN = 294.0
@ -2506,7 +2506,7 @@ class MGXSLibrary:
Parameters
----------
filename : str
filename : str or PathLike
Filename of file, default is mgxs.h5.
libver : {'earliest', 'latest'}
Compatibility mode for the HDF5 file. 'latest' will produce files
@ -2514,8 +2514,7 @@ class MGXSLibrary:
"""
check_type('filename', filename, str)
check_type('filename', filename, (str, PathLike))
# Create and write to the HDF5 file
file = h5py.File(filename, "w", libver=libver)
file.attrs['filetype'] = np.bytes_(_FILETYPE_MGXS_LIBRARY)
@ -2554,7 +2553,7 @@ class MGXSLibrary:
raise ValueError("Either path or openmc.config['mg_cross_sections']"
"must be set")
check_type('filename', filename, str)
check_type('filename', filename, (str, PathLike))
file = h5py.File(filename, 'r')
# Check filetype and version

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
import copy
from dataclasses import dataclass, field
from functools import cache
from pathlib import Path
import math
@ -8,11 +9,13 @@ from numbers import Integral, Real
import random
import re
from tempfile import NamedTemporaryFile, TemporaryDirectory
from typing import Any, Protocol
import warnings
import h5py
import lxml.etree as ET
import numpy as np
from scipy.optimize import curve_fit
import openmc
import openmc._xml as xml
@ -24,6 +27,12 @@ from openmc.plots import add_plot_params, _BASIS_INDICES
from openmc.utility_funcs import change_directory
# Protocol for a function that is passed to search_keff
class ModelModifier(Protocol):
def __call__(self, val: float, **kwargs: Any) -> None:
...
class Model:
"""Model container.
@ -203,6 +212,29 @@ class Model:
result[mat.name].add(mat)
return result
# TODO: This should really get incorporated in lower-level calls to
# get_all_materials, but right now it requires information from the Model object
def _get_all_materials(self) -> dict[int, openmc.Material]:
"""Get all materials including those in DAGMC universes
Returns
-------
dict
Dictionary mapping material ID to material instances
"""
# Get all materials from the Geometry object
materials = self.geometry.get_all_materials()
# Account for materials in DAGMC universes
for cell in self.geometry.get_all_cells().values():
if isinstance(cell.fill, openmc.DAGMCUniverse):
names = cell.fill.material_names
materials.update({
mat.id: mat for mat in self.materials if mat.name in names
})
return materials
def add_kinetics_parameters_tallies(self, num_groups: int | None = None):
"""Add tallies for calculating kinetics parameters using the IFP method.
@ -1759,7 +1791,7 @@ class Model:
mgxs_lib.build_library()
# Create a "tallies.xml" file for the MGXS Library
mgxs_lib.add_to_tallies_file(model.tallies, merge=True)
mgxs_lib.add_to_tallies(model.tallies, merge=True)
# Run
statepoint_filename = model.run(cwd=directory)
@ -1948,7 +1980,7 @@ class Model:
mgxs_lib.build_library()
# Create a "tallies.xml" file for the MGXS Library
mgxs_lib.add_to_tallies_file(model.tallies, merge=True)
mgxs_lib.add_to_tallies(model.tallies, merge=True)
# Run
statepoint_filename = model.run(cwd=directory)
@ -2043,7 +2075,7 @@ class Model:
mgxs_lib.build_library()
# Create a "tallies.xml" file for the MGXS Library
mgxs_lib.add_to_tallies_file(model.tallies, merge=True)
mgxs_lib.add_to_tallies(model.tallies, merge=True)
# Run
statepoint_filename = model.run(cwd=directory)
@ -2196,3 +2228,262 @@ class Model:
# Take a wild guess as to how many rays are needed
self.settings.particles = 2 * int(max_length)
def keff_search(
self,
func: ModelModifier,
x0: float,
x1: float,
target: float = 1.0,
k_tol: float = 1e-4,
sigma_final: float = 3e-4,
p: float = 0.5,
q: float = 0.95,
memory: int = 4,
x_min: float | None = None,
x_max: float | None = None,
b0: int | None = None,
b_min: int = 20,
b_max: int | None = None,
maxiter: int = 50,
output: bool = False,
func_kwargs: dict[str, Any] | None = None,
run_kwargs: dict[str, Any] | None = None,
) -> SearchResult:
r"""Perform a keff search on a model parametrized by a single variable.
This method uses the GRsecant method described in a paper by `Price and
Roskoff <https://doi.org/10.1016/j.pnucene.2023.104731>`_. The GRsecant
method is a modification of the secant method that accounts for
uncertainties in the function evaluations. The method uses a weighted
linear fit of the most recent function evaluations to predict the next
point to evaluate. It also adaptively changes the number of batches to
meet the target uncertainty value at each iteration.
The target uncertainty for iteration :math:`n+1` is determined by the
following equation (following Eq. (8) in the paper):
.. math::
\sigma_{i+1} = q \sigma_\text{final} \left ( \frac{ \min \left \{
\left\lvert k_i - k_\text{target} \right\rvert : k=0,1,\dots,n
\right \} }{k_\text{tol}} \right )^p
where :math:`q` is a multiplicative factor less than 1, given as the
``sigma_factor`` parameter below.
Parameters
----------
func : ModelModifier
Function that takes the parameter to be searched and makes a
modification to the model.
x0 : float
First guess for the parameter passed to `func`
x1 : float
Second guess for the parameter passed to `func`
target : float, optional
keff value to search for
k_tol : float, optional
Stopping criterion on the function value; the absolute value must be
within ``k_tol`` of zero to be accepted.
sigma_final : float, optional
Maximum accepted k-effective uncertainty for the stopping criterion.
p : float, optional
Exponent used in the stopping criterion.
q : float, optional
Multiplicative factor used in the stopping criterion.
memory : int, optional
Number of most-recent points used in the weighted linear fit of
``f(x) = a + b x`` to predict the next point.
x_min : float, optional
Minimum allowed value for the parameter ``x``.
x_max : float, optional
Maximum allowed value for the parameter ``x``.
b0 : int, optional
Number of active batches to use for the initial function
evaluations. If None, uses the model's current setting.
b_min : int, optional
Minimum number of active batches to use in a function evaluation.
b_max : int, optional
Maximum number of active batches to use in a function evaluation.
maxiter : int, optional
Maximum number of iterations to perform.
output : bool, optional
Whether or not to display output showing iteration progress.
func_kwargs : dict, optional
Keyword-based arguments to pass to the `func` function.
run_kwargs : dict, optional
Keyword arguments to pass to :meth:`openmc.Model.run` or
:meth:`openmc.lib.run`.
Returns
-------
SearchResult
Result object containing the estimated root (parameter value) and
evaluation history (parameters, means, standard deviations, and
batches), plus convergence status and termination reason.
"""
import openmc.lib
check_type('model modifier', func, Callable)
check_type('target', target, Real)
if memory < 2:
raise ValueError("memory must be ≥ 2")
func_kwargs = {} if func_kwargs is None else dict(func_kwargs)
run_kwargs = {} if run_kwargs is None else dict(run_kwargs)
run_kwargs.setdefault('output', False)
# Create lists to store the history of evaluations
xs: list[float] = []
fs: list[float] = []
ss: list[float] = []
gs: list[int] = []
count = 0
# Helper function to evaluate f and store results
def eval_at(x: float, batches: int) -> tuple[float, float]:
# Modify the model with the current guess
func(x, **func_kwargs)
# Change the number of batches and run the model
batches += self.settings.inactive
if openmc.lib.is_initialized:
openmc.lib.settings.set_batches(batches)
openmc.lib.reset()
openmc.lib.run(**run_kwargs)
sp_filepath = f'statepoint.{batches}.h5'
else:
self.settings.batches = batches
sp_filepath = self.run(**run_kwargs)
# Extract keff and its uncertainty
with openmc.StatePoint(sp_filepath) as sp:
keff = sp.keff
if output:
nonlocal count
count += 1
print(f'Iteration {count}: {batches=}, {x=:.6g}, {keff=:.5f}')
xs.append(float(x))
fs.append(float(keff.n - target))
ss.append(float(keff.s))
gs.append(int(batches))
return fs[-1], ss[-1]
# Default b0 to current model settings if not explicitly provided
if b0 is None:
b0 = self.settings.batches - self.settings.inactive
# Perform the search (inlined GRsecant) in a temporary directory
with TemporaryDirectory() as tmpdir:
if not openmc.lib.is_initialized:
run_kwargs.setdefault('cwd', tmpdir)
# ---- Seed with two evaluations
f0, s0 = eval_at(x0, b0)
if abs(f0) <= k_tol and s0 <= sigma_final:
return SearchResult(x0, xs, fs, ss, gs, True, "converged")
f1, s1 = eval_at(x1, b0)
if abs(f1) <= k_tol and s1 <= sigma_final:
return SearchResult(x1, xs, fs, ss, gs, True, "converged")
for _ in range(maxiter - 2):
# ------ Step 1: propose next x via GRsecant
m = min(memory, len(xs))
# Perform a curve fit on f(x) = a + bx accounting for
# uncertainties. This is equivalent to minimizing the function
# in Equation (A.14)
(a, b), _ = curve_fit(
lambda x, a, b: a + b*x,
xs[-m:], fs[-m:], sigma=ss[-m:], absolute_sigma=True
)
x_new = float(-a / b)
# Clamp x_new to the bounds if provided
if x_min is not None:
x_new = max(x_new, x_min)
if x_max is not None:
x_new = min(x_new, x_max)
# ------ Step 2: choose target σ for next run (Eq. 8 + clamp)
min_abs_f = float(np.min(np.abs(fs)))
base = q * sigma_final
ratio = min_abs_f / k_tol if k_tol > 0 else 1.0
sig = base * (ratio ** p)
sig_target = max(sig, base)
# ------ Step 3: choose generations to hit σ_target (Appendix C)
# Use at least two past points for regression
if len(gs) >= 2 and np.var(np.log(gs)) > 0.0:
# Perform a curve fit based on Eq. (C.3) to solve for ln(k).
# Note that unlike in the paper, we do not leave r as an
# undetermined parameter and choose r=0.5.
(ln_k,), _ = curve_fit(
lambda ln_b, ln_k: ln_k - 0.5*ln_b,
np.log(gs[-4:]), np.log(ss[-4:]),
)
k = float(np.exp(ln_k))
else:
k = float(ss[-1] * math.sqrt(gs[-1]))
b_new = (k / sig_target) ** 2
# Clamp and round up to integer
b_new = max(b_min, math.ceil(b_new))
if b_max is not None:
b_new = min(b_new, b_max)
# Evaluate at proposed x with batches determined above
f_new, s_new = eval_at(x_new, b_new)
# Termination based on both criteria (|f| and σ)
if abs(f_new) <= k_tol and s_new <= sigma_final:
return SearchResult(x_new, xs, fs, ss, gs, True, "converged")
return SearchResult(xs[-1], xs, fs, ss, gs, False, "maxiter")
@dataclass
class SearchResult:
"""Result of a GRsecant keff search.
Attributes
----------
root : float
Estimated parameter value where f(x) = 0 at termination.
parameters : list[float]
Parameter values (x) evaluated during the search, in order.
keffs : list[float]
Estimated keff values for each evaluation.
stdevs : list[float]
One-sigma uncertainties of keff for each evaluation.
batches : list[int]
Number of active batches used for each evaluation.
converged : bool
Whether both |f| <= k_tol and sigma <= sigma_final were met.
flag : str
Reason for termination (e.g., "converged", "maxiter").
"""
root: float
parameters: list[float] = field(repr=False)
means: list[float] = field(repr=False)
stdevs: list[float] = field(repr=False)
batches: list[int] = field(repr=False)
converged: bool
flag: str
@property
def function_calls(self) -> int:
"""Number of function evaluations performed."""
return len(self.parameters)
@property
def total_batches(self) -> int:
"""Total number of active batches used across all evaluations."""
return sum(self.batches)

View file

@ -439,7 +439,7 @@ class PlotBase(IDManagerMixin):
@filename.setter
def filename(self, filename):
cv.check_type('filename', filename, str)
cv.check_type('filename', filename, (str, PathLike))
self._filename = filename
@property

View file

@ -6,7 +6,7 @@ from numbers import Integral, Real
from pathlib import Path
import lxml.etree as ET
import warnings
import openmc
import openmc.checkvalue as cv
from openmc.checkvalue import PathLike
@ -47,6 +47,21 @@ class Settings:
half-width of the 95% two-sided confidence interval. If False,
uncertainties on tally results will be reported as the sample standard
deviation.
collision_track : dict
Options for writing collision information. Acceptable keys are:
:max_collisions: Maximum number of collisions to be banked per file. (int)
:max_collision_track_files: Maximum number of collision_track files. (int)
:mcpl: Output in the form of an MCPL-file. (bool)
:cell_ids: List of cell IDs to define cells in which collisions should be banked. (list of int)
:universe_ids: List of universe IDs to define universes in which collisions should be banked. (list of int)
:material_ids: List of material IDs to define materials in which collisions should be banked. (list of int)
:nuclides: List of nuclides to define nuclides in which collisions should be banked.
(ex: ["I135m", "U233"] ). (list of str)
:reactions: List of reaction to define specific reactions that should be banked
(ex: ["(n,fission)", 2, "(n,2n)"] ). (list of str or int)
:deposited_E_threshold: Number to define the minimum deposited energy during
per collision to trigger banking. (float)
create_fission_neutrons : bool
Indicate whether fission neutrons should be created or not.
cutoff : dict
@ -395,6 +410,9 @@ class Settings:
# Iterated Fission Probability
self._ifp_n_generation = None
# Collision track feature
self._collision_track = {}
# Output options
self._statepoint = {}
self._sourcepoint = {}
@ -434,7 +452,8 @@ class Settings:
self._max_particle_events = None
self._write_initial_source = None
self._weight_windows = WeightWindowsList()
self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators')
self._weight_window_generators = cv.CheckedList(
WeightWindowGenerator, 'weight window generators')
self._weight_windows_on = None
self._weight_windows_file = None
self._weight_window_checkpoints = {}
@ -475,8 +494,9 @@ class Settings:
@generations_per_batch.setter
def generations_per_batch(self, generations_per_batch: int):
cv.check_type('generations per patch', generations_per_batch, Integral)
cv.check_greater_than('generations per batch', generations_per_batch, 0)
cv.check_type('generations per batch', generations_per_batch, Integral)
cv.check_greater_than('generations per batch',
generations_per_batch, 0)
self._generations_per_batch = generations_per_batch
@property
@ -506,7 +526,8 @@ class Settings:
@rel_max_lost_particles.setter
def rel_max_lost_particles(self, rel_max_lost_particles: float):
cv.check_type('rel_max_lost_particles', rel_max_lost_particles, Real)
cv.check_greater_than('rel_max_lost_particles', rel_max_lost_particles, 0)
cv.check_greater_than('rel_max_lost_particles',
rel_max_lost_particles, 0)
cv.check_less_than('rel_max_lost_particles', rel_max_lost_particles, 1)
self._rel_max_lost_particles = rel_max_lost_particles
@ -516,8 +537,10 @@ class Settings:
@max_write_lost_particles.setter
def max_write_lost_particles(self, max_write_lost_particles: int):
cv.check_type('max_write_lost_particles', max_write_lost_particles, Integral)
cv.check_greater_than('max_write_lost_particles', max_write_lost_particles, 0)
cv.check_type('max_write_lost_particles',
max_write_lost_particles, Integral)
cv.check_greater_than('max_write_lost_particles',
max_write_lost_particles, 0)
self._max_write_lost_particles = max_write_lost_particles
@property
@ -538,12 +561,12 @@ class Settings:
def keff_trigger(self, keff_trigger: dict):
if not isinstance(keff_trigger, dict):
msg = f'Unable to set a trigger on keff from "{keff_trigger}" ' \
'which is not a Python dictionary'
'which is not a Python dictionary'
raise ValueError(msg)
elif 'type' not in keff_trigger:
msg = f'Unable to set a trigger on keff from "{keff_trigger}" ' \
'which does not have a "type" key'
'which does not have a "type" key'
raise ValueError(msg)
elif keff_trigger['type'] not in ['variance', 'std_dev', 'rel_err']:
@ -553,7 +576,7 @@ class Settings:
elif 'threshold' not in keff_trigger:
msg = f'Unable to set a trigger on keff from "{keff_trigger}" ' \
'which does not have a "threshold" key'
'which does not have a "threshold" key'
raise ValueError(msg)
elif not isinstance(keff_trigger['threshold'], Real):
@ -570,7 +593,7 @@ class Settings:
@energy_mode.setter
def energy_mode(self, energy_mode: str):
cv.check_value('energy mode', energy_mode,
['continuous-energy', 'multi-group'])
['continuous-energy', 'multi-group'])
self._energy_mode = energy_mode
@property
@ -593,7 +616,8 @@ class Settings:
def source(self, source: SourceBase | Iterable[SourceBase]):
if not isinstance(source, MutableSequence):
source = [source]
self._source = cv.CheckedList(SourceBase, 'source distributions', source)
self._source = cv.CheckedList(
SourceBase, 'source distributions', source)
@property
def confidence_intervals(self) -> bool:
@ -610,7 +634,8 @@ class Settings:
@electron_treatment.setter
def electron_treatment(self, electron_treatment: str):
cv.check_value('electron treatment', electron_treatment, ['led', 'ttb'])
cv.check_value('electron treatment',
electron_treatment, ['led', 'ttb'])
self._electron_treatment = electron_treatment
@property
@ -704,7 +729,8 @@ class Settings:
@trigger_max_batches.setter
def trigger_max_batches(self, trigger_max_batches: int):
cv.check_type('trigger maximum batches', trigger_max_batches, Integral)
cv.check_greater_than('trigger maximum batches', trigger_max_batches, 0)
cv.check_greater_than('trigger maximum batches',
trigger_max_batches, 0)
self._trigger_max_batches = trigger_max_batches
@property
@ -713,8 +739,10 @@ class Settings:
@trigger_batch_interval.setter
def trigger_batch_interval(self, trigger_batch_interval: int):
cv.check_type('trigger batch interval', trigger_batch_interval, Integral)
cv.check_greater_than('trigger batch interval', trigger_batch_interval, 0)
cv.check_type('trigger batch interval',
trigger_batch_interval, Integral)
cv.check_greater_than('trigger batch interval',
trigger_batch_interval, 0)
self._trigger_batch_interval = trigger_batch_interval
@property
@ -798,19 +826,22 @@ class Settings:
@surf_source_write.setter
def surf_source_write(self, surf_source_write: dict):
cv.check_type("surface source writing options", surf_source_write, Mapping)
cv.check_type("surface source writing options",
surf_source_write, Mapping)
for key, value in surf_source_write.items():
cv.check_value(
"surface source writing key",
key,
("surface_ids", "max_particles", "max_source_files", "mcpl", "cell", "cellfrom", "cellto"),
("surface_ids", "max_particles", "max_source_files",
"mcpl", "cell", "cellfrom", "cellto"),
)
if key == "surface_ids":
cv.check_type(
"surface ids for source banking", value, Iterable, Integral
)
for surf_id in value:
cv.check_greater_than("surface id for source banking", surf_id, 0)
cv.check_greater_than(
"surface id for source banking", surf_id, 0)
elif key == "mcpl":
cv.check_type("write to an MCPL-format file", value, bool)
@ -827,6 +858,79 @@ class Settings:
self._surf_source_write = surf_source_write
@property
def collision_track(self) -> dict:
return self._collision_track
@collision_track.setter
def collision_track(self, collision_track: dict):
cv.check_type('Collision tracking options', collision_track, Mapping)
for key, value in collision_track.items():
cv.check_value('collision_track key', key,
('cell_ids', 'reactions', 'universe_ids', 'material_ids', 'nuclides',
'deposited_E_threshold', 'max_collisions', 'max_collision_track_files', 'mcpl'))
if key == 'cell_ids':
cv.check_type('cell ids for collision tracking data banking', value,
Iterable, Integral)
for cell_id in value:
cv.check_greater_than('cell id for collision tracking data banking',
cell_id, 0)
elif key == 'reactions':
cv.check_type('MT numbers for collision tracking data banking', value,
Iterable)
for reaction in value:
if isinstance(reaction, int):
cv.check_greater_than(
'MT number for collision tracking data banking', reaction, 0
)
elif isinstance(reaction, str):
# check against allowed strings? so far let C++ code handle it
pass
else:
raise TypeError(
f"MT number for collision tracking data banking must be a positive int or string, "
f"got {type(reaction).__name__}")
elif key == 'universe_ids':
cv.check_type('universe ids for collision tracking data banking', value,
Iterable, Integral)
for universe_id in value:
cv.check_greater_than('universe id for collision tracking data banking',
universe_id, 0)
elif key == 'material_ids':
cv.check_type('material ids for collision tracking data banking', value,
Iterable, Integral)
for material_id in value:
cv.check_greater_than('material id for collision tracking data banking',
material_id, 0)
elif key == 'nuclides':
cv.check_type('nuclides for collision tracking data banking', value,
Iterable, str)
for nuclide in value:
# If nuclide name doesn't look valid, give a warning
try:
openmc.data.zam(nuclide)
except ValueError:
warnings.warn(f"Nuclide {nuclide} is not valid")
elif key == 'deposited_E_threshold':
cv.check_type('Deposited Energy Threshold for collision tracking data banking',
value, Real)
cv.check_greater_than('Deposited Energy Threshold for collision tracking data banking',
value, 0)
elif key == 'max_collisions':
cv.check_type('maximum collisions banks per file',
value, Integral)
cv.check_greater_than('maximum collisions banks in collision tracking',
value, 0)
elif key == 'max_collision_track_files':
cv.check_type('maximum collisions banks',
value, Integral)
cv.check_greater_than('maximum number of collision_track files ',
value, 0)
elif key == 'mcpl':
cv.check_type('write to an MCPL-format file', value, bool)
self._collision_track = collision_track
@property
def no_reduce(self) -> bool:
return self._no_reduce
@ -943,7 +1047,7 @@ class Settings:
def cutoff(self, cutoff: dict):
if not isinstance(cutoff, Mapping):
msg = f'Unable to set cutoff from "{cutoff}" which is not a '\
'Python dictionary'
'Python dictionary'
raise ValueError(msg)
for key in cutoff:
if key == 'weight':
@ -961,7 +1065,7 @@ class Settings:
cv.check_greater_than('energy cutoff', cutoff[key], 0.0)
else:
msg = f'Unable to set cutoff to "{key}" which is unsupported ' \
'by OpenMC'
'by OpenMC'
self._cutoff = cutoff
@ -1130,12 +1234,14 @@ class Settings:
@weight_window_checkpoints.setter
def weight_window_checkpoints(self, weight_window_checkpoints: dict):
for key in weight_window_checkpoints.keys():
cv.check_value('weight_window_checkpoints', key, ('collision', 'surface'))
cv.check_value('weight_window_checkpoints',
key, ('collision', 'surface'))
self._weight_window_checkpoints = weight_window_checkpoints
@property
def max_splits(self):
raise AttributeError('max_splits has been deprecated. Please use max_history_splits instead')
raise AttributeError(
'max_splits has been deprecated. Please use max_history_splits instead')
@property
def max_history_splits(self) -> int:
@ -1184,7 +1290,8 @@ class Settings:
def weight_window_generators(self, wwgs):
if not isinstance(wwgs, MutableSequence):
wwgs = [wwgs]
self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators', wwgs)
self._weight_window_generators = cv.CheckedList(
WeightWindowGenerator, 'weight window generators', wwgs)
@property
def random_ray(self) -> dict:
@ -1221,7 +1328,8 @@ class Settings:
for mesh, domains in value:
cv.check_type('mesh', mesh, MeshBase)
cv.check_type('domains', domains, Iterable)
valid_types = (openmc.Material, openmc.Cell, openmc.Universe)
valid_types = (openmc.Material,
openmc.Cell, openmc.Universe)
for domain in domains:
if not isinstance(domain, valid_types):
raise ValueError(
@ -1255,9 +1363,12 @@ class Settings:
@source_rejection_fraction.setter
def source_rejection_fraction(self, source_rejection_fraction: float):
cv.check_type('source_rejection_fraction', source_rejection_fraction, Real)
cv.check_greater_than('source_rejection_fraction', source_rejection_fraction, 0)
cv.check_less_than('source_rejection_fraction', source_rejection_fraction, 1)
cv.check_type('source_rejection_fraction',
source_rejection_fraction, Real)
cv.check_greater_than('source_rejection_fraction',
source_rejection_fraction, 0)
cv.check_less_than('source_rejection_fraction',
source_rejection_fraction, 1)
self._source_rejection_fraction = source_rejection_fraction
@property
@ -1422,6 +1533,45 @@ class Settings:
subelement = ET.SubElement(element, key)
subelement.text = str(self._surf_source_write[key])
def _create_collision_track_subelement(self, root):
if self._collision_track:
element = ET.SubElement(root, "collision_track")
if 'cell_ids' in self._collision_track:
subelement = ET.SubElement(element, "cell_ids")
subelement.text = ' '.join(
str(x) for x in self._collision_track['cell_ids'])
if 'reactions' in self._collision_track:
subelement = ET.SubElement(element, "reactions")
subelement.text = ' '.join(
str(x) for x in self._collision_track['reactions'])
if 'universe_ids' in self._collision_track:
subelement = ET.SubElement(element, "universe_ids")
subelement.text = ' '.join(
str(x) for x in self._collision_track['universe_ids'])
if 'material_ids' in self._collision_track:
subelement = ET.SubElement(element, "material_ids")
subelement.text = ' '.join(
str(x) for x in self._collision_track['material_ids'])
if 'nuclides' in self._collision_track:
subelement = ET.SubElement(element, "nuclides")
subelement.text = ' '.join(
str(x) for x in self._collision_track['nuclides'])
if 'deposited_E_threshold' in self._collision_track:
subelement = ET.SubElement(element, "deposited_E_threshold")
subelement.text = str(
self._collision_track['deposited_E_threshold'])
if 'max_collisions' in self._collision_track:
subelement = ET.SubElement(element, "max_collisions")
subelement.text = str(self._collision_track['max_collisions'])
if 'max_collision_track_files' in self._collision_track:
subelement = ET.SubElement(
element, "max_collision_track_files")
subelement.text = str(
self._collision_track['max_collision_track_files'])
if 'mcpl' in self._collision_track:
subelement = ET.SubElement(element, "mcpl")
subelement.text = str(self._collision_track['mcpl']).lower()
def _create_confidence_intervals(self, root):
if self._confidence_intervals is not None:
element = ET.SubElement(root, "confidence_intervals")
@ -1477,8 +1627,8 @@ class Settings:
# use default heuristic for entropy mesh if not set by user
if self.entropy_mesh.dimension is None:
if self.particles is None:
raise RuntimeError("Number of particles must be set in order to " \
"use entropy mesh dimension heuristic")
raise RuntimeError("Number of particles must be set in order to "
"use entropy mesh dimension heuristic")
else:
n = ceil((self.particles / 20.0)**(1.0 / 3.0))
d = len(self.entropy_mesh.lower_left)
@ -1568,7 +1718,8 @@ class Settings:
path = f"./mesh[@id='{self.ufs_mesh.id}']"
if root.find(path) is None:
root.append(self.ufs_mesh.to_xml_element())
if mesh_memo is not None: mesh_memo.add(self.ufs_mesh.id)
if mesh_memo is not None:
mesh_memo.add(self.ufs_mesh.id)
def _create_use_decay_photons_subelement(self, root):
if self._use_decay_photons is not None:
@ -1694,11 +1845,13 @@ class Settings:
if 'collision' in self._weight_window_checkpoints:
subelement = ET.SubElement(element, "collision")
subelement.text = str(self._weight_window_checkpoints['collision']).lower()
subelement.text = str(
self._weight_window_checkpoints['collision']).lower()
if 'surface' in self._weight_window_checkpoints:
subelement = ET.SubElement(element, "surface")
subelement.text = str(self._weight_window_checkpoints['surface']).lower()
subelement.text = str(
self._weight_window_checkpoints['surface']).lower()
def _create_max_history_splits_subelement(self, root):
if self._max_history_splits is not None:
@ -1730,6 +1883,9 @@ class Settings:
for domain in domains:
domain_elem = ET.SubElement(mesh_elem, 'domain')
domain_elem.set('id', str(domain.id))
domain_elem.set(
'type', domain.__class__.__name__.lower())
if mesh_memo is not None and mesh.id not in mesh_memo:
domain_elem.set('type', domain.__class__.__name__.lower())
# See if a <mesh> element already exists -- if not, add it
path = f"./mesh[@id='{mesh.id}']"
@ -1881,6 +2037,25 @@ class Settings:
value = int(value)
self.surf_source_write[key] = value
def _collision_track_from_xml_element(self, root):
elem = root.find('collision_track')
if elem is not None:
for key in ('cell_ids', 'reactions', 'universe_ids', 'material_ids', 'nuclides',
'deposited_E_threshold', 'max_collisions', "max_collision_track_files", 'mcpl'):
value = get_text(elem, key)
if value is not None:
if key in ('cell_ids', 'universe_ids', 'material_ids'):
value = [int(x) for x in value.split()]
elif key in ('reactions', 'nuclides'):
value = value.split()
elif key in ('max_collisions', 'max_collision_track_files'):
value = int(value)
elif key == 'deposited_E_threshold':
value = float(value)
elif key == 'mcpl':
value = value in ('true', '1')
self.collision_track[key] = value
def _confidence_intervals_from_xml_element(self, root):
text = get_text(root, 'confidence_intervals')
if text is not None:
@ -2180,7 +2355,8 @@ class Settings:
elif domain_type == 'universe':
domain = openmc.Universe(domain_id)
domains.append(domain)
self.random_ray['source_region_meshes'].append((mesh, domains))
self.random_ray['source_region_meshes'].append(
(mesh, domains))
def _use_decay_photons_from_xml_element(self, root):
text = get_text(root, 'use_decay_photons')
@ -2223,6 +2399,7 @@ class Settings:
self._create_sourcepoint_subelement(element)
self._create_surf_source_read_subelement(element)
self._create_surf_source_write_subelement(element)
self._create_collision_track_subelement(element)
self._create_confidence_intervals(element)
self._create_electron_treatment_subelement(element)
self._create_energy_mode_subelement(element)
@ -2336,6 +2513,7 @@ class Settings:
settings._sourcepoint_from_xml_element(elem)
settings._surf_source_read_from_xml_element(elem)
settings._surf_source_write_from_xml_element(elem)
settings._collision_track_from_xml_element(elem)
settings._confidence_intervals_from_xml_element(elem)
settings._electron_treatment_from_xml_element(elem)
settings._energy_mode_from_xml_element(elem)

View file

@ -6,7 +6,6 @@ from numbers import Real
from pathlib import Path
import warnings
from typing import Any
from pathlib import Path
import lxml.etree as ET
import numpy as np
@ -107,10 +106,12 @@ class SourceBase(ABC):
cv.check_type('fissionable', value, bool)
self._constraints['fissionable'] = value
elif key == 'rejection_strategy':
cv.check_value('rejection strategy', value, ('resample', 'kill'))
cv.check_value('rejection strategy',
value, ('resample', 'kill'))
self._constraints['rejection_strategy'] = value
else:
raise ValueError(f'Unknown key in constraints dictionary: {key}')
raise ValueError(
f'Unknown key in constraints dictionary: {key}')
@abstractmethod
def populate_xml_element(self, element):
@ -144,13 +145,16 @@ class SourceBase(ABC):
dt_elem = ET.SubElement(constraints_elem, "domain_type")
dt_elem.text = constraints["domain_type"]
id_elem = ET.SubElement(constraints_elem, "domain_ids")
id_elem.text = ' '.join(str(uid) for uid in constraints["domain_ids"])
id_elem.text = ' '.join(str(uid)
for uid in constraints["domain_ids"])
if "time_bounds" in constraints:
dt_elem = ET.SubElement(constraints_elem, "time_bounds")
dt_elem.text = ' '.join(str(t) for t in constraints["time_bounds"])
dt_elem.text = ' '.join(str(t)
for t in constraints["time_bounds"])
if "energy_bounds" in constraints:
dt_elem = ET.SubElement(constraints_elem, "energy_bounds")
dt_elem.text = ' '.join(str(E) for E in constraints["energy_bounds"])
dt_elem.text = ' '.join(str(E)
for E in constraints["energy_bounds"])
if "fissionable" in constraints:
dt_elem = ET.SubElement(constraints_elem, "fissionable")
dt_elem.text = str(constraints["fissionable"]).lower()
@ -199,7 +203,8 @@ class SourceBase(ABC):
elif source_type == 'mesh':
return MeshSource.from_xml_element(elem, meshes)
else:
raise ValueError(f'Source type {source_type} is not recognized')
raise ValueError(
f'Source type {source_type} is not recognized')
@staticmethod
def _get_constraints(elem: ET.Element) -> dict[str, Any]:
@ -316,7 +321,8 @@ class IndependentSource(SourceBase):
time: openmc.stats.Univariate | None = None,
strength: float = 1.0,
particle: str = 'neutron',
domains: Sequence[openmc.Cell | openmc.Material | openmc.Universe] | None = None,
domains: Sequence[openmc.Cell | openmc.Material |
openmc.Universe] | None = None,
constraints: dict[str, Any] | None = None
):
if domains is not None:
@ -527,11 +533,12 @@ class MeshSource(SourceBase):
'fissionable', and 'rejection_strategy'.
"""
def __init__(
self,
mesh: MeshBase,
sources: Sequence[SourceBase],
constraints: dict[str, Any] | None = None,
constraints: dict[str, Any] | None = None,
):
super().__init__(strength=None, constraints=constraints)
self.mesh = mesh
@ -577,7 +584,8 @@ class MeshSource(SourceBase):
elif isinstance(self.mesh, UnstructuredMesh):
if s.ndim > 1:
raise ValueError('Sources must be a 1-D array for unstructured mesh')
raise ValueError(
'Sources must be a 1-D array for unstructured mesh')
self._sources = s
for src in self._sources:
@ -646,7 +654,8 @@ class MeshSource(SourceBase):
mesh_id = int(get_text(elem, 'mesh'))
mesh = meshes[mesh_id]
sources = [SourceBase.from_xml_element(e) for e in elem.iterchildren('source')]
sources = [SourceBase.from_xml_element(
e) for e in elem.iterchildren('source')]
constraints = cls._get_constraints(elem)
return cls(mesh, sources, constraints=constraints)
@ -656,7 +665,8 @@ def Source(*args, **kwargs):
A function for backward compatibility of sources. Will be removed in the
future. Please update to IndependentSource.
"""
warnings.warn("This class is deprecated in favor of 'IndependentSource'", FutureWarning)
warnings.warn(
"This class is deprecated in favor of 'IndependentSource'", FutureWarning)
return openmc.IndependentSource(*args, **kwargs)
@ -703,6 +713,7 @@ class CompiledSource(SourceBase):
'fissionable', and 'rejection_strategy'.
"""
def __init__(
self,
library: PathLike,
@ -914,7 +925,8 @@ class ParticleType(IntEnum):
try:
return cls[value.upper()]
except KeyError:
raise ValueError(f"Invalid string for creation of {cls.__name__}: {value}")
raise ValueError(
f"Invalid string for creation of {cls.__name__}: {value}")
@classmethod
def from_pdg_number(cls, pdg_number: int) -> ParticleType:
@ -984,6 +996,7 @@ class SourceParticle:
Type of the particle
"""
def __init__(
self,
r: Iterable[float] = (0., 0., 0.),
@ -1042,7 +1055,8 @@ def write_source_file(
openmc.SourceParticle
"""
cv.check_iterable_type("source particles", source_particles, SourceParticle)
cv.check_iterable_type(
"source particles", source_particles, SourceParticle)
pl = ParticleList(source_particles)
pl.export_to_hdf5(filename, **kwargs)
@ -1104,7 +1118,8 @@ class ParticleList(list):
for particle in f.particles:
# Determine particle type based on the PDG number
try:
particle_type = ParticleType.from_pdg_number(particle.pdgcode)
particle_type = ParticleType.from_pdg_number(
particle.pdgcode)
except ValueError:
particle_type = "UNKNOWN"
@ -1241,3 +1256,133 @@ def read_source_file(filename: PathLike) -> ParticleList:
return ParticleList.from_hdf5(filename)
else:
return ParticleList.from_mcpl(filename)
def read_collision_track_hdf5(filename):
"""Read a collision track file in HDF5 format.
Parameters
----------
filename : str or path-like
Path to the HDF5 collision track file.
Returns
-------
numpy.ndarray
Structured array containing collision track data.
See Also
--------
read_collision_track_mcpl
read_collision_track_file
"""
with h5py.File(filename, 'r') as file:
data = file['collision_track_bank'][:]
return data
def read_collision_track_mcpl(file_path):
"""Read a collision track file in MCPL format.
Parameters
----------
file_path : str or path-like
Path to the MCPL collision track file.
Returns
-------
numpy.ndarray
Structured array of particle collision track information, including
position, direction, energy, weight, reaction data, and identifiers.
See Also
--------
read_collision_track_hdf5
read_collision_track_file
"""
import mcpl
myfile = mcpl.MCPLFile(file_path)
data = {
'r': [], # for position (x, y, z)
'u': [], # for direction (ux, uy, uz)
'E': [], 'dE': [], 'time': [],
'wgt': [], 'event_mt': [], 'delayed_group': [],
'cell_id': [], 'nuclide_id': [], 'material_id': [],
'universe_id': [], 'n_collision': [], 'particle': [],
'parent_id': [], 'progeny_id': []
}
# Read and collect data from the MCPL file
for i, p in enumerate(myfile.particles):
if f'blob_{i}' in myfile.blobs:
blob_data = myfile.blobs[f'blob_{i}']
decoded_str = blob_data.decode('utf-8')
pairs = decoded_str.split(';')
values_dict = {k.strip(): v.strip()
for k, v in (pair.split(':') for pair in pairs if pair.strip())}
data['r'].append((p.x, p.y, p.z)) # Append as tuple
data['u'].append((p.ux, p.uy, p.uz)) # Append as tuple
data['E'].append(p.ekin * 1e6)
data['dE'].append(float(values_dict.get('dE', 0)))
data['time'].append(p.time * 1e-3)
data['wgt'].append(p.weight)
data['event_mt'].append(int(values_dict.get('event_mt', 0)))
data['delayed_group'].append(
int(values_dict.get('delayed_group', 0)))
data['cell_id'].append(int(values_dict.get('cell_id', 0)))
data['nuclide_id'].append(int(values_dict.get('nuclide_id', 0)))
data['material_id'].append(int(values_dict.get('material_id', 0)))
data['universe_id'].append(int(values_dict.get('universe_id', 0)))
data['n_collision'].append(int(values_dict.get('n_collision', 0)))
data['particle'].append(ParticleType.from_pdg_number(p.pdgcode))
data['parent_id'].append(int(values_dict.get('parent_id', 0)))
data['progeny_id'].append(int(values_dict.get('progeny_id', 0)))
dtypes = [
('r', [('x', 'f8'), ('y', 'f8'), ('z', 'f8')]),
('u', [('x', 'f8'), ('y', 'f8'), ('z', 'f8')]),
('E', 'f8'), ('dE', 'f8'), ('time', 'f8'), ('wgt', 'f8'),
('event_mt', 'f8'), ('delayed_group', 'i4'), ('cell_id', 'i4'),
('nuclide_id', 'i4'), ('material_id', 'i4'), ('universe_id', 'i4'),
('n_collision', 'i4'), ('particle', 'i4'),
('parent_id', 'i8'), ('progeny_id', 'i8')
]
structured_array = np.zeros(len(data['r']), dtype=dtypes)
for key in data:
structured_array[key] = data[key] # Assign data
return structured_array
def read_collision_track_file(filename):
"""Read a collision track file (HDF5 or MCPL) and return its data.
Parameters
----------
filename : str or path-like
Path to the collision track file to read. Must end with
``.h5`` or ``.mcpl``.
Returns
-------
numpy.ndarray
Structured array containing collision track data.
See Also
--------
read_collision_track_hdf5
read_collision_track_mcpl
"""
filename = Path(filename)
if filename.suffix not in ('.h5', '.mcpl'):
raise ValueError('Collision track file must have a .h5 or .mcpl extension.')
if filename.suffix == '.h5':
return read_collision_track_hdf5(filename)
else:
return read_collision_track_mcpl(filename)

View file

@ -434,6 +434,10 @@ class StatePoint:
if "multiply_density" in group.attrs:
tally.multiply_density = group.attrs["multiply_density"].item() > 0
# Check if tally has higher_moments attribute
if 'higher_moments' in group.attrs:
tally.higher_moments = bool(group.attrs['higher_moments'][()])
# Read the number of realizations
n_realizations = group['n_realizations'][()]

View file

@ -79,6 +79,9 @@ class PolarAzimuthal(UnitSphere):
reference_uvw : Iterable of float
Direction from which polar angle is measured. Defaults to the positive
z-direction.
reference_vwu : Iterable of float
Direction from which azimuthal angle is measured. Defaults to the positive
x-direction.
Attributes
----------
@ -89,8 +92,9 @@ class PolarAzimuthal(UnitSphere):
"""
def __init__(self, mu=None, phi=None, reference_uvw=(0., 0., 1.)):
def __init__(self, mu=None, phi=None, reference_uvw=(0., 0., 1.), reference_vwu=(1., 0., 0.)):
super().__init__(reference_uvw)
self.reference_vwu = reference_vwu
if mu is not None:
self.mu = mu
else:
@ -100,6 +104,20 @@ class PolarAzimuthal(UnitSphere):
self.phi = phi
else:
self.phi = Uniform(0., 2*pi)
@property
def reference_vwu(self):
return self._reference_vwu
@reference_vwu.setter
def reference_vwu(self, vwu):
cv.check_type('reference v direction', vwu, Iterable, Real)
vwu = np.asarray(vwu)
uvw = self.reference_uvw
cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu))
vwu -= vwu.dot(uvw)*uvw
cv.check_less_than('reference v direction must be orthogonal to reference u direction', np.abs(vwu.dot(uvw)), 1e-6)
self._reference_vwu = vwu/np.linalg.norm(vwu)
@property
def mu(self):
@ -132,6 +150,8 @@ class PolarAzimuthal(UnitSphere):
element.set("type", "mu-phi")
if self.reference_uvw is not None:
element.set("reference_uvw", ' '.join(map(str, self.reference_uvw)))
if self.reference_vwu is not None:
element.set("reference_vwu", ' '.join(map(str, self.reference_vwu)))
element.append(self.mu.to_xml_element('mu'))
element.append(self.phi.to_xml_element('phi'))
return element
@ -155,6 +175,9 @@ class PolarAzimuthal(UnitSphere):
uvw = get_elem_list(elem, "reference_uvw", float)
if uvw is not None:
mu_phi.reference_uvw = uvw
vwu = get_elem_list(elem, "reference_vwu", float)
if vwu is not None:
mu_phi.reference_vwu = vwu
mu_phi.mu = Univariate.from_xml_element(elem.find('mu'))
mu_phi.phi = Univariate.from_xml_element(elem.find('phi'))
return mu_phi

View file

@ -295,6 +295,20 @@ class Discrete(Univariate):
"""
return np.sum(self.p)
def mean(self) -> float:
"""Return mean of the discrete distribution
The mean is the weighted average of the discrete values.
.. versionadded:: 0.15.3
Returns
-------
float
Mean of discrete distribution
"""
return np.sum(self.x * self.p) / np.sum(self.p)
def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete:
r"""Remove low-importance points from discrete distribution.
@ -413,6 +427,18 @@ class Uniform(Univariate):
rng = np.random.RandomState(seed)
return rng.uniform(self.a, self.b, n_samples)
def mean(self) -> float:
"""Return mean of the uniform distribution
.. versionadded:: 0.15.3
Returns
-------
float
Mean of uniform distribution
"""
return 0.5 * (self.a + self.b)
def to_xml_element(self, element_name: str):
"""Return XML representation of the uniform distribution
@ -1123,7 +1149,7 @@ class Tabular(Univariate):
"""
interpolation = get_text(elem, 'interpolation')
params = get_elem_list(elem, "parameters", float)
params = get_elem_list(elem, "parameters", float)
m = (len(params) + 1)//2 # +1 for when len(params) is odd
x = params[:m]
p = params[m:]
@ -1347,6 +1373,30 @@ class Mixture(Univariate):
for p, dist in zip(self.probability, self.distribution)
])
def mean(self) -> float:
"""Return mean of the mixture distribution
The mean is the weighted average of the means of the component
distributions, weighted by probability * integral.
.. versionadded:: 0.15.3
Returns
-------
float
Mean of the mixture distribution
"""
# Weight each component by its probability and integral
weights = [p*dist.integral() for p, dist in
zip(self.probability, self.distribution)]
total_weight = sum(weights)
if total_weight == 0:
return 0.0
return sum([w*dist.mean() for w, dist in
zip(weights, self.distribution)]) / total_weight
def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture:
r"""Remove low-importance points / distributions
@ -1369,14 +1419,14 @@ class Mixture(Univariate):
Distribution with low-importance points / distributions removed
"""
# Determine integral of original distribution to compare later
original_integral = self.integral()
# Calculate mean * integral for original distribution to compare later.
original_mean_integral = self.mean() * self.integral()
# Determine indices for any distributions that contribute non-negligibly
# to overall intensity
intensities = [prob*dist.integral() for prob, dist in
zip(self.probability, self.distribution)]
indices = _intensity_clip(intensities, tolerance=tolerance)
# to overall mean * integral
mean_integrals = [prob*dist.mean()*dist.integral() for prob, dist in
zip(self.probability, self.distribution)]
indices = _intensity_clip(mean_integrals, tolerance=tolerance)
# Clip mixture of distributions
probability = self.probability[indices]
@ -1397,12 +1447,14 @@ class Mixture(Univariate):
# Create new distribution
new_dist = type(self)(probability, distribution)
# Show warning if integral of new distribution is not within
# tolerance of original
diff = (original_integral - new_dist.integral())/original_integral
# Show warning if mean * integral of new distribution is not within
# tolerance of original. For energy distributions, mean * integral
# represents total energy.
new_mean_integral = new_dist.mean() * new_dist.integral()
diff = (original_mean_integral - new_mean_integral)/original_mean_integral
if diff > tolerance:
warn("Clipping mixture distribution resulted in an integral that is "
f"lower by a fraction of {diff} when tolerance={tolerance}.")
warn("Clipping mixture distribution resulted in a mean*integral "
f"that is lower by a fraction of {diff} when tolerance={tolerance}.")
return new_dist

View file

@ -3,6 +3,7 @@ from collections.abc import Iterable, MutableSequence
import copy
from functools import partial, reduce, wraps
from itertools import product
from math import sqrt, log
from numbers import Integral, Real
import operator
from pathlib import Path
@ -12,6 +13,7 @@ import h5py
import numpy as np
import pandas as pd
import scipy.sparse as sps
from scipy.stats import chi2, norm
import openmc
import openmc.checkvalue as cv
@ -91,10 +93,20 @@ class Tally(IDManagerMixin):
sum_sq : numpy.ndarray
An array containing the sum of each independent realization squared for
each bin
sum_third : numpy.ndarray
An array containing the sum of each independent realization to the third power for
each bin
sum_fourth : numpy.ndarray
An array containing the sum of each independent realization to the fourth power for
each bin
mean : numpy.ndarray
An array containing the sample mean for each bin
std_dev : numpy.ndarray
An array containing the sample standard deviation for each bin
vov : numpy.ndarray
An array containing the variance of the variance for each tally bin
higher_moments : bool
Whether or not the tally accumulates the sums third and fourth to compute higher-order moments
figure_of_merit : numpy.ndarray
An array containing the figure of merit for each bin
@ -129,8 +141,12 @@ class Tally(IDManagerMixin):
self._sum = None
self._sum_sq = None
self._sum_third = None
self._sum_fourth = None
self._mean = None
self._std_dev = None
self._vov = None
self._higher_moments = False
self._simulation_time = None
self._with_batch_statistics = False
self._derived = False
@ -221,6 +237,15 @@ class Tally(IDManagerMixin):
cv.check_type('multiply density', value, bool)
self._multiply_density = value
@property
def higher_moments(self) -> bool:
return self._higher_moments
@higher_moments.setter
def higher_moments(self, value):
cv.check_type("higher_moments", value, bool)
self._higher_moments = value
@property
def filters(self):
return self._filters
@ -371,6 +396,11 @@ class Tally(IDManagerMixin):
# Update nuclides
nuclide_names = group['nuclides'][()]
self._nuclides = [name.decode().strip() for name in nuclide_names]
# Check for higher_moments attribute
if "higher_moments" in group.attrs:
self._higher_moments = bool(group.attrs["higher_moments"][()])
else:
self._higher_moments = False
# Extract Tally data from the file
data = group['results']
@ -385,10 +415,25 @@ class Tally(IDManagerMixin):
self._sum = sum_
self._sum_sq = sum_sq
if self._higher_moments:
# Extract additional Tally data when higher moments enabled
sum_third = data[:, :, 2]
sum_fourth = data[:, :, 3]
# Reshape the results arrays
sum_third = np.reshape(sum_third, self.shape)
sum_fourth = np.reshape(sum_fourth, self.shape)
# Set the additional data for this Tally
self._sum_third = sum_third
self._sum_fourth = sum_fourth
# Convert NumPy arrays to SciPy sparse LIL matrices
if self.sparse:
self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape)
self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape)
self._sum_third = sps.lil_matrix(self._sum_third.flatten(), self._sum_third.shape)
self._sum_fourth = sps.lil_matrix(self.sum_fourth.flatten(), self._sum_fourth.shape)
# Read simulation time (needed for figure of merit)
self._simulation_time = f["runtime"]["simulation"][()]
@ -428,6 +473,52 @@ class Tally(IDManagerMixin):
cv.check_type('sum_sq', sum_sq, Iterable)
self._sum_sq = sum_sq
@property
@ensure_results
def sum_third(self):
if not self._higher_moments:
raise ValueError(
"Higher moments have not been enabled for this tally. To make "
"higher moments available, set the higher_moments attribute to "
"True before running a simulation."
)
if not self._sp_filename or self.derived:
return None
if self.sparse:
return np.reshape(self._sum_third.toarray(), self.shape)
else:
return self._sum_third
@sum_third.setter
def sum_third(self, sum_third):
cv.check_type("sum_third", sum_third, Iterable)
self._sum_third = sum_third
@property
@ensure_results
def sum_fourth(self):
if not self._higher_moments:
raise ValueError(
"Higher moments have not been enabled for this tally. To make "
"higher moments available, set the higher_moments attribute to "
"True before running a simulation."
)
if not self._sp_filename or self.derived:
return None
if self.sparse:
return np.reshape(self._sum_fourth.toarray(), self.shape)
else:
return self._sum_fourth
@sum_fourth.setter
def sum_fourth(self, sum_fourth):
cv.check_type("sum_fourth", sum_fourth, Iterable)
self._sum_fourth = sum_fourth
@property
def mean(self):
if self._mean is None:
@ -470,14 +561,357 @@ class Tally(IDManagerMixin):
else:
return self._std_dev
@property
def vov(self):
if self._vov is None:
n = self.num_realizations
sum1 = self.sum
sum2 = self.sum_sq
sum3 = self.sum_third
sum4 = self.sum_fourth
self._vov = np.zeros_like(sum1, dtype=float)
# Calculate the variance of the variance (Eq. 2.232 in
# https://doi.org/10.2172/2372634)
numerator = (sum4 - (4.0*sum3*sum1)/n
+ (6.0*sum2*(sum1**2))/(n**2)
- (3.0*(sum1)**4)/(n**3))
denominator = (sum2 - (1.0/n)*(sum1**2))**2
mask = denominator > 0.0
self._vov[mask] = numerator[mask]/denominator[mask] - 1.0/n
if self.sparse:
self._vov = sps.lil_matrix(self._vov.flatten(), self._vov.shape)
if self.sparse:
return np.reshape(self._vov.toarray(), self.shape)
else:
return self._vov
@property
def m2(self):
n = self.num_realizations
return self.sum_sq/n - self.mean**2
@property
def m3(self):
n = self.num_realizations
mean = self.mean
sum2 = self.sum_sq/n
sum3 = self.sum_third/n
return sum3 - 3.0*mean*sum2 + 2.0*mean**3
@property
def m4(self):
n = self.num_realizations
mean = self.mean
sum2 = self.sum_sq/n
sum3 = self.sum_third/n
sum4 = self.sum_fourth/n
return sum4 - 4.0*mean*sum3 + 6.0*(mean**2)*sum2 - 3.0*mean**4
def skew(self, bias=False) -> np.ndarray:
"""Return the sample skewness of each tally bin.
This method computes and returns the unadjusted or adjusted
Fisher-Pearson coefficient of skewness.
Parameters
----------
bias : bool
If False, calculations are corrected for bias and the adjusted
Fisher-Pearson skewness (:math:`G_1`) is returned. If True,
calculations are not corrected for bias and the unadjusted skewness
(:math:`g_1`) is returned.
Returns
-------
float
The skewness of each tally bin
"""
n = self.num_realizations
m2 = self.m2
m3 = self.m3
with np.errstate(divide="ignore", invalid="ignore"):
g1 = np.where(m2 > 0.0, m3/(m2**1.5), 0.0)
if bias:
return g1
else:
if n <= 2:
raise ValueError("Insufficient number of independent realizations"
f"for bias-corrected skewness: need n >= 3, got {n=}.")
else:
return sqrt(n*(n - 1))/(n - 2)*g1
def kurtosis(self, fisher=True, bias=False) -> np.ndarray:
r"""Return the sample kurtosis of each tally bin.
This method computes and returns the sample kurtosis using either
Pearson's or Fisher's definition, with or without finite-sample bias
correction. The value returned depends on the `bias` and `fisher`
arguments as follows:
- **bias=True, fisher=False**: Returns :math:`b_2` (Pearson's kurtosis)
This is the raw fourth standardized moment: :math:`m_4/m_2^2`. For a
normal distribution, :math:`b_2\approx 3`.
- **bias=True, fisher=True**: Returns :math:`g_2` (excess kurtosis) This
is :math:`b_2 - 3`, centered at 0 for normal distributions. Positive
values indicate heavier tails, negative values lighter tails.
- **bias=False, fisher=True** (default): Returns :math:`G_2` (adjusted
excess kurtosis). This applies finite-sample bias correction to
:math:`g_2`. This is the recommended estimator for statistical
inference.
- **bias=False, fisher=False**: Returns bias-corrected Pearson's
kurtosis. This is :math:`G_2 + 3`.
Parameters
----------
fisher : bool, optional
If True (default), Fisher's definition is used (excess kurtosis). If
False, Pearson's definition is used.
bias : bool, optional
If False (default), calculations are corrected for statistical bias
using finite-sample adjustments. If True, calculations use the
biased estimator (population formulas).
Returns
-------
numpy.ndarray
The kurtosis of each tally bin
"""
n = self.num_realizations
m2 = self.m2
m4 = self.m4
with np.errstate(divide="ignore", invalid="ignore"):
b2 = np.where(m2 > 0.0, m4/(m2**2), 0.0)
g2 = b2 - 3.0
if bias:
# Biased estimator (g2 or b2)
return g2 if fisher else b2
else:
# Unbiased estimator with finite-sample correction
if n <= 3:
raise ValueError("Insufficient number of independent realizations"
f"for bias-corrected kurtosis: need n >= 4, got {n=}.")
else:
G2 = ((n - 1)/((n - 2)*(n - 3)))*((n + 1)*g2 + 6.0)
return G2 if fisher else G2 + 3.0
def skewtest(self, alternative: str = "two-sided"):
"""Perform D'Agostino and Pearson's test for skewness.
This method tests the null hypothesis that the skewness of the
population that the sample was drawn from is the same as that of a
corresponding normal distribution.
Parameters
----------
alternative : {'two-sided', 'less', 'greater'}, optional
Defines the alternative hypothesis. The following options are
available:
* 'two-sided': the skewness of the distribution is different from
that of the normal distribution (i.e., non-zero)
* 'less': the skewness of the distribution is less than that of the
normal distribution
* 'greater': the skewness of the distribution is greater than that
of the normal distribution
Returns
-------
statistic : np.ndarray
The computed z-score for the skewness test for each tally bin
pvalue : np.ndarray
The p-value for the hypothesis test for each tally bin
Notes
-----
This test is based on `D'Agostino and Pearson's test
<https://doi.org/10.1093/biomet/60.3.613>`_. The test requires at least
8 realizations to produce valid results.
"""
n = self.num_realizations
if n < 8:
raise ValueError("Skewness test is not well-defined for n < 8.")
g1 = self.skew(bias=True)
# --- Z1 (skewness) ---
y = g1 * sqrt(((n + 1.0)*(n + 3.0))/(6.0*(n - 2.0)))
beta2 = (3.0*(n**2 + 27.0*n - 70.0)*(n + 1.0)*(n + 3.0)
)/((n - 2.0)*(n + 5.0)*(n + 7.0)*(n + 9.0))
W2 = -1.0 + sqrt(2.0*(beta2 - 1.0))
delta = 1.0 / sqrt(log(sqrt(W2)))
alpha = sqrt(2.0 / (W2 - 1.0))
Zb1 = np.where(
y >= 0.0,
delta*np.log((y/alpha) + np.sqrt((y/alpha)**2 + 1.0)),
-delta*np.log((-y/alpha) + np.sqrt((y/alpha)**2 + 1.0))
)
# p-value
if alternative == "two-sided":
p = 2.0 * (1.0 - norm.cdf(np.abs(Zb1)))
elif alternative == "greater":
p = 1.0 - norm.cdf(Zb1)
elif alternative == "less":
p = norm.cdf(Zb1)
else:
raise ValueError("alternative must be 'two-sided', 'greater', or 'less'")
return Zb1, p
def kurtosistest(self, alternative: str = "two-sided"):
"""Perform D'Agostino and Pearson's test for kurtosis.
This method tests the null hypothesis that the kurtosis of the
population that the sample was drawn from is the same as that of a
corresponding normal distribution.
Parameters
----------
alternative : {'two-sided', 'less', 'greater'}, optional
Defines the alternative hypothesis. Default is 'two-sided'. The
following options are available:
* 'two-sided': the kurtosis of the distribution is different from
that of the normal distribution
* 'less': the kurtosis of the distribution is less than that of the
normal distribution
* 'greater': the kurtosis of the distribution is greater than that
of the normal distribution
Returns
-------
statistic : np.ndarray
The computed z-score for the kurtosis test for each tally bin
pvalue : np.ndarray
The p-value for the hypothesis test for each tally bin
Raises
------
ValueError
If the number of realizations is less than 20, or if an invalid
alternative hypothesis is specified.
Notes
-----
This test is based on `D'Agostino and Pearson's test
<https://doi.org/10.1093/biomet/60.3.613>`_. The test is typically
recommended for at least 20 realizations to produce valid results.
"""
n = self.num_realizations
if n < 20:
raise ValueError("Kurtosis test is typically recommended for n >= 20.")
b2 = self.kurtosis(bias=True, fisher=False)
# --- Z2 (kurtosis) ---
mean_b2 = 3.0 * (n - 1.0) / (n + 1.0)
var_b2 = (24.0*n*(n - 2.0)*(n - 3.0)/(
(n + 1.0)**2*(n + 3.0)*(n + 5.0)))
x = (b2 - mean_b2)/np.sqrt(var_b2)
moment = ((6.0*(n**2 - 5.0*n + 2.0))/((n + 7.0)*(n + 9.0))
)*sqrt((6.0*(n + 3.0)*(n + 5.0))/(n*(n - 2.0)*(n - 3.0)))
A = 6.0 + (8.0/moment)*((2.0/moment) + sqrt(1.0 + 4.0/(moment**2)))
Zb2 = (1.0- 2.0/(9.0*A) - ((1.0 - 2.0/A) / (1.0 + (x
)*sqrt(2.0/(A - 4.0))))**(1.0/3.0)) / sqrt(2.0/(9.0*A))
# p-value
if alternative == "two-sided":
p = 2.0 * (1.0 - norm.cdf(np.abs(Zb2)))
elif alternative == "greater":
p = 1.0 - norm.cdf(Zb2)
elif alternative == "less":
p = norm.cdf(Zb2)
else:
raise ValueError("alternative must be 'two-sided', 'greater', or 'less'")
return Zb2, p
def normaltest(self, alternative: str = "two-sided"):
"""Perform D'Agostino and Pearson's omnibus test for normality.
This method tests the null hypothesis that a sample comes from a normal
distribution. It combines skewness and kurtosis to produce an omnibus
test of normality.
Parameters
----------
alternative : {'two-sided', 'less', 'greater'}, optional
Defines the alternative hypothesis used for the component skewness
and kurtosis tests. Default is 'two-sided'. The following options
are available:
* 'two-sided': the distribution is different from normal
* 'less': used for the component tests
* 'greater': used for the component tests
Returns
-------
statistic : np.ndarray
The computed z-score for the normality test for each tally bin
pvalue : np.ndarray
The p-value for the hypothesis test for each tally bin
Raises
------
ValueError
If the number of realizations is less than 20, or if an invalid
alternative hypothesis is specified.
Notes
-----
This test combines a test for skewness and a test for kurtosis to
produce an `omnibus test <https://doi.org/10.1093/biomet/60.3.613>`_.
The test statistic is:
.. math::
K^2 = Z_1^2 + Z_2^2
where :math:`Z_1` is the z-score from the skewness test and :math:`Z_2`
is the z-score from the kurtosis test. This statistic follows a
chi-square distribution with 2 degrees of freedom.
The test requires at least 20 realizations to produce valid results.
"""
n = self.num_realizations
if n < 20:
raise ValueError("normaltest requires n >= 20 (per D'Agostino-Pearson).")
# Use the component tests
Z1, _ = self.skewtest(alternative)
Z2, _ = self.kurtosistest(alternative)
# Combine as chi-square with df=2 since we have skewness and kurtosis
K2 = Z1*Z1 + Z2*Z2
p = chi2.sf(K2, df=2)
return K2, p
@property
def figure_of_merit(self):
mean = self.mean
std_dev = self.std_dev
fom = np.zeros_like(mean)
nonzero = np.abs(mean) > 0
fom[nonzero] = 1.0 / (
(std_dev[nonzero] / mean[nonzero])**2 * self._simulation_time)
rel_err = std_dev[nonzero] / mean[nonzero]
fom[nonzero] = 1.0 / (rel_err**2 * self._simulation_time)
return fom
@property
@ -528,6 +962,12 @@ class Tally(IDManagerMixin):
if self._sum_sq is not None:
self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(),
self._sum_sq.shape)
if self._sum_third is not None:
self._sum_third = sps.lil_matrix(self._sum_third.flatten(),
self._sum_third.shape)
if self._sum_fourth is not None:
self._sum_fourth = sps.lil_matrix(self._sum_fourth.flatten(),
self._sum_fourth.shape)
if self._mean is not None:
self._mean = sps.lil_matrix(self._mean.flatten(),
self._mean.shape)
@ -543,6 +983,10 @@ class Tally(IDManagerMixin):
self._sum = np.reshape(self._sum.toarray(), self.shape)
if self._sum_sq is not None:
self._sum_sq = np.reshape(self._sum_sq.toarray(), self.shape)
if self._sum_third is not None:
self._sum_third = np.reshape(self._sum_third.toarray(), self.shape)
if self._sum_fourth is not None:
self._sum_fourth = np.reshape(self._sum_fourth.toarray(), self.shape)
if self._mean is not None:
self._mean = np.reshape(self._mean.toarray(), self.shape)
if self._std_dev is not None:
@ -869,6 +1313,34 @@ class Tally(IDManagerMixin):
merged_tally._sum_sq = np.reshape(merged_sum_sq, merged_tally.shape)
# Concatenate sum_third arrays if present in both tallies
if self._sum_third is not None and other._sum_third is not None:
self_sum_third = self.get_reshaped_data(value="sum_third")
other_sum_third = other_copy.get_reshaped_data(value="sum_third")
if join_right:
merged_sum_third = np.concatenate((self_sum_third, other_sum_third),
axis=merge_axis)
else:
merged_sum_third = np.concatenate((other_sum_third, self_sum_third),
axis=merge_axis)
merged_tally._sum_third = np.reshape(merged_sum_third, merged_tally.shape)
# Concatenate sum_fourth arrays if present in both tallies
if self._sum_fourth is not None and other._sum_fourth is not None:
self_sum_fourth = self.get_reshaped_data(value="sum_fourth")
other_sum_fourth = other_copy.get_reshaped_data(value="sum_fourth")
if join_right:
merged_sum_fourth = np.concatenate((self_sum_fourth, other_sum_fourth),
axis=merge_axis)
else:
merged_sum_fourth = np.concatenate((other_sum_fourth, self_sum_fourth),
axis=merge_axis)
merged_tally._sum_fourth = np.reshape(merged_sum_fourth, merged_tally.shape)
# Concatenate mean arrays if present in both tallies
if self.mean is not None and other.mean is not None:
self_mean = self.get_reshaped_data(value='mean')
@ -958,6 +1430,11 @@ class Tally(IDManagerMixin):
subelement = ET.SubElement(element, "derivative")
subelement.text = str(self.derivative.id)
# Optional higher moments accumulation
if self.higher_moments:
subelement = ET.SubElement(element, "higher_moments")
subelement.text = str(self.higher_moments).lower()
return element
def add_results(self, statepoint: cv.PathLike | openmc.StatePoint):
@ -984,8 +1461,12 @@ class Tally(IDManagerMixin):
# point are based on the current statepoint file
self._sum = None
self._sum_sq = None
self._sum_third = None
self._sum_fourth = None
self._mean = None
self._std_dev = None
self._vov = None
self._higher_moments = False
self._num_realizations = 0
self._results_read = False
@ -1355,7 +1836,9 @@ class Tally(IDManagerMixin):
(value == 'std_dev' and self.std_dev is None) or \
(value == 'rel_err' and self.mean is None) or \
(value == 'sum' and self.sum is None) or \
(value == 'sum_sq' and self.sum_sq is None):
(value == 'sum_sq' and self.sum_sq is None) or \
(value == "sum_third" and self.sum_third is None) or \
(value == "sum_fourth" and self.sum_fourth is None):
msg = f'The Tally ID="{self.id}" has no data to return'
raise ValueError(msg)
@ -1378,10 +1861,14 @@ class Tally(IDManagerMixin):
data = self.sum[indices]
elif value == 'sum_sq':
data = self.sum_sq[indices]
elif value == "sum_third":
data = self.sum_third[indices]
elif value == "sum_fourth":
data = self.sum_fourth[indices]
else:
msg = f'Unable to return results from Tally ID="{value}" since ' \
f'the requested value "{self.id}" is not \'mean\', ' \
'\'std_dev\', \'rel_err\', \'sum\', or \'sum_sq\''
'\'std_dev\', \'rel_err\', \'sum\', \'sum_sq\', \'sum_third\' or \'sum_fourth\''
raise LookupError(msg)
return data
@ -2711,6 +3198,16 @@ class Tally(IDManagerMixin):
new_sum_sq = self.get_values(scores, filters, filter_bins,
nuclides, 'sum_sq')
new_tally.sum_sq = new_sum_sq
if not self.derived and self._sum_third is not None:
new_sum_third = self.get_values(
scores, filters, filter_bins, nuclides, "sum_third"
)
new_tally._sum_third = new_sum_third
if not self.derived and self._sum_fourth is not None:
new_sum_fourth = self.get_values(
scores, filters, filter_bins, nuclides, "sum_fourth"
)
new_tally._sum_fourth = new_sum_fourth
if self.mean is not None:
new_mean = self.get_values(scores, filters, filter_bins,
nuclides, 'mean')
@ -3151,6 +3648,12 @@ class Tally(IDManagerMixin):
if not self.derived and self.sum_sq is not None:
new_tally._sum_sq = np.zeros(new_tally.shape, dtype=np.float64)
new_tally._sum_sq[diag_indices, :, :] = self.sum_sq
if not self.derived and self._sum_third is not None:
new_tally._sum_third = np.zeros(new_tally.shape, dtype=np.float64)
new_tally._sum_third[diag_indices, :, :] = self.sum_third
if not self.derived and self._sum_fourth is not None:
new_tally._sum_fourth = np.zeros(new_tally.shape, dtype=np.float64)
new_tally._sum_fourth[diag_indices, :, :] = self.sum_fourth
if self.mean is not None:
new_tally._mean = np.zeros(new_tally.shape, dtype=np.float64)
new_tally._mean[diag_indices, :, :] = self.mean

View file

@ -296,16 +296,16 @@ class Tracks(list):
for state in pt.states:
points.InsertNextPoint(state['r'])
# Create VTK line and assign points to line.
n = pt.states.size
line = vtk.vtkPolyLine()
line.GetPointIds().SetNumberOfIds(n)
for i in range(n):
line.GetPointIds().SetId(i, point_offset + i)
point_offset += n
# Create VTK line and assign points to line.
n = pt.states.size
line = vtk.vtkPolyLine()
line.GetPointIds().SetNumberOfIds(n)
for i in range(n):
line.GetPointIds().SetId(i, point_offset + i)
point_offset += n
# Add line to cell array
cells.InsertNextCell(line)
# Add line to cell array
cells.InsertNextCell(line)
data = vtk.vtkPolyData()
data.SetPoints(points)

View file

@ -3,6 +3,8 @@ import os
from pathlib import Path
from tempfile import TemporaryDirectory
import h5py
import openmc
from .checkvalue import PathLike
@ -57,3 +59,20 @@ def input_path(filename: PathLike) -> Path:
return Path(filename).resolve()
else:
return Path(filename)
@contextmanager
def h5py_file_or_group(group_or_filename: PathLike | h5py.Group, *args, **kwargs):
"""Context manager for opening an HDF5 file or using an existing group
Parameters
----------
group_or_filename : path-like or h5py.Group
Path to HDF5 file, or group from an existing HDF5 file
"""
if isinstance(group_or_filename, h5py.Group):
yield group_or_filename
else:
with h5py.File(group_or_filename, *args, **kwargs) as f:
yield f

View file

@ -20,6 +20,8 @@ vector<SourceSite> source_bank;
SharedArray<SourceSite> surf_source_bank;
SharedArray<CollisionTrackSite> collision_track_bank;
// The fission bank is allocated as a SharedArray, rather than a vector, as it
// will be shared by all threads in the simulation. It will be allocated to a
// fixed maximum capacity in the init_fission_bank() function. Then, Elements
@ -50,6 +52,7 @@ void free_memory_bank()
{
simulation::source_bank.clear();
simulation::surf_source_bank.clear();
simulation::collision_track_bank.clear();
simulation::fission_bank.clear();
simulation::progeny_per_particle.clear();
simulation::ifp_source_delayed_group_bank.clear();

238
src/collision_track.cpp Normal file
View file

@ -0,0 +1,238 @@
#include "openmc/collision_track.h"
#include <algorithm>
#include <string>
#include <fmt/format.h>
#include "openmc/bank.h"
#include "openmc/bank_io.h"
#include "openmc/cell.h"
#include "openmc/constants.h"
#include "openmc/error.h"
#include "openmc/file_utils.h"
#include "openmc/hdf5_interface.h"
#include "openmc/material.h"
#include "openmc/mcpl_interface.h"
#include "openmc/message_passing.h"
#include "openmc/nuclide.h"
#include "openmc/output.h"
#include "openmc/particle.h"
#include "openmc/settings.h"
#include "openmc/simulation.h"
#include "openmc/universe.h"
#ifdef OPENMC_MPI
#include <mpi.h>
#endif
namespace openmc {
namespace {
hid_t h5_collision_track_banktype()
{
hid_t postype = H5Tcreate(H5T_COMPOUND, sizeof(Position));
H5Tinsert(postype, "x", HOFFSET(Position, x), H5T_NATIVE_DOUBLE);
H5Tinsert(postype, "y", HOFFSET(Position, y), H5T_NATIVE_DOUBLE);
H5Tinsert(postype, "z", HOFFSET(Position, z), H5T_NATIVE_DOUBLE);
hid_t banktype = H5Tcreate(H5T_COMPOUND, sizeof(CollisionTrackSite));
H5Tinsert(banktype, "r", HOFFSET(CollisionTrackSite, r), postype);
H5Tinsert(banktype, "u", HOFFSET(CollisionTrackSite, u), postype);
H5Tinsert(banktype, "E", HOFFSET(CollisionTrackSite, E), H5T_NATIVE_DOUBLE);
H5Tinsert(banktype, "dE", HOFFSET(CollisionTrackSite, dE), H5T_NATIVE_DOUBLE);
H5Tinsert(
banktype, "time", HOFFSET(CollisionTrackSite, time), H5T_NATIVE_DOUBLE);
H5Tinsert(
banktype, "wgt", HOFFSET(CollisionTrackSite, wgt), H5T_NATIVE_DOUBLE);
H5Tinsert(banktype, "event_mt", HOFFSET(CollisionTrackSite, event_mt),
H5T_NATIVE_INT);
H5Tinsert(banktype, "delayed_group",
HOFFSET(CollisionTrackSite, delayed_group), H5T_NATIVE_INT);
H5Tinsert(
banktype, "cell_id", HOFFSET(CollisionTrackSite, cell_id), H5T_NATIVE_INT);
H5Tinsert(banktype, "nuclide_id", HOFFSET(CollisionTrackSite, nuclide_id),
H5T_NATIVE_INT);
H5Tinsert(banktype, "material_id", HOFFSET(CollisionTrackSite, material_id),
H5T_NATIVE_INT);
H5Tinsert(banktype, "universe_id", HOFFSET(CollisionTrackSite, universe_id),
H5T_NATIVE_INT);
H5Tinsert(banktype, "n_collision", HOFFSET(CollisionTrackSite, n_collision),
H5T_NATIVE_INT);
H5Tinsert(banktype, "particle", HOFFSET(CollisionTrackSite, particle),
H5T_NATIVE_INT);
H5Tinsert(banktype, "parent_id", HOFFSET(CollisionTrackSite, parent_id),
H5T_NATIVE_INT64);
H5Tinsert(banktype, "progeny_id", HOFFSET(CollisionTrackSite, progeny_id),
H5T_NATIVE_INT64);
H5Tclose(postype);
return banktype;
}
void write_collision_track_bank(hid_t group_id,
openmc::span<CollisionTrackSite> collision_track_bank,
const openmc::vector<int64_t>& bank_index)
{
hid_t banktype = h5_collision_track_banktype();
#ifdef OPENMC_MPI
write_bank_dataset("collision_track_bank", group_id, collision_track_bank,
bank_index, banktype, mpi::collision_track_site);
#else
write_bank_dataset("collision_track_bank", group_id, collision_track_bank,
bank_index, banktype);
#endif
H5Tclose(banktype);
}
void write_h5_collision_track(const char* filename,
openmc::span<CollisionTrackSite> collision_track_bank,
const openmc::vector<int64_t>& bank_index)
{
#ifdef PHDF5
bool parallel = true;
#else
bool parallel = false;
#endif
if (!filename)
fatal_error("write_h5_collision_track filename needs a nonempty name.");
std::string filename_(filename);
const auto extension = get_file_extension(filename_);
if (extension.empty()) {
filename_.append(".h5");
} else if (extension != "h5") {
warning("write_h5_collision_track was passed a file extension differing "
"from .h5, but an hdf5 file will be written.");
}
hid_t file_id;
if (mpi::master || parallel) {
file_id = file_open(filename_.c_str(), 'w', true);
// Write filetype and version info
write_attribute(file_id, "filetype", "collision_track");
write_attribute(file_id, "version", VERSION_COLLISION_TRACK);
}
write_collision_track_bank(file_id, collision_track_bank, bank_index);
if (mpi::master || parallel)
file_close(file_id);
}
} // namespace
bool should_record_event(int id_cell, int mt_event, const std::string& nuclide,
int id_universe, int id_material, double energy_loss)
{
auto matches_filter = [](const auto& filter_set, const auto& value) {
return filter_set.empty() || filter_set.count(value) > 0;
};
const auto& cfg = settings::collision_track_config;
return simulation::current_batch > settings::n_inactive &&
!simulation::collision_track_bank.full() &&
matches_filter(cfg.cell_ids, id_cell) &&
matches_filter(cfg.mt_numbers, mt_event) &&
matches_filter(cfg.universe_ids, id_universe) &&
matches_filter(cfg.material_ids, id_material) &&
matches_filter(cfg.nuclides, nuclide) &&
(cfg.deposited_energy_threshold == 0 ||
cfg.deposited_energy_threshold < energy_loss);
}
void collision_track_reserve_bank()
{
simulation::collision_track_bank.reserve(
settings::collision_track_config.max_collisions);
}
void collision_track_flush_bank()
{
const auto& cfg = settings::collision_track_config;
if (simulation::ct_current_file > cfg.max_files)
return;
bool last_batch = (simulation::current_batch == settings::n_batches);
if (!simulation::collision_track_bank.full() && !last_batch)
return;
auto size = simulation::collision_track_bank.size();
if (size == 0 && !last_batch)
return;
auto collision_track_work_index = mpi::calculate_parallel_index_vector(size);
openmc::span<CollisionTrackSite> collisiontrackbankspan(
simulation::collision_track_bank.begin(), size);
std::string ext = cfg.mcpl_write ? "mcpl" : "h5";
auto filename = fmt::format("{}collision_track.{}.{}", settings::path_output,
simulation::ct_current_file, ext);
if (cfg.max_files == 1 || (simulation::ct_current_file == 1 && last_batch)) {
filename = settings::path_output + "collision_track." + ext;
}
write_message("Creating {}...", filename, 4);
if (cfg.mcpl_write) {
write_mcpl_collision_track(
filename.c_str(), collisiontrackbankspan, collision_track_work_index);
} else {
write_h5_collision_track(
filename.c_str(), collisiontrackbankspan, collision_track_work_index);
}
simulation::collision_track_bank.clear();
if (!last_batch && cfg.max_files >= 1) {
collision_track_reserve_bank();
}
++simulation::ct_current_file;
}
void collision_track_record(Particle& particle)
{
int cell_index = particle.lowest_coord().cell();
if (cell_index == C_NONE)
return;
int cell_id = model::cells[cell_index]->id_;
const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get();
std::string nuclide = nuclide_ptr->name_;
int universe_id = model::universes[particle.lowest_coord().universe()]->id_;
double delta_E = particle.E_last() - particle.E();
int material_index = particle.material();
if (material_index == C_NONE)
return;
int material_id = model::materials[material_index]->id_;
if (!should_record_event(cell_id, particle.event_mt(), nuclide, universe_id,
material_id, delta_E))
return;
CollisionTrackSite site;
site.r = particle.r();
site.u = particle.u();
site.E = particle.E_last();
site.dE = delta_E;
site.time = particle.time();
site.wgt = particle.wgt();
site.event_mt = particle.event_mt();
site.delayed_group = particle.delayed_group();
site.cell_id = cell_id;
site.nuclide_id =
10000 * nuclide_ptr->Z_ + 10 * nuclide_ptr->A_ + nuclide_ptr->metastable_;
site.material_id = material_id;
site.universe_id = universe_id;
site.n_collision = particle.n_collision();
site.particle = particle.type();
site.parent_id = particle.id();
site.progeny_id = particle.n_progeny();
simulation::collision_track_bank.thread_safe_append(site);
}
} // namespace openmc

View file

@ -58,6 +58,15 @@ PolarAzimuthal::PolarAzimuthal(Direction u, UPtrDist mu, UPtrDist phi)
PolarAzimuthal::PolarAzimuthal(pugi::xml_node node)
: UnitSphereDistribution {node}
{
// Read reference directional unit vector
if (check_for_node(node, "reference_vwu")) {
auto v_ref = get_node_array<double>(node, "reference_vwu");
if (v_ref.size() != 3)
fatal_error("Angular distribution reference v direction must have "
"three parameters specified.");
v_ref_ = Direction(v_ref.data());
}
w_ref_ = u_ref_.cross(v_ref_);
if (check_for_node(node, "mu")) {
pugi::xml_node node_dist = node.child("mu");
mu_ = distribution_from_xml(node_dist);
@ -79,11 +88,15 @@ Direction PolarAzimuthal::sample(uint64_t* seed) const
double mu = mu_->sample(seed);
if (mu == 1.0)
return u_ref_;
if (mu == -1.0)
return -u_ref_;
// Sample azimuthal angle
double phi = phi_->sample(seed);
return rotate_angle(u_ref_, mu, &phi, seed);
double f = std::sqrt(1 - mu * mu);
return mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_;
}
//==============================================================================

View file

@ -3,6 +3,7 @@
#include "openmc/bank.h"
#include "openmc/capi.h"
#include "openmc/cmfd_solver.h"
#include "openmc/collision_track.h"
#include "openmc/constants.h"
#include "openmc/cross_sections.h"
#include "openmc/dagmc.h"
@ -76,6 +77,7 @@ int openmc_finalize()
// Reset global variables
settings::assume_separate = false;
settings::check_overlaps = false;
settings::collision_track_config = CollisionTrackConfig {};
settings::confidence_intervals = false;
settings::create_fission_neutrons = true;
settings::create_delayed_neutrons = true;
@ -177,6 +179,9 @@ int openmc_finalize()
if (mpi::source_site != MPI_DATATYPE_NULL) {
MPI_Type_free(&mpi::source_site);
}
if (mpi::collision_track_site != MPI_DATATYPE_NULL) {
MPI_Type_free(&mpi::collision_track_site);
}
#endif
openmc_reset_random_ray();

View file

@ -536,14 +536,14 @@ void read_complex(
H5Tclose(complex_id);
}
void read_tally_results(
hid_t group_id, hsize_t n_filter, hsize_t n_score, double* results)
void read_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score,
hsize_t n_results, double* results)
{
// Create dataspace for hyperslab in memory
constexpr int ndim = 3;
hsize_t dims[ndim] {n_filter, n_score, 3};
hsize_t dims[ndim] {n_filter, n_score, n_results};
hsize_t start[ndim] {0, 0, 1};
hsize_t count[ndim] {n_filter, n_score, 2};
hsize_t count[ndim] {n_filter, n_score, n_results - 1};
hid_t memspace = H5Screate_simple(ndim, dims, nullptr);
H5Sselect_hyperslab(memspace, H5S_SELECT_SET, start, nullptr, count, nullptr);
@ -686,15 +686,15 @@ void write_string(
group_id, 0, nullptr, buffer.length(), name, buffer.c_str(), indep);
}
void write_tally_results(
hid_t group_id, hsize_t n_filter, hsize_t n_score, const double* results)
void write_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score,
hsize_t n_results, const double* results)
{
// Set dimensions of sum/sum_sq hyperslab to store
constexpr int ndim = 3;
hsize_t count[ndim] {n_filter, n_score, 2};
hsize_t count[ndim] {n_filter, n_score, n_results - 1};
// Set dimensions of results array
hsize_t dims[ndim] {n_filter, n_score, 3};
hsize_t dims[ndim] {n_filter, n_score, n_results};
hsize_t start[ndim] {0, 0, 1};
hid_t memspace = H5Screate_simple(ndim, dims, nullptr);
H5Sselect_hyperslab(memspace, H5S_SELECT_SET, start, nullptr, count, nullptr);

View file

@ -178,6 +178,37 @@ void initialize_mpi(MPI_Comm intracomm)
MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG};
MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site);
MPI_Type_commit(&mpi::source_site);
CollisionTrackSite bc;
MPI_Aint dispc[16];
MPI_Get_address(&bc.r, &dispc[0]); // double
MPI_Get_address(&bc.u, &dispc[1]); // double
MPI_Get_address(&bc.E, &dispc[2]); // double
MPI_Get_address(&bc.dE, &dispc[3]); // double
MPI_Get_address(&bc.time, &dispc[4]); // double
MPI_Get_address(&bc.wgt, &dispc[5]); // double
MPI_Get_address(&bc.event_mt, &dispc[6]); // int
MPI_Get_address(&bc.delayed_group, &dispc[7]); // int
MPI_Get_address(&bc.cell_id, &dispc[8]); // int
MPI_Get_address(&bc.nuclide_id, &dispc[9]); // int
MPI_Get_address(&bc.material_id, &dispc[10]); // int
MPI_Get_address(&bc.universe_id, &dispc[11]); // int
MPI_Get_address(&bc.n_collision, &dispc[12]); // int
MPI_Get_address(&bc.particle, &dispc[13]); // int
MPI_Get_address(&bc.parent_id, &dispc[14]); // int64_t
MPI_Get_address(&bc.progeny_id, &dispc[15]); // int64_t
for (int i = 15; i >= 0; --i) {
dispc[i] -= dispc[0];
}
int blocksc[] = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
MPI_Datatype typesc[] = {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE,
MPI_DOUBLE, MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_INT,
MPI_INT, MPI_INT, MPI_INT, MPI_INT64_T, MPI_INT64_T};
MPI_Type_create_struct(
16, blocksc, dispc, typesc, &mpi::collision_track_site);
MPI_Type_commit(&mpi::collision_track_site);
}
#endif // OPENMC_MPI

View file

@ -18,6 +18,7 @@
#include <cstring>
#include <memory>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
@ -61,6 +62,8 @@ using mcpl_hdr_nparticles_fpt = uint64_t (*)(mcpl_file_t* file_handle);
using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle);
using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle);
using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* file_handle,
const char* key, int32_t ldata, const char* data);
using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename);
using mcpl_hdr_set_srcname_fpt = void (*)(
mcpl_outfile_t* outfile_handle, const char* srcname);
@ -110,6 +113,7 @@ struct McplApi {
mcpl_close_file_fpt close_file;
mcpl_create_outfile_fpt create_outfile;
mcpl_hdr_set_srcname_fpt hdr_set_srcname;
mcpl_hdr_add_data_fpt hdr_add_data;
mcpl_add_particle_fpt add_particle;
mcpl_close_outfile_fpt close_outfile;
mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum;
@ -146,6 +150,8 @@ struct McplApi {
load_symbol_platform("mcpl_create_outfile"));
hdr_set_srcname = reinterpret_cast<mcpl_hdr_set_srcname_fpt>(
load_symbol_platform("mcpl_hdr_set_srcname"));
hdr_add_data = reinterpret_cast<mcpl_hdr_add_data_fpt>(
load_symbol_platform("mcpl_hdr_add_data"));
add_particle = reinterpret_cast<mcpl_add_particle_fpt>(
load_symbol_platform("mcpl_add_particle"));
close_outfile = reinterpret_cast<mcpl_close_outfile_fpt>(
@ -545,4 +551,153 @@ void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
}
}
// Collision track feature with MCPL
void write_mcpl_collision_track_internal(mcpl_outfile_t* file_id,
span<CollisionTrackSite> collision_track_bank,
const vector<int64_t>& bank_index_all_ranks)
{
if (mpi::master) {
if (!file_id) {
fatal_error("MCPL: Internal error - master rank called "
"write_mcpl_source_bank_internal with null file_id.");
}
vector<CollisionTrackSite> receive_buffer;
vector<CollisionTrackSite> all_sites;
all_sites.reserve(static_cast<size_t>(bank_index_all_ranks.back()));
vector<std::string> all_blobs;
all_blobs.reserve(static_cast<size_t>(bank_index_all_ranks.back()));
for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
size_t num_sites_on_rank = static_cast<size_t>(
bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
if (num_sites_on_rank == 0)
continue;
span<const CollisionTrackSite> sites_to_process;
#ifdef OPENMC_MPI
if (rank_idx == mpi::rank) {
sites_to_process = openmc::span<const CollisionTrackSite>(
collision_track_bank.data(), num_sites_on_rank);
} else {
receive_buffer.resize(num_sites_on_rank);
MPI_Recv(receive_buffer.data(), num_sites_on_rank,
mpi::collision_track_site, rank_idx, rank_idx, mpi::intracomm,
MPI_STATUS_IGNORE);
sites_to_process = openmc::span<const CollisionTrackSite>(
receive_buffer.data(), num_sites_on_rank);
}
#else
sites_to_process = openmc::span<const CollisionTrackSite>(
collision_track_bank.data(), num_sites_on_rank);
#endif
for (const auto& site : sites_to_process) {
std::ostringstream custom_data_stream;
custom_data_stream << " dE : " << site.dE
<< " ; event_mt : " << site.event_mt
<< " ; delayed_group : " << site.delayed_group
<< " ; cell_id : " << site.cell_id
<< " ; nuclide_id : " << site.nuclide_id
<< " ; material_id : " << site.material_id
<< " ; universe_id : " << site.universe_id
<< " ; n_collision : " << site.n_collision
<< " ; parent_id : " << site.parent_id
<< " ; progeny_id : " << site.progeny_id;
all_blobs.push_back(custom_data_stream.str());
all_sites.push_back(site);
}
}
for (size_t idx = 0; idx < all_blobs.size(); ++idx) {
const auto& blob = all_blobs[idx];
std::string key = "blob_" + std::to_string(idx);
g_mcpl_api->hdr_add_data(file_id, key.c_str(), blob.size(), blob.c_str());
}
for (const auto& site : all_sites) {
mcpl_particle_repr_t p_repr {};
p_repr.position[0] = site.r.x;
p_repr.position[1] = site.r.y;
p_repr.position[2] = site.r.z;
p_repr.direction[0] = site.u.x;
p_repr.direction[1] = site.u.y;
p_repr.direction[2] = site.u.z;
p_repr.ekin = site.E * 1e-6;
p_repr.time = site.time * 1e3;
p_repr.weight = site.wgt;
switch (site.particle) {
case ParticleType::neutron:
p_repr.pdgcode = 2112;
break;
case ParticleType::photon:
p_repr.pdgcode = 22;
break;
case ParticleType::electron:
p_repr.pdgcode = 11;
break;
case ParticleType::positron:
p_repr.pdgcode = -11;
break;
default:
continue;
}
g_mcpl_api->add_particle(file_id, &p_repr);
}
} else {
#ifdef OPENMC_MPI
if (!collision_track_bank.empty()) {
MPI_Send(collision_track_bank.data(), collision_track_bank.size(),
mpi::collision_track_site, 0, mpi::rank, mpi::intracomm);
}
#endif
}
}
void write_mcpl_collision_track(const char* filename,
span<CollisionTrackSite> collision_track_bank,
const vector<int64_t>& bank_index)
{
ensure_mcpl_ready_or_fatal();
std::string filename_(filename);
const auto extension = get_file_extension(filename_);
if (extension.empty()) {
filename_.append(".mcpl");
} else if (extension != "mcpl") {
warning(fmt::format("Specified filename '{}' has an extension '.{}', but "
"an MCPL file (.mcpl) will be written using this name.",
filename, extension));
}
mcpl_outfile_t* file_id = nullptr;
if (mpi::master) {
file_id = g_mcpl_api->create_outfile(filename_.c_str());
if (!file_id) {
fatal_error(fmt::format(
"MCPL: Failed to create output file '{}'. Check permissions and path.",
filename_));
}
std::string src_line;
if (VERSION_DEV) {
src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
} else {
src_line = fmt::format(
"OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
}
g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
}
write_mcpl_collision_track_internal(
file_id, collision_track_bank, bank_index);
if (mpi::master) {
if (file_id) {
g_mcpl_api->close_outfile(file_id);
}
}
}
} // namespace openmc

View file

@ -230,6 +230,42 @@ void MaterialVolumes::add_volume_unsafe(
// Mesh implementation
//==============================================================================
template<typename T>
const std::unique_ptr<Mesh>& Mesh::create(
T dataset, const std::string& mesh_type, const std::string& mesh_library)
{
// Determine mesh type. Add to model vector and map
if (mesh_type == RegularMesh::mesh_type) {
model::meshes.push_back(make_unique<RegularMesh>(dataset));
} else if (mesh_type == RectilinearMesh::mesh_type) {
model::meshes.push_back(make_unique<RectilinearMesh>(dataset));
} else if (mesh_type == CylindricalMesh::mesh_type) {
model::meshes.push_back(make_unique<CylindricalMesh>(dataset));
} else if (mesh_type == SphericalMesh::mesh_type) {
model::meshes.push_back(make_unique<SphericalMesh>(dataset));
#ifdef OPENMC_DAGMC_ENABLED
} else if (mesh_type == UnstructuredMesh::mesh_type &&
mesh_library == MOABMesh::mesh_lib_type) {
model::meshes.push_back(make_unique<MOABMesh>(dataset));
#endif
#ifdef OPENMC_LIBMESH_ENABLED
} else if (mesh_type == UnstructuredMesh::mesh_type &&
mesh_library == LibMesh::mesh_lib_type) {
model::meshes.push_back(make_unique<LibMesh>(dataset));
#endif
} else if (mesh_type == UnstructuredMesh::mesh_type) {
fatal_error("Unstructured mesh support is not enabled or the mesh "
"library is invalid.");
} else {
fatal_error(fmt::format("Invalid mesh type: {}", mesh_type));
}
// Map ID to position in vector
model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1;
return model::meshes.back();
}
Mesh::Mesh(pugi::xml_node node)
{
// Read mesh id
@ -238,6 +274,17 @@ Mesh::Mesh(pugi::xml_node node)
name_ = get_node_value(node, "name");
}
Mesh::Mesh(hid_t group)
{
// Read mesh ID
read_attribute(group, "id", id_);
// Read mesh name
if (object_exists(group, "name")) {
read_dataset(group, "name", name_);
}
}
void Mesh::set_id(int32_t id)
{
assert(id >= 0 || id == C_NONE);
@ -265,7 +312,13 @@ void Mesh::set_id(int32_t id)
// Update ID and entry in the mesh map
id_ = id;
model::mesh_map[id] = model::meshes.size() - 1;
// find the index of this mesh in the model::meshes vector
// (search in reverse because this mesh was likely just added to the vector)
auto it = std::find_if(model::meshes.rbegin(), model::meshes.rend(),
[this](const std::unique_ptr<Mesh>& mesh) { return mesh.get() == this; });
model::mesh_map[id] = std::distance(model::meshes.begin(), it.base()) - 1;
}
vector<double> Mesh::volumes() const
@ -627,6 +680,46 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node)
}
}
UnstructuredMesh::UnstructuredMesh(hid_t group) : Mesh(group)
{
n_dimension_ = 3;
// check the mesh type
if (object_exists(group, "type")) {
std::string temp;
read_dataset(group, "type", temp);
if (temp != mesh_type) {
fatal_error(fmt::format("Invalid mesh type: {}", temp));
}
}
// check if a length unit multiplier was specified
if (object_exists(group, "length_multiplier")) {
read_dataset(group, "length_multiplier", length_multiplier_);
}
// get the filename of the unstructured mesh to load
if (object_exists(group, "filename")) {
read_dataset(group, "filename", filename_);
if (!file_exists(filename_)) {
fatal_error("Mesh file '" + filename_ + "' does not exist!");
}
} else {
fatal_error(fmt::format(
"No filename supplied for unstructured mesh with ID: {}", id_));
}
if (attribute_exists(group, "options")) {
read_attribute(group, "options", options_);
}
// check if mesh tally data should be written with
// statepoint files
if (attribute_exists(group, "output")) {
read_attribute(group, "output", output_);
}
}
void UnstructuredMesh::determine_bounds()
{
double xmin = INFTY;
@ -1086,6 +1179,72 @@ void StructuredMesh::surface_bins_crossed(
// RegularMesh implementation
//==============================================================================
int RegularMesh::set_grid()
{
auto shape = xt::adapt(shape_, {n_dimension_});
// Check that dimensions are all greater than zero
if (xt::any(shape <= 0)) {
set_errmsg("All entries for a regular mesh dimensions "
"must be positive.");
return OPENMC_E_INVALID_ARGUMENT;
}
// Make sure lower_left and dimension match
if (lower_left_.size() != n_dimension_) {
set_errmsg("Number of entries in lower_left must be the same "
"as the regular mesh dimensions.");
return OPENMC_E_INVALID_ARGUMENT;
}
if (width_.size() > 0) {
// Check to ensure width has same dimensions
if (width_.size() != n_dimension_) {
set_errmsg("Number of entries on width must be the same as "
"the regular mesh dimensions.");
return OPENMC_E_INVALID_ARGUMENT;
}
// Check for negative widths
if (xt::any(width_ < 0.0)) {
set_errmsg("Cannot have a negative width on a regular mesh.");
return OPENMC_E_INVALID_ARGUMENT;
}
// Set width and upper right coordinate
upper_right_ = xt::eval(lower_left_ + shape * width_);
} else if (upper_right_.size() > 0) {
// Check to ensure upper_right_ has same dimensions
if (upper_right_.size() != n_dimension_) {
set_errmsg("Number of entries on upper_right must be the "
"same as the regular mesh dimensions.");
return OPENMC_E_INVALID_ARGUMENT;
}
// Check that upper-right is above lower-left
if (xt::any(upper_right_ < lower_left_)) {
set_errmsg(
"The upper_right coordinates of a regular mesh must be greater than "
"the lower_left coordinates.");
return OPENMC_E_INVALID_ARGUMENT;
}
// Set width
width_ = xt::eval((upper_right_ - lower_left_) / shape);
}
// Set material volumes
volume_frac_ = 1.0 / xt::prod(shape)();
element_volume_ = 1.0;
for (int i = 0; i < n_dimension_; i++) {
element_volume_ *= width_[i];
}
return 0;
}
RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node}
{
// Determine number of dimensions for mesh
@ -1100,12 +1259,6 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node}
}
std::copy(shape.begin(), shape.end(), shape_.begin());
// Check that dimensions are all greater than zero
if (xt::any(shape <= 0)) {
fatal_error("All entries on the <dimension> element for a tally "
"mesh must be positive.");
}
// Check for lower-left coordinates
if (check_for_node(node, "lower_left")) {
// Read mesh lower-left corner location
@ -1114,12 +1267,6 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node}
fatal_error("Must specify <lower_left> on a mesh.");
}
// Make sure lower_left and dimension match
if (shape.size() != lower_left_.size()) {
fatal_error("Number of entries on <lower_left> must be the same "
"as the number of entries on <dimension>.");
}
if (check_for_node(node, "width")) {
// Make sure one of upper-right or width were specified
if (check_for_node(node, "upper_right")) {
@ -1128,49 +1275,52 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node}
width_ = get_node_xarray<double>(node, "width");
// Check to ensure width has same dimensions
auto n = width_.size();
if (n != lower_left_.size()) {
fatal_error("Number of entries on <width> must be the same as "
"the number of entries on <lower_left>.");
}
// Check for negative widths
if (xt::any(width_ < 0.0)) {
fatal_error("Cannot have a negative <width> on a tally mesh.");
}
// Set width and upper right coordinate
upper_right_ = xt::eval(lower_left_ + shape * width_);
} else if (check_for_node(node, "upper_right")) {
upper_right_ = get_node_xarray<double>(node, "upper_right");
// Check to ensure width has same dimensions
auto n = upper_right_.size();
if (n != lower_left_.size()) {
fatal_error("Number of entries on <upper_right> must be the "
"same as the number of entries on <lower_left>.");
}
// Check that upper-right is above lower-left
if (xt::any(upper_right_ < lower_left_)) {
fatal_error("The <upper_right> coordinates must be greater than "
"the <lower_left> coordinates on a tally mesh.");
}
// Set width
width_ = xt::eval((upper_right_ - lower_left_) / shape);
} else {
fatal_error("Must specify either <upper_right> or <width> on a mesh.");
}
// Set material volumes
volume_frac_ = 1.0 / xt::prod(shape)();
if (int err = set_grid()) {
fatal_error(openmc_err_msg);
}
}
element_volume_ = 1.0;
for (int i = 0; i < n_dimension_; i++) {
element_volume_ *= width_[i];
RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group}
{
// Determine number of dimensions for mesh
if (!object_exists(group, "dimension")) {
fatal_error("Must specify <dimension> on a regular mesh.");
}
xt::xtensor<int, 1> shape;
read_dataset(group, "dimension", shape);
int n = n_dimension_ = shape.size();
if (n != 1 && n != 2 && n != 3) {
fatal_error("Mesh must be one, two, or three dimensions.");
}
std::copy(shape.begin(), shape.end(), shape_.begin());
// Check for lower-left coordinates
if (object_exists(group, "lower_left")) {
// Read mesh lower-left corner location
read_dataset(group, "lower_left", lower_left_);
} else {
fatal_error("Must specify lower_left dataset on a mesh.");
}
if (object_exists(group, "upper_right")) {
read_dataset(group, "upper_right", upper_right_);
} else {
fatal_error("Must specify either upper_right dataset on a mesh.");
}
if (int err = set_grid()) {
fatal_error(openmc_err_msg);
}
}
@ -1343,6 +1493,19 @@ RectilinearMesh::RectilinearMesh(pugi::xml_node node) : StructuredMesh {node}
}
}
RectilinearMesh::RectilinearMesh(hid_t group) : StructuredMesh {group}
{
n_dimension_ = 3;
read_dataset(group, "x_grid", grid_[0]);
read_dataset(group, "y_grid", grid_[1]);
read_dataset(group, "z_grid", grid_[2]);
if (int err = set_grid()) {
fatal_error(openmc_err_msg);
}
}
const std::string RectilinearMesh::mesh_type = "rectilinear";
std::string RectilinearMesh::get_mesh_type() const
@ -1478,6 +1641,19 @@ CylindricalMesh::CylindricalMesh(pugi::xml_node node)
}
}
CylindricalMesh::CylindricalMesh(hid_t group) : PeriodicStructuredMesh {group}
{
n_dimension_ = 3;
read_dataset(group, "r_grid", grid_[0]);
read_dataset(group, "phi_grid", grid_[1]);
read_dataset(group, "z_grid", grid_[2]);
read_dataset(group, "origin", origin_);
if (int err = set_grid()) {
fatal_error(openmc_err_msg);
}
}
const std::string CylindricalMesh::mesh_type = "cylindrical";
std::string CylindricalMesh::get_mesh_type() const
@ -1756,6 +1932,20 @@ SphericalMesh::SphericalMesh(pugi::xml_node node)
}
}
SphericalMesh::SphericalMesh(hid_t group) : PeriodicStructuredMesh {group}
{
n_dimension_ = 3;
read_dataset(group, "r_grid", grid_[0]);
read_dataset(group, "theta_grid", grid_[1]);
read_dataset(group, "phi_grid", grid_[2]);
read_dataset(group, "origin", origin_);
if (int err = set_grid()) {
fatal_error(openmc_err_msg);
}
}
const std::string SphericalMesh::mesh_type = "spherical";
std::string SphericalMesh::get_mesh_type() const
@ -2520,6 +2710,11 @@ MOABMesh::MOABMesh(pugi::xml_node node) : UnstructuredMesh(node)
initialize();
}
MOABMesh::MOABMesh(hid_t group) : UnstructuredMesh(group)
{
initialize();
}
MOABMesh::MOABMesh(const std::string& filename, double length_multiplier)
: UnstructuredMesh()
{
@ -3228,6 +3423,15 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node)
initialize();
}
LibMesh::LibMesh(hid_t group) : UnstructuredMesh(group)
{
// filename_ and length_multiplier_ will already be set by the
// UnstructuredMesh constructor
set_mesh_pointer_from_filename(filename_);
set_length_multiplier(length_multiplier_);
initialize();
}
// create the mesh from a pointer to a libMesh Mesh
LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier)
{
@ -3618,34 +3822,51 @@ void read_meshes(pugi::xml_node root)
mesh_lib = get_node_value(node, "library", true, true);
}
// Read mesh and add to vector
if (mesh_type == RegularMesh::mesh_type) {
model::meshes.push_back(make_unique<RegularMesh>(node));
} else if (mesh_type == RectilinearMesh::mesh_type) {
model::meshes.push_back(make_unique<RectilinearMesh>(node));
} else if (mesh_type == CylindricalMesh::mesh_type) {
model::meshes.push_back(make_unique<CylindricalMesh>(node));
} else if (mesh_type == SphericalMesh::mesh_type) {
model::meshes.push_back(make_unique<SphericalMesh>(node));
#ifdef OPENMC_DAGMC_ENABLED
} else if (mesh_type == UnstructuredMesh::mesh_type &&
mesh_lib == MOABMesh::mesh_lib_type) {
model::meshes.push_back(make_unique<MOABMesh>(node));
#endif
#ifdef OPENMC_LIBMESH_ENABLED
} else if (mesh_type == UnstructuredMesh::mesh_type &&
mesh_lib == LibMesh::mesh_lib_type) {
model::meshes.push_back(make_unique<LibMesh>(node));
#endif
} else if (mesh_type == UnstructuredMesh::mesh_type) {
fatal_error("Unstructured mesh support is not enabled or the mesh "
"library is invalid.");
} else {
fatal_error("Invalid mesh type: " + mesh_type);
Mesh::create(node, mesh_type, mesh_lib);
}
}
void read_meshes(hid_t group)
{
std::unordered_set<int> mesh_ids;
std::vector<int> ids;
read_attribute(group, "ids", ids);
for (auto id : ids) {
// Check to make sure multiple meshes in the same file don't share IDs
if (contains(mesh_ids, id)) {
fatal_error(fmt::format("Two or more meshes use the same unique ID "
"'{}' in the same HDF5 input file",
id));
}
mesh_ids.insert(id);
// If we've already read a mesh with the same ID in a *different* file,
// assume it is the same here
if (model::mesh_map.find(id) != model::mesh_map.end()) {
warning(fmt::format("Mesh with ID={} appears in multiple files.", id));
continue;
}
// Map ID to position in vector
model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1;
std::string name = fmt::format("mesh {}", id);
hid_t mesh_group = open_group(group, name.c_str());
std::string mesh_type;
if (object_exists(mesh_group, "type")) {
read_dataset(mesh_group, "type", mesh_type);
} else {
mesh_type = "regular";
}
// determine the mesh library to use
std::string mesh_lib;
if (object_exists(mesh_group, "library")) {
read_dataset(mesh_group, "library", mesh_lib);
}
Mesh::create(mesh_group, mesh_type, mesh_lib);
}
}

View file

@ -10,6 +10,7 @@ bool master {true};
#ifdef OPENMC_MPI
MPI_Comm intracomm {MPI_COMM_NULL};
MPI_Datatype source_site {MPI_DATATYPE_NULL};
MPI_Datatype collision_track_site {MPI_DATATYPE_NULL};
#endif
extern "C" bool openmc_master()

View file

@ -8,6 +8,7 @@
#include "openmc/bank.h"
#include "openmc/capi.h"
#include "openmc/cell.h"
#include "openmc/collision_track.h"
#include "openmc/constants.h"
#include "openmc/dagmc.h"
#include "openmc/error.h"
@ -121,6 +122,9 @@ void Particle::from_source(const SourceSite* src)
fission() = false;
zero_flux_derivs();
lifetime() = 0.0;
#ifdef OPENMC_DAGMC_ENABLED
history().reset();
#endif
// Copy attributes from source bank site
type() = src->particle;
@ -348,6 +352,11 @@ void Particle::event_collide()
collision_mg(*this);
}
// Collision track feature to recording particle interaction
if (settings::collision_track) {
collision_track_record(*this);
}
// Score collision estimator tallies -- this is done after a collision
// has occurred rather than before because we need information on the
// outgoing energy for any tallies with an outgoing energy filter

View file

@ -122,6 +122,7 @@ void sample_neutron_reaction(Particle& p)
"with k-effective close to or greater than one.");
}
}
p.event_mt() = rx.mt_;
}
// Create secondary photons
@ -663,7 +664,9 @@ void absorption(Particle& p, int i_nuclide)
p.wgt() = 0.0;
p.event() = TallyEvent::ABSORB;
p.event_mt() = N_DISAPPEAR;
if (!p.fission()) {
p.event_mt() = N_DISAPPEAR;
}
}
}
}

View file

@ -278,6 +278,10 @@ uint64_t RandomRay::transport_history_based_single_ray()
// Transports ray across a single source region
void RandomRay::event_advance_ray()
{
// If geometry debug mode is on, check for cell overlaps
if (settings::check_overlaps)
check_cell_overlap(*this);
// Find the distance to the nearest boundary
boundary() = distance_to_boundary(*this);
double distance = boundary().distance();

View file

@ -11,6 +11,7 @@
#endif
#include "openmc/capi.h"
#include "openmc/collision_track.h"
#include "openmc/constants.h"
#include "openmc/container_util.h"
#include "openmc/distribution.h"
@ -26,6 +27,7 @@
#include "openmc/plot.h"
#include "openmc/random_lcg.h"
#include "openmc/random_ray/random_ray.h"
#include "openmc/reaction.h"
#include "openmc/simulation.h"
#include "openmc/source.h"
#include "openmc/string_utils.h"
@ -45,6 +47,7 @@ namespace settings {
// Default values for boolean flags
bool assume_separate {false};
bool check_overlaps {false};
bool collision_track {false};
bool cmfd_run {false};
bool confidence_intervals {false};
bool create_delayed_neutrons {true};
@ -128,6 +131,7 @@ std::unordered_set<int> statepoint_batch;
double source_rejection_fraction {0.05};
double free_gas_threshold {400.0};
std::unordered_set<int> source_write_surf_id;
CollisionTrackConfig collision_track_config {};
int64_t ssw_max_particles;
int64_t ssw_max_files;
int64_t ssw_cell_id {C_NONE};
@ -925,8 +929,72 @@ void read_settings_xml(pugi::xml_node root)
}
}
// If source is not separate and is to be written out in the statepoint file,
// make sure that the sourcepoint batch numbers are contained in the
// Check if the user has specified to write specific collisions
if (check_for_node(root, "collision_track")) {
settings::collision_track = true;
// Get collision track node
xml_node node_ct = root.child("collision_track");
collision_track_config = CollisionTrackConfig {};
// Determine cell ids at which crossing particles are to be banked
if (check_for_node(node_ct, "cell_ids")) {
auto temp = get_node_array<int>(node_ct, "cell_ids");
for (const auto& b : temp) {
collision_track_config.cell_ids.insert(b);
}
}
if (check_for_node(node_ct, "reactions")) {
auto temp = get_node_array<std::string>(node_ct, "reactions");
for (const auto& b : temp) {
int reaction_int = reaction_type(b);
if (reaction_int > 0) {
collision_track_config.mt_numbers.insert(reaction_int);
}
}
}
if (check_for_node(node_ct, "universe_ids")) {
auto temp = get_node_array<int>(node_ct, "universe_ids");
for (const auto& b : temp) {
collision_track_config.universe_ids.insert(b);
}
}
if (check_for_node(node_ct, "material_ids")) {
auto temp = get_node_array<int>(node_ct, "material_ids");
for (const auto& b : temp) {
collision_track_config.material_ids.insert(b);
}
}
if (check_for_node(node_ct, "nuclides")) {
auto temp = get_node_array<std::string>(node_ct, "nuclides");
for (const auto& b : temp) {
collision_track_config.nuclides.insert(b);
}
}
if (check_for_node(node_ct, "deposited_E_threshold")) {
collision_track_config.deposited_energy_threshold =
std::stod(get_node_value(node_ct, "deposited_E_threshold"));
}
// Get maximum number of particles to be banked per collision
if (check_for_node(node_ct, "max_collisions")) {
collision_track_config.max_collisions =
std::stoll(get_node_value(node_ct, "max_collisions"));
} else {
warning("A maximum number of collisions needs to be specified. "
"By default the code sets 'max_collisions' parameter equals to "
"1000.");
}
// Get maximum number of collision_track files to be created
if (check_for_node(node_ct, "max_collision_track_files")) {
collision_track_config.max_files =
std::stoll(get_node_value(node_ct, "max_collision_track_files"));
}
if (check_for_node(node_ct, "mcpl")) {
collision_track_config.mcpl_write = get_node_value_bool(node_ct, "mcpl");
}
}
// If source is not separate and is to be written out in the statepoint
// file, make sure that the sourcepoint batch numbers are contained in the
// statepoint list
if (!source_separate) {
for (const auto& b : sourcepoint_batch) {
@ -1171,8 +1239,8 @@ void read_settings_xml(pugi::xml_node root)
variance_reduction::weight_windows_generators.emplace_back(
std::make_unique<WeightWindowsGenerator>(node_wwg));
}
// if any of the weight windows are intended to be generated otf, make sure
// they're applied
// if any of the weight windows are intended to be generated otf, make
// sure they're applied
for (const auto& wwg : variance_reduction::weight_windows_generators) {
if (wwg->on_the_fly_) {
settings::weight_windows_on = true;
@ -1220,11 +1288,6 @@ extern "C" int openmc_set_n_batches(
return OPENMC_E_INVALID_ARGUMENT;
}
if (simulation::current_batch >= n_batches) {
set_errmsg("Number of batches must be greater than current batch.");
return OPENMC_E_INVALID_ARGUMENT;
}
if (!settings::trigger_on) {
// Set n_batches and n_max_batches to same value
settings::n_batches = n_batches;

View file

@ -2,6 +2,7 @@
#include "openmc/bank.h"
#include "openmc/capi.h"
#include "openmc/collision_track.h"
#include "openmc/container_util.h"
#include "openmc/eigenvalue.h"
#include "openmc/error.h"
@ -9,7 +10,6 @@
#include "openmc/geometry_aux.h"
#include "openmc/ifp.h"
#include "openmc/material.h"
#include "openmc/mcpl_interface.h"
#include "openmc/message_passing.h"
#include "openmc/nuclide.h"
#include "openmc/output.h"
@ -118,6 +118,7 @@ int openmc_simulation_init()
// Reset global variables -- this is done before loading state point (as that
// will potentially populate k_generation and entropy)
simulation::current_batch = 0;
simulation::ct_current_file = 1;
simulation::ssw_current_file = 1;
simulation::k_generation.clear();
simulation::entropy.clear();
@ -297,6 +298,7 @@ namespace openmc {
namespace simulation {
int ct_current_file;
int current_batch;
int current_gen;
bool initialized {false};
@ -347,6 +349,11 @@ void allocate_banks()
// Allocate surface source bank
simulation::surf_source_bank.reserve(settings::ssw_max_particles);
}
if (settings::collision_track) {
// Allocate collision track bank
collision_track_reserve_bank();
}
}
void initialize_batch()
@ -400,11 +407,8 @@ void finalize_batch()
simulation::time_tallies.stop();
// update weight windows if needed
if (settings::solver_type != SolverType::RANDOM_RAY ||
simulation::current_batch == settings::n_batches) {
for (const auto& wwg : variance_reduction::weight_windows_generators) {
wwg->update();
}
for (const auto& wwg : variance_reduction::weight_windows_generators) {
wwg->update();
}
// Reset global tally results
@ -493,6 +497,10 @@ void finalize_batch()
++simulation::ssw_current_file;
}
}
// Write collision track file if requested
if (settings::collision_track) {
collision_track_flush_bank();
}
}
void initialize_generation()

View file

@ -9,6 +9,7 @@
#include <fmt/core.h>
#include "openmc/bank.h"
#include "openmc/bank_io.h"
#include "openmc/capi.h"
#include "openmc/constants.h"
#include "openmc/eigenvalue.h"
@ -201,6 +202,12 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source)
write_attribute(tally_group, "multiply_density", 0);
}
if (tally->higher_moments()) {
write_attribute(tally_group, "higher_moments", 1);
} else {
write_attribute(tally_group, "higher_moments", 0);
}
if (tally->estimator_ == TallyEstimator::ANALOG) {
write_dataset(tally_group, "estimator", "analog");
} else if (tally->estimator_ == TallyEstimator::TRACKLENGTH) {
@ -264,12 +271,13 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source)
for (const auto& tally : model::tallies) {
if (!tally->writable_)
continue;
// Write sum and sum_sq for each bin
// Write results for each bin
std::string name = "tally " + std::to_string(tally->id_);
hid_t tally_group = open_group(tallies_group, name.c_str());
auto& results = tally->results_;
write_tally_results(tally_group, results.shape()[0],
results.shape()[1], results.data());
results.shape()[1], results.shape()[2], results.data());
close_group(tally_group);
}
} else {
@ -509,7 +517,8 @@ extern "C" int openmc_statepoint_load(const char* filename)
} else {
auto& results = tally->results_;
read_tally_results(tally_group, results.shape()[0],
results.shape()[1], results.data());
results.shape()[1], results.shape()[2], results.data());
read_dataset(tally_group, "n_realizations", tally->n_realizations_);
close_group(tally_group);
}
@ -634,91 +643,12 @@ void write_source_bank(hid_t group_id, span<SourceSite> source_bank,
{
hid_t banktype = h5banktype();
// Set total and individual process dataspace sizes for source bank
int64_t dims_size = bank_index.back();
int64_t count_size = bank_index[mpi::rank + 1] - bank_index[mpi::rank];
#ifdef PHDF5
// Set size of total dataspace for all procs and rank
hsize_t dims[] {static_cast<hsize_t>(dims_size)};
hid_t dspace = H5Screate_simple(1, dims, nullptr);
hid_t dset = H5Dcreate(group_id, "source_bank", banktype, dspace, H5P_DEFAULT,
H5P_DEFAULT, H5P_DEFAULT);
// Create another data space but for each proc individually
hsize_t count[] {static_cast<hsize_t>(count_size)};
hid_t memspace = H5Screate_simple(1, count, nullptr);
// Select hyperslab for this dataspace
hsize_t start[] {static_cast<hsize_t>(bank_index[mpi::rank])};
H5Sselect_hyperslab(dspace, H5S_SELECT_SET, start, nullptr, count, nullptr);
// Set up the property list for parallel writing
hid_t plist = H5Pcreate(H5P_DATASET_XFER);
H5Pset_dxpl_mpio(plist, H5FD_MPIO_COLLECTIVE);
// Write data to file in parallel
H5Dwrite(dset, banktype, memspace, dspace, plist, source_bank.data());
// Free resources
H5Sclose(dspace);
H5Sclose(memspace);
H5Dclose(dset);
H5Pclose(plist);
#ifdef OPENMC_MPI
write_bank_dataset("source_bank", group_id, source_bank, bank_index, banktype,
mpi::source_site);
#else
if (mpi::master) {
// Create dataset big enough to hold all source sites
hsize_t dims[] {static_cast<hsize_t>(dims_size)};
hid_t dspace = H5Screate_simple(1, dims, nullptr);
hid_t dset = H5Dcreate(group_id, "source_bank", banktype, dspace,
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
// Save source bank sites since the array is overwritten below
#ifdef OPENMC_MPI
vector<SourceSite> temp_source {source_bank.begin(), source_bank.end()};
#endif
for (int i = 0; i < mpi::n_procs; ++i) {
// Create memory space
hsize_t count[] {static_cast<hsize_t>(bank_index[i + 1] - bank_index[i])};
hid_t memspace = H5Screate_simple(1, count, nullptr);
#ifdef OPENMC_MPI
// Receive source sites from other processes
if (i > 0)
MPI_Recv(source_bank.data(), count[0], mpi::source_site, i, i,
mpi::intracomm, MPI_STATUS_IGNORE);
#endif
// Select hyperslab for this dataspace
dspace = H5Dget_space(dset);
hsize_t start[] {static_cast<hsize_t>(bank_index[i])};
H5Sselect_hyperslab(
dspace, H5S_SELECT_SET, start, nullptr, count, nullptr);
// Write data to hyperslab
H5Dwrite(
dset, banktype, memspace, dspace, H5P_DEFAULT, source_bank.data());
H5Sclose(memspace);
H5Sclose(dspace);
}
// Close all ids
H5Dclose(dset);
#ifdef OPENMC_MPI
// Restore state of source bank
std::copy(temp_source.begin(), temp_source.end(), source_bank.begin());
#endif
} else {
#ifdef OPENMC_MPI
MPI_Send(source_bank.data(), count_size, mpi::source_site, 0, mpi::rank,
mpi::intracomm);
#endif
}
write_bank_dataset(
"source_bank", group_id, source_bank, bank_index, banktype);
#endif
H5Tclose(banktype);
@ -1001,7 +931,8 @@ void write_tally_results_nr(hid_t file_id)
// Write reduced tally results to file
auto shape = results_copy.shape();
write_tally_results(tally_group, shape[0], shape[1], results_copy.data());
write_tally_results(
tally_group, shape[0], shape[1], shape[2], results_copy.data());
close_group(tally_group);
} else {

View file

@ -107,6 +107,9 @@ Tally::Tally(pugi::xml_node node)
multiply_density_ = get_node_value_bool(node, "multiply_density");
}
if (check_for_node(node, "higher_moments")) {
higher_moments_ = get_node_value_bool(node, "higher_moments");
}
// =======================================================================
// READ DATA FOR FILTERS
@ -800,7 +803,11 @@ void Tally::init_triggers(pugi::xml_node node)
void Tally::init_results()
{
int n_scores = scores_.size() * nuclides_.size();
results_ = xt::empty<double>({n_filter_bins_, n_scores, 3});
if (higher_moments_) {
results_ = xt::empty<double>({n_filter_bins_, n_scores, 5});
} else {
results_ = xt::empty<double>({n_filter_bins_, n_scores, 3});
}
}
void Tally::reset()
@ -825,22 +832,46 @@ void Tally::accumulate()
total_source = 1.0;
}
// Determine number of particles contributing to tally
double contributing_particles = settings::reduce_tallies
? settings::n_particles
: simulation::work_per_rank;
// Account for number of source particles in normalization
double norm =
total_source / (settings::n_particles * settings::gen_per_batch);
total_source / (contributing_particles * settings::gen_per_batch);
if (settings::solver_type == SolverType::RANDOM_RAY) {
norm = 1.0;
}
// Accumulate each result
// Accumulate each result
if (higher_moments_) {
#pragma omp parallel for
for (int i = 0; i < results_.shape()[0]; ++i) {
for (int j = 0; j < results_.shape()[1]; ++j) {
double val = results_(i, j, TallyResult::VALUE) * norm;
results_(i, j, TallyResult::VALUE) = 0.0;
results_(i, j, TallyResult::SUM) += val;
results_(i, j, TallyResult::SUM_SQ) += val * val;
// filter bins (specific cell, energy bins)
for (int i = 0; i < results_.shape()[0]; ++i) {
// score bins (flux, total reaction rate, fission reaction rate, etc.)
for (int j = 0; j < results_.shape()[1]; ++j) {
double val = results_(i, j, TallyResult::VALUE) * norm;
double val2 = val * val;
results_(i, j, TallyResult::VALUE) = 0.0;
results_(i, j, TallyResult::SUM) += val;
results_(i, j, TallyResult::SUM_SQ) += val2;
results_(i, j, TallyResult::SUM_THIRD) += val2 * val;
results_(i, j, TallyResult::SUM_FOURTH) += val2 * val2;
}
}
} else {
#pragma omp parallel for
// filter bins (specific cell, energy bins)
for (int i = 0; i < results_.shape()[0]; ++i) {
// score bins (flux, total reaction rate, fission reaction rate, etc.)
for (int j = 0; j < results_.shape()[1]; ++j) {
double val = results_(i, j, TallyResult::VALUE) * norm;
results_(i, j, TallyResult::VALUE) = 0.0;
results_(i, j, TallyResult::SUM) += val;
results_(i, j, TallyResult::SUM_SQ) += val * val;
}
}
}
}

View file

@ -26,6 +26,7 @@
#include "openmc/random_ray/flat_source_domain.h"
#include "openmc/search.h"
#include "openmc/settings.h"
#include "openmc/simulation.h"
#include "openmc/tallies/filter_energy.h"
#include "openmc/tallies/filter_mesh.h"
#include "openmc/tallies/filter_particle.h"
@ -546,8 +547,10 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value,
// build a shape for a view of the tally results, this will always be
// dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension)
std::array<int, 5> shape = {
1, 1, 1, tally->n_scores(), static_cast<int>(TallyResult::SIZE)};
// Look for the size of the last dimension of the results array
const auto& results_arr = tally->results();
const int results_dim = static_cast<int>(results_arr.shape()[2]);
std::array<int, 5> shape = {1, 1, 1, tally->n_scores(), results_dim};
// set the shape for the filters applied on the tally
for (int i = 0; i < tally->filters().size(); i++) {
@ -585,7 +588,7 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value,
// get a fully reshaped view of the tally according to tally ordering of
// filters
auto tally_values = xt::reshape_view(tally->results(), shape);
auto tally_values = xt::reshape_view(results_arr, shape);
// get a that is (particle, energy, mesh, scores, values)
auto transposed_view = xt::transpose(tally_values, transpose);
@ -966,11 +969,17 @@ void WeightWindowsGenerator::update() const
Tally* tally = model::tallies[tally_idx_].get();
// if we're beyond the number of max realizations or not at the corrrect
// update interval, skip the update
if (max_realizations_ < tally->n_realizations_ ||
tally->n_realizations_ % update_interval_ != 0)
// If in random ray mode, only update on the last batch
if (settings::solver_type == SolverType::RANDOM_RAY) {
if (simulation::current_batch != settings::n_batches) {
return;
}
// If in Monte Carlo mode and beyond the number of max realizations or
// not at the correct update interval, skip the update
} else if (max_realizations_ < tally->n_realizations_ ||
tally->n_realizations_ % update_interval_ != 0) {
return;
}
wws->update_weights(tally, tally_value_, threshold_, ratio_, method_);
@ -1328,6 +1337,10 @@ extern "C" int openmc_weight_windows_import(const char* filename)
hid_t weight_windows_group = open_group(ww_file, "weight_windows");
hid_t mesh_group = open_group(ww_file, "meshes");
read_meshes(mesh_group);
std::vector<std::string> names = group_names(weight_windows_group);
for (const auto& name : names) {

View file

@ -29,12 +29,29 @@ def run_in_tmpdir(tmpdir):
yield
finally:
orig.chdir()
@pytest.fixture(scope="module")
def endf_data():
return os.environ['OPENMC_ENDF_DATA']
return os.environ['OPENMC_ENDF_DATA']
@pytest.fixture(scope='session', autouse=True)
def resolve_paths():
with openmc.config.patch('resolve_paths', False):
yield
@pytest.fixture(scope='session', autouse=True)
def disable_depletion_multiprocessing_under_mpi():
"""Fork-based depletion multiprocessing may deadlock if MPI is active."""
if not regression_config['mpi']:
yield
return
from openmc.deplete import pool
original_setting = pool.USE_MULTIPROCESSING
pool.USE_MULTIPROCESSING = False
try:
yield
finally:
pool.USE_MULTIPROCESSING = original_setting

View file

@ -5,6 +5,7 @@ set(TEST_NAMES
test_interpolate
test_math
test_mcpl_stat_sum
test_mesh
# Add additional unit test files here
)

View file

@ -0,0 +1,257 @@
#include <cstdio>
#include <iostream>
#include <string>
#include <catch2/catch_test_macros.hpp>
#include <pugixml.hpp>
#include "openmc/hdf5_interface.h"
#include "openmc/mesh.h"
using namespace openmc;
TEST_CASE("Test mesh hdf5 roundtrip - regular")
{
// The XML data as a string
std::string xml_string = R"(
<mesh id="1">
<dimension>3 4 5</dimension>
<lower_left>-2 -3 -5</lower_left>
<upper_right>2 3 5</upper_right>
</mesh>
)";
// Create a pugixml document object
pugi::xml_document doc;
// Load the XML from the string
pugi::xml_parse_result result = doc.load_string(xml_string.c_str());
pugi::xml_node root = doc.child("mesh");
auto mesh = RegularMesh(root);
hid_t file_id = file_open("mesh.h5", 'w');
mesh.to_hdf5(file_id);
file_close(file_id);
hid_t file_id2 = file_open("mesh.h5", 'r');
hid_t group = open_group(file_id2, "mesh 1");
auto mesh2 = RegularMesh(group);
file_close(file_id2);
remove("mesh.h5");
REQUIRE(mesh2.shape_ == mesh.shape_);
REQUIRE(mesh2.lower_left() == mesh.lower_left());
REQUIRE(mesh2.upper_right() == mesh.upper_right());
}
TEST_CASE("Test mesh hdf5 roundtrip - rectilinear")
{
// The XML data as a string
std::string xml_string = R"(
<mesh id="1" type="rectilinear">
<x_grid>0.0 1.0 5.0 10.0</x_grid>
<y_grid>-10.0 -5.0 0.0</y_grid>
<z_grid>-100.0 0.0 100.0</z_grid>
</mesh>
)";
// Create a pugixml document object
pugi::xml_document doc;
// Load the XML from the string
pugi::xml_parse_result result = doc.load_string(xml_string.c_str());
pugi::xml_node root = doc.child("mesh");
auto mesh = RectilinearMesh(root);
hid_t file_id = file_open("mesh.h5", 'w');
mesh.to_hdf5(file_id);
file_close(file_id);
hid_t file_id2 = file_open("mesh.h5", 'r');
hid_t group = open_group(file_id2, "mesh 1");
auto mesh2 = RectilinearMesh(group);
file_close(file_id2);
remove("mesh.h5");
REQUIRE(mesh2.shape_ == mesh.shape_);
REQUIRE(mesh2.grid_ == mesh.grid_);
}
TEST_CASE("Test mesh hdf5 roundtrip - cylindrical")
{
// The XML data as a string
std::string xml_string = R"(
<mesh id="1" type="cylindrical">
<r_grid>0.1 0.2 0.5 1.0</r_grid>
<phi_grid>0.0 6.283185307179586</phi_grid>
<z_grid>0.1 0.2 0.4 0.6 1.0</z_grid>
<origin>0 0 0</origin>
</mesh>
)";
// Create a pugixml document object
pugi::xml_document doc;
// Load the XML from the string
pugi::xml_parse_result result = doc.load_string(xml_string.c_str());
pugi::xml_node root = doc.child("mesh");
auto mesh = CylindricalMesh(root);
hid_t file_id = file_open("mesh.h5", 'w');
mesh.to_hdf5(file_id);
file_close(file_id);
hid_t file_id2 = file_open("mesh.h5", 'r');
hid_t group = open_group(file_id2, "mesh 1");
auto mesh2 = CylindricalMesh(group);
file_close(file_id2);
remove("mesh.h5");
REQUIRE(mesh2.shape_ == mesh.shape_);
REQUIRE(mesh2.grid_ == mesh.grid_);
}
TEST_CASE("Test mesh hdf5 roundtrip - spherical")
{
// The XML data as a string
std::string xml_string = R"(
<mesh id="1" type="spherical">
<r_grid>0.1 0.2 0.5 1.0</r_grid>
<theta_grid>0.0 3.141592653589793</theta_grid>
<phi_grid>0.0 6.283185307179586</phi_grid>
<origin>0.0 0.0 0.0</origin>
</mesh>'
)";
// Create a pugixml document object
pugi::xml_document doc;
// Load the XML from the string
pugi::xml_parse_result result = doc.load_string(xml_string.c_str());
pugi::xml_node root = doc.child("mesh");
auto mesh = SphericalMesh(root);
hid_t file_id = file_open("mesh.h5", 'w');
mesh.to_hdf5(file_id);
file_close(file_id);
hid_t file_id2 = file_open("mesh.h5", 'r');
hid_t group = open_group(file_id2, "mesh 1");
auto mesh2 = SphericalMesh(group);
file_close(file_id2);
remove("mesh.h5");
REQUIRE(mesh2.shape_ == mesh.shape_);
REQUIRE(mesh2.grid_ == mesh.grid_);
}
TEST_CASE("Test multiple meshes HDF5 roundtrip - spherical")
{
// The XML data as a string
std::string xml_string = R"(
<meshes>
<mesh id="1" type="spherical">
<r_grid>0.1 0.2 0.5 1.0</r_grid>
<theta_grid>0.0 3.141592653589793</theta_grid>
<phi_grid>0.0 6.283185307179586</phi_grid>
<origin>0.0 0.0 0.0</origin>
</mesh>
<mesh id="2">
<dimension>3 4 5</dimension>
<lower_left>-2 -3 -5</lower_left>
<upper_right>2 3 5</upper_right>
</mesh>
</meshes>
)";
// Create a pugixml document object
pugi::xml_document doc;
// Load the XML from the string
pugi::xml_parse_result result = doc.load_string(xml_string.c_str());
pugi::xml_node root = doc.child("meshes");
read_meshes(root);
const auto spherical_mesh_xml =
dynamic_cast<SphericalMesh*>(model::meshes[0].get());
const auto regular_mesh_xml =
dynamic_cast<RegularMesh*>(model::meshes[1].get());
hid_t file_id = file_open("meshes.h5", 'w');
hid_t root_group = create_group(file_id, "root");
open_group(file_id, "root");
meshes_to_hdf5(root_group);
close_group(root_group);
file_close(file_id);
hid_t file_id2 = file_open("meshes.h5", 'r');
hid_t root_group_read = open_group(file_id2, "root");
hid_t mesh_group_read = open_group(root_group_read, "meshes");
read_meshes(mesh_group_read);
// increment mesh IDs to avoid collision during read
for (auto& mesh : model::meshes) {
mesh->set_id(mesh->id() + 10);
}
const auto spherical_mesh_hdf5 = dynamic_cast<SphericalMesh*>(
model::meshes[model::mesh_map[spherical_mesh_xml->id_]].get());
const auto regular_mesh_hdf5 = dynamic_cast<RegularMesh*>(
model::meshes[model::mesh_map[regular_mesh_xml->id_]].get());
remove("meshes.h5");
REQUIRE(spherical_mesh_hdf5->shape_ == spherical_mesh_xml->shape_);
REQUIRE(spherical_mesh_hdf5->grid_ == spherical_mesh_xml->grid_);
REQUIRE(regular_mesh_hdf5->shape_ == regular_mesh_xml->shape_);
REQUIRE(regular_mesh_hdf5->lower_left() == regular_mesh_xml->lower_left());
REQUIRE(regular_mesh_hdf5->upper_right() == regular_mesh_xml->upper_right());
}

View file

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<reactions>(n,fission) 101</reactions>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<cell_ids>22</cell_ids>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<material_ids>1</material_ids>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<nuclides>O16 U235</nuclides>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,59 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<cell_ids>22</cell_ids>
<universe_ids>77</universe_ids>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<deposited_E_threshold>550000.0</deposited_E_threshold>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,63 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<cell_ids>22 33</cell_ids>
<reactions>elastic 18 (n,disappear)</reactions>
<universe_ids>77</universe_ids>
<material_ids>1 11</material_ids>
<nuclides>U238 U235 H1 U234</nuclides>
<deposited_E_threshold>100000.0</deposited_E_threshold>
<max_collisions>300</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,57 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="11.0" units="g/cm3"/>
<nuclide name="U234" ao="0.0004524"/>
<nuclide name="U235" ao="0.0506068"/>
<nuclide name="U238" ao="0.948709"/>
<nuclide name="U236" ao="0.0002318"/>
<nuclide name="O16" ao="2.0"/>
</material>
<material id="11">
<density value="1.0" units="g/cm3"/>
<nuclide name="H1" ao="2.0"/>
<nuclide name="O16" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="5" fill="77" region="-5 4 -7 6 -9 8" universe="1"/>
<cell id="8" material="11" region="-11 10 -13 12 -15 14 (5 | -4 | 7 | -6 | 9 | -8)" universe="1"/>
<cell id="22" material="1" region="-1 2 -3" universe="77"/>
<cell id="33" material="11" region="1 | -2 | 3" universe="77"/>
<surface id="1" type="z-cylinder" coeffs="0.0 0.0 2.0"/>
<surface id="2" type="z-plane" coeffs="-2.0"/>
<surface id="3" type="z-plane" coeffs="2.0"/>
<surface id="4" type="x-plane" coeffs="-3.0"/>
<surface id="5" type="x-plane" coeffs="3.0"/>
<surface id="6" type="y-plane" coeffs="-3.0"/>
<surface id="7" type="y-plane" coeffs="3.0"/>
<surface id="8" type="z-plane" coeffs="-3.0"/>
<surface id="9" type="z-plane" coeffs="3.0"/>
<surface id="10" type="x-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="11" type="x-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="12" type="y-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="13" type="y-plane" boundary="vacuum" coeffs="4.0"/>
<surface id="14" type="z-plane" boundary="vacuum" coeffs="-4.0"/>
<surface id="15" type="z-plane" boundary="vacuum" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>100</particles>
<batches>5</batches>
<inactive>1</inactive>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-2.0 -2.0 -2.0 2.0 2.0 2.0</parameters>
</space>
<constraints>
<fissionable>true</fissionable>
</constraints>
</source>
<collision_track>
<max_collisions>200</max_collisions>
</collision_track>
<seed>1</seed>
</settings>
</model>

View file

@ -0,0 +1,2 @@
k-combined:
5.642735E-02 1.494035E-02

View file

@ -0,0 +1,255 @@
"""Test the 'collision_track' setting.
Results
-------
All results are generated using only 1 MPI process.
All results are generated using 1 thread except for "test_consistency_low_realization_number".
This specific test verifies that when the number of realization (i.e., point being candidate
to be stored) is lower than the capacity, results are reproducible even with multiple
threads (i.e., there is no potential thread competition that would produce different
results in that case).
All results are generated using the history-based mode except for cases e01 to e03.
All results are visually verified using the '_visualize.py' script in the regression test folder.
OpenMC models
-------------
Four OpenMC models with CSG-only geometries are used to cover the transmission, vacuum,
reflective and periodic Boundary Conditions (BC):
- model_1: cylindrical core in 2 boxes (vacuum and transmission BC),
# Test cases for simulation parameters using CSG-only geometries
# ============================================================
# Each test case is defined by a combination of folder name, model name, and specific parameters.
# Below is a summary of the parameters used in the test cases:
#
# - max_collisions: Maximum number of particles to track in the simulation.
# - reactions: List of MT numbers (reaction types- 2 for scattering, 18 for fission, 101 for absorbtion).
# - cell_ids: IDs of specific cells in the model.
# - mat_ids: Material IDs for filtering particles.
# - nuclides: Nuclides for filtering particles.
# - univ_ids: Universe IDs for filtering particles.
# - E_threshold: Energy threshold for filtering particles (optional).
#
# The test cases are designed to validate the behavior of the simulation under various configurations.
*: BC stands for Boundary Conditions, T for Transmission, R for Reflective, and V for Vacuum.
An additional case, called 'case-a01', is used to check that the results are comparable when
the number of threads is set to 2 if the number of realization is lower than the capacity.
*: BC stands for Boundary Conditions, T for Transmission, and V for Vacuum.
Notes:
- The test cases list is non-exhaustive compared to the number of possible combinations.
Test cases have been selected based on use and internal code logic.
TODO:
- Test with a lattice.
"""
import os
import openmc
import openmc.lib
import pytest
from tests.testing_harness import CollisionTrackTestHarness
from tests.regression_tests import config
@pytest.fixture(scope="function")
def two_threads(monkeypatch):
"""Set the number of OMP threads to 2 for the test."""
monkeypatch.setenv("OMP_NUM_THREADS", "2")
@pytest.fixture(scope="function")
def single_process(monkeypatch):
"""Set the number of MPI process to 1 for the test."""
monkeypatch.setitem(config, "mpi_np", "1")
@pytest.fixture(scope="module")
def model_1():
"""Cylindrical core contained in a first box which is contained in a larger box.
A lower universe is used to describe the interior of the first box which
contains the core and its surrounding space.
"""
openmc.reset_auto_ids()
model = openmc.Model()
# =============================================================================
# Materials
# =============================================================================
fuel = openmc.Material(material_id=1)
fuel.add_nuclide("U234", 0.0004524)
fuel.add_nuclide("U235", 0.0506068)
fuel.add_nuclide("U238", 0.9487090)
fuel.add_nuclide("U236", 0.0002318)
fuel.add_nuclide("O16", 2.0)
fuel.set_density("g/cm3", 11.0)
water = openmc.Material(material_id=11)
water.add_nuclide("H1", 2.0)
water.add_nuclide("O16", 1.0)
water.set_density("g/cm3", 1.0)
# =============================================================================
# Geometry
# =============================================================================
# -----------------------------------------------------------------------------
# Cylindrical core
# -----------------------------------------------------------------------------
# Parameters
core_radius = 2.0
core_height = 4.0
# Surfaces
core_cylinder = openmc.ZCylinder(r=core_radius)
core_lower_plane = openmc.ZPlane(-core_height / 2.0)
core_upper_plane = openmc.ZPlane(core_height / 2.0)
# Region
core_region = -core_cylinder & +core_lower_plane & -core_upper_plane
# Cells
core = openmc.Cell(fill=fuel, region=core_region, cell_id=22)
outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane
outside_core = openmc.Cell(
fill=water, region=outside_core_region, cell_id=33)
# Universe
inside_box1_universe = openmc.Universe(
cells=[core, outside_core], universe_id=77)
# -----------------------------------------------------------------------------
# Box 1
# -----------------------------------------------------------------------------
# Parameters
box1_size = 6.0
# Surfaces
box1_rpp = openmc.model.RectangularParallelepiped(
-box1_size / 2.0, box1_size / 2.0,
-box1_size / 2.0, box1_size / 2.0,
-box1_size / 2.0, box1_size / 2.0,
)
# Cell
box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp, cell_id=5)
# -----------------------------------------------------------------------------
# Box 2
# -----------------------------------------------------------------------------
# Parameters
box2_size = 8
# Surfaces
box2_rpp = openmc.model.RectangularParallelepiped(
-box2_size / 2.0, box2_size / 2.0,
-box2_size / 2.0, box2_size / 2.0,
-box2_size / 2.0, box2_size / 2.0,
boundary_type="vacuum"
)
# Cell
box2 = openmc.Cell(fill=water, region=-box2_rpp & +box1_rpp, cell_id=8)
# Register geometry
model.geometry = openmc.Geometry([box1, box2])
# =============================================================================
# Settings
# =============================================================================
model.settings = openmc.Settings()
model.settings.particles = 100
model.settings.batches = 5
model.settings.inactive = 1
model.settings.seed = 1
bounds = [
-core_radius,
-core_radius,
-core_height / 2.0,
core_radius,
core_radius,
core_height / 2.0,
]
distribution = openmc.stats.Box(bounds[:3], bounds[3:])
model.settings.source = openmc.IndependentSource(
space=distribution, constraints={'fissionable': True})
return model
@pytest.mark.parametrize(
"folder, model_name, parameter",
[("case_1_Reactions", "model_1", {"max_collisions": 300, "reactions": ["(n,fission)", 101]}),
("case_2_Cell_ID", "model_1", {
"max_collisions": 300, "cell_ids": [22]}),
("case_3_Material_ID", "model_1", {
"max_collisions": 300, "material_ids": [1]}),
("case_4_Nuclide_ID", "model_1", {
"max_collisions": 300, "nuclides": ["O16", "U235"]}),
("case_5_Universe_ID", "model_1", {
"max_collisions": 300, "cell_ids": [22], "universe_ids": [77]}),
("case_6_deposited_energy_threshold", "model_1", {
"max_collisions": 300, "deposited_E_threshold": 5.5e5}),
("case_7_all_parameters_used_together", "model_1", {
"max_collisions": 300,
"reactions": ["elastic", 18, "(n,disappear)"],
"material_ids": [1, 11],
"universe_ids": [77],
"nuclides": ["U238", "U235", "H1", "U234"],
"cell_ids": [22, 33],
"deposited_E_threshold": 1e5})
],
)
def test_collision_track_several_cases(
folder, model_name, parameter, request
):
# Since for these tests the actual number of collisions recorded is < max_collisions,
# we can run them with 1 or 2 threads, and in history or event mode.
model = request.getfixturevalue(model_name)
model.settings.collision_track = parameter
harness = CollisionTrackTestHarness(
"statepoint.5.h5", model=model, workdir=folder
)
harness.main()
@pytest.mark.skipif(config["event"], reason="Results from history-based mode.")
def test_collision_track_2threads(model_1, two_threads, single_process):
# This test checks that the `max_collisions` setting is honored:
# no collisions beyond the specified limit should be recorded.
#
# For the result to be reproducible, the number of threads and
# the transport mode (history vs. event) must remain fixed.
assert os.environ["OMP_NUM_THREADS"] == "2"
assert config["mpi_np"] == "1"
model_1.settings.collision_track = {
"max_collisions": 200
}
harness = CollisionTrackTestHarness(
"statepoint.5.h5", model=model_1, workdir="case_8_2threads"
)
harness.main()

View file

@ -76,6 +76,8 @@ def test_against_self(run_in_tmpdir,
dt = [360] # single step
# Perform simulation using the predictor algorithm
if config['mpi'] and multiproc:
pytest.skip("Multiprocessing depletion is disabled when MPI is enabled.")
openmc.deplete.pool.USE_MULTIPROCESSING = multiproc
openmc.deplete.PredictorIntegrator(op,
dt,
@ -135,6 +137,8 @@ def test_against_coupled(run_in_tmpdir,
dt = [dt] # single step
# Perform simulation using the predictor algorithm
if config['mpi'] and multiproc:
pytest.skip("Multiprocessing depletion is disabled when MPI is enabled.")
openmc.deplete.pool.USE_MULTIPROCESSING = multiproc
openmc.deplete.PredictorIntegrator(
op, dt, power=174, timestep_units=time_units).integrate()

Some files were not shown because too many files have changed in this diff Show more