diff --git a/.gitignore b/.gitignore index 88e59895d..3b2a24a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.o *.log *.out +*.pkl # Compiler python objects *.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt index 85dcd7084..5e949070c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,37 @@ option(OPENMC_USE_DAGMC "Enable support for DAGMC (CAD) geometry" option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tallies" OFF) option(OPENMC_USE_MPI "Enable MPI" OFF) +# Warnings for deprecated options +foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") + if(DEFINED ${OLD_OPT}) + string(TOUPPER ${OLD_OPT} OPT_UPPER) + if ("${OLD_OPT}" STREQUAL "profile" OR "${OLD_OPT}" STREQUAL "coverage") + set(NEW_OPT_PREFIX "OPENMC_ENABLE") + else() + set(NEW_OPT_PREFIX "OPENMC_USE") + endif() + message(WARNING "The OpenMC CMake option '${OLD_OPT}' has been deprecated. " + "Its value will be ignored. " + "Please use '-D${NEW_OPT_PREFIX}_${OPT_UPPER}=${${OLD_OPT}}' instead.") + unset(${OLD_OPT} CACHE) + endif() +endforeach() + +foreach(OLD_BLD in ITEMS "debug" "optimize") + if(DEFINED ${OLD_BLD}) + if("${OLD_BLD}" STREQUAL "debug") + set(BLD_VAR "Debug") + else() + set(BLD_VAR "Release") + endif() + message(WARNING "The OpenMC CMake option '${OLD_BLD}' has been deprecated. " + "Its value will be ignored. " + "OpenMC now uses the CMAKE_BUILD_TYPE variable to set the build mode. " + "Please use '-DCMAKE_BUILD_TYPE=${BLD_VAR}' instead.") + unset(${OLD_BLD} CACHE) + endif() +endforeach() + #=============================================================================== # Set a default build configuration if not explicitly specified #=============================================================================== diff --git a/Dockerfile b/Dockerfile index 341390c9a..d408b9de5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,22 +15,24 @@ # sudo docker run image_name:tag_name or ID with no tag sudo docker run ID number -FROM debian:bullseye-slim AS dependencies + +# global ARG as these ARGS are used in multiple stages +# By default one core is used to compile +ARG compile_cores=1 # By default this Dockerfile builds OpenMC without DAGMC and LIBMESH support ARG build_dagmc=off ARG build_libmesh=off -# By default one core is used to compile -ARG compile_cores=1 +FROM debian:bullseye-slim AS dependencies + +ARG compile_cores +ARG build_dagmc +ARG build_libmesh # Set default value of HOME to /root ENV HOME=/root -# OpenMC variables -ARG openmc_branch=master -ENV OPENMC_REPO='https://github.com/openmc-dev/openmc' - # Embree variables ENV EMBREE_TAG='v3.12.2' ENV EMBREE_REPO='https://github.com/embree/embree' @@ -60,7 +62,6 @@ ENV NJOY_REPO='https://github.com/njoy/NJOY2016' # Setup environment variables for Docker image ENV LD_LIBRARY_PATH=${DAGMC_INSTALL_DIR}/lib:$LD_LIBRARY_PATH \ - OPENMC_CROSS_SECTIONS=/root/nndc_hdf5/cross_sections.xml \ OPENMC_ENDF_DATA=/root/endf-b-vii.1 \ DEBIAN_FRONTEND=noninteractive @@ -174,12 +175,25 @@ RUN if [ "$build_libmesh" = "on" ]; then \ FROM dependencies AS build +ENV HOME=/root + +ARG openmc_branch=master +ENV OPENMC_REPO='https://github.com/openmc-dev/openmc' + +ARG compile_cores +ARG build_dagmc +ARG build_libmesh + +ENV DAGMC_INSTALL_DIR=$HOME/DAGMC/ +ENV LIBMESH_INSTALL_DIR=$HOME/LIBMESH + # clone and install openmc RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ && git clone --shallow-submodules --recurse-submodules --single-branch -b ${openmc_branch} --depth=1 ${OPENMC_REPO} \ && mkdir build && cd build ; \ if [ ${build_dagmc} = "on" ] && [ ${build_libmesh} = "on" ]; then \ cmake ../openmc \ + -DCMAKE_CXX_COMPILER=mpicxx \ -DOPENMC_USE_MPI=on \ -DHDF5_PREFER_PARALLEL=on \ -DOPENMC_USE_DAGMC=on \ @@ -188,6 +202,7 @@ RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ fi ; \ if [ ${build_dagmc} = "on" ] && [ ${build_libmesh} = "off" ]; then \ cmake ../openmc \ + -DCMAKE_CXX_COMPILER=mpicxx \ -DOPENMC_USE_MPI=on \ -DHDF5_PREFER_PARALLEL=on \ -DOPENMC_USE_DAGMC=ON \ @@ -195,6 +210,7 @@ RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ fi ; \ if [ ${build_dagmc} = "off" ] && [ ${build_libmesh} = "on" ]; then \ cmake ../openmc \ + -DCMAKE_CXX_COMPILER=mpicxx \ -DOPENMC_USE_MPI=on \ -DHDF5_PREFER_PARALLEL=on \ -DOPENMC_USE_LIBMESH=on \ @@ -202,6 +218,7 @@ RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ fi ; \ if [ ${build_dagmc} = "off" ] && [ ${build_libmesh} = "off" ]; then \ cmake ../openmc \ + -DCMAKE_CXX_COMPILER=mpicxx \ -DOPENMC_USE_MPI=on \ -DHDF5_PREFER_PARALLEL=on ; \ fi ; \ @@ -211,5 +228,8 @@ RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ FROM build AS release +ENV HOME=/root +ENV OPENMC_CROSS_SECTIONS=/root/nndc_hdf5/cross_sections.xml + # Download cross sections (NNDC and WMP) and ENDF data needed by test suite RUN ${HOME}/OpenMC/openmc/tools/ci/download-xs.sh diff --git a/docs/source/io_formats/nuclear_data.rst b/docs/source/io_formats/nuclear_data.rst index ef61604cb..8174108e1 100644 --- a/docs/source/io_formats/nuclear_data.rst +++ b/docs/source/io_formats/nuclear_data.rst @@ -339,6 +339,16 @@ Incoherent elastic scattering [eV\ :math:`^{-1}`]. :Attributes: - **type** (*char[]*) -- 'IncoherentElastic' +Sum of functions +---------------- + +:Object type: Group +:Attributes: - **type** (*char[]*) -- "Sum" + - **n** (*int*) -- Number of functions +:Datasets: + - ***func_** (:ref:`function <1d_functions>`) -- Dataset for the + i-th function (indexing starts at 1) + .. _angle_energy: -------------------------- @@ -501,6 +511,19 @@ equiprobable bins. - **skewed** (*int8_t*) -- Whether discrete angles are equi-probable (0) or have a skewed distribution (1). +Mixed Elastic +------------- + +This angle-energy distribution is used when an evaluation specifies both +coherent and incoherent elastic thermal neutron scattering. + +:Object type: Group +:Attributes: - **type** (*char[]*) -- "mixed_elastic" +:Groups: - **coherent** -- Distribution for coherent elastic scattering. The + format is given in :ref:`angle_energy`. + - **incoherent** -- Distribution for incoherent elastic scattering. + The format is given in :ref:`angle_energy`. + .. _energy_distribution: -------------------- diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index f96d7181d..c1598f2c3 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -79,9 +79,15 @@ The current version of the statepoint file format is 17.0. - **width** (*double[]*) -- Width of each mesh cell in each dimension. - **Unstructured Mesh Only:** + - **filename** (*char[]*) -- Name of the mesh file. + - **library** (*char[]*) -- Mesh library used to represent the + mesh ("moab" or "libmesh"). + - **length_multiplier** (*double*) Scaling factor applied to the mesh. - **volumes** (*double[]*) -- Volume of each mesh cell. - - **centroids** (*double[]*) -- Location of the mesh cell - centroids. + - **vertices** (*double[]*) -- x, y, z values of the mesh vertices. + - **connectivity** (*int[]*) -- Connectivity array for the mesh + cells. + - **element_types** (*int[]*) -- Mesh element types. **/tallies/filters/** diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index dce1f2503..1b131bed5 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -103,16 +103,17 @@ integrate over the entire timestep. Our aim here is not to exhaustively describe all integration methods but rather to give a few examples that elucidate the main considerations one must take into account when choosing a method. Generally, there is a tradeoff between the -accuracy of the method and its computational expense. The expense is driven -almost entirely by the time to compute a transport solution, i.e., to evaluate -:math:`\mathbf{A}` for a given :math:`\mathbf{n}`. Thus, the cost of a method -scales with the number of :math:`\mathbf{A}` evaluations that are performed per -timestep. On the other hand, methods that require more evaluations generally -achieve higher accuracy. The predictor method only requires one evaluation and -its error converges as :math:`\mathcal{O}(h)`. The CE/CM method requires two -evaluations and is thus twice as expensive as the predictor method, but achieves -an error of :math:`\mathcal{O}(h^2)`. An exhaustive description of time -integration methods and their merits can be found in the `thesis of Colin Josey +accuracy of the method and its computational expense. In the case of +transport-coupled depletion, the expense is driven almost entirely by the time +to compute a transport solution, i.e., to evaluate :math:`\mathbf{A}` for a +given :math:`\mathbf{n}`. Thus, the cost of a method scales with the number of +:math:`\mathbf{A}` evaluations that are performed per timestep. On the other +hand, methods that require more evaluations generally achieve higher accuracy. +The predictor method only requires one evaluation and its error converges as +:math:`\mathcal{O}(h)`. The CE/CM method requires two evaluations and is thus +twice as expensive as the predictor method, but achieves an error of +:math:`\mathcal{O}(h^2)`. An exhaustive description of time integration methods +and their merits can be found in the `thesis of Colin Josey `_. OpenMC does not rely on a single time integration method but rather has several @@ -169,12 +170,14 @@ Data Considerations In principle, solving Eq. :eq:`depletion-matrix` using CRAM is fairly simple: just construct the burnup matrix at various times and solve a set of sparse -linear systems. However, constructing the burnup matrix itself involves not only -solving the transport equation to estimate transmutation reaction rates but also -a series of choices about what data to include. In OpenMC, the burnup matrix is -constructed based on data inside of a *depletion chain* file, which includes -fundamental data gathered from ENDF incident neutron, decay, and fission product -yield sublibraries. For each nuclide, this file includes: +linear systems. However, constructing the burnup matrix itself involves not +only solving the transport equation to estimate transmutation reaction rates +(in the case of transport-coupled depletion) or to obtain microscopic cross +sections (in the case of transport-independent depletion), but also a series of +choices about what data to include. In OpenMC, the burnup matrix is constructed +based on data inside of a *depletion chain* file, which includes fundamental +data gathered from ENDF incident neutron, decay, and fission product yield +sublibraries. For each nuclide, this file includes: - What transmutation reactions are possible, their Q values, and their products; - If a nuclide is not stable, what decay modes are possible, their branching @@ -185,9 +188,12 @@ yield sublibraries. For each nuclide, this file includes: Transmutation Reactions ----------------------- -OpenMC will setup tallies in a problem based on what transmutation reactions are -available in a depletion chain file, so any arbitrary number of transmutation -reactions can be tracked. The pregenerated chain files that are available on +In transport-coupled depletion, OpenMC will setup tallies in a problem based on +what transmutation reactions are available in a depletion chain file, so any +arbitrary number of transmutation reactions can be tracked. In +transport-independent depletion, OpenMC will calculate reaction rates for every +reaction that is present in both the available cross sections and the depletion +chain file. The pregenerated chain files that are available on https://openmc.org include the following transmutation reactions: fission, (n,\ :math:`\gamma`\ ), (n,2n), (n,3n), (n,4n), (n,p), and (n,\ :math:`\alpha`\ ). @@ -202,11 +208,12 @@ accurately model the branching of the capture reaction in Am241. This is complicated by the fact that the branching ratio may depend on the incident neutron energy causing capture. -OpenMC does not currently allow energy-dependent capture branching ratios. -However, the depletion chain file does allow a transmutation reaction to be -listed multiple times with different branching ratios resulting in different -products. Spectrum-averaged capture branching ratios have been computed in LWR -and SFR spectra and are available at https://openmc.org/depletion-chains. +OpenMC's transport solver does not currently allow energy-dependent capture +branching ratios. However, the depletion chain file does allow a transmutation +reaction to be listed multiple times with different branching ratios resulting +in different products. Spectrum-averaged capture branching ratios have been +computed in LWR and SFR spectra and are available at +https://openmc.org/depletion-chains. Fission Product Yields ---------------------- @@ -217,26 +224,31 @@ energies. It is an open question as to what the best way to handle this energy dependence is. OpenMC includes three methods for treating the energy dependence of FPY: -1. Use FPY data corresponding to a specified energy. +1. Use FPY data corresponding to a specified energy. This is used by default in + both transport-coupled and transport-independent depletion. 2. Tally fission rates above and below a specified cutoff energy. Assume that all fissions below the cutoff energy correspond to thermal FPY data and all - fission above the cutoff energy correspond to fast FPY data. + fission above the cutoff energy correspond to fast FPY data. Only applicable + to transport-coupled depletion. 3. Compute the average energy at which fission events occur and use an effective FPY by linearly interpolating between FPY provided at neighboring energies. + Only applicable to transport-coupled depletion. -The method can be selected through the ``fission_yield_mode`` argument to the -:class:`openmc.deplete.Operator` constructor. +The method for transport-coupled depletion can be selected through the +``fission_yield_mode`` argument to the :class:`openmc.deplete.CoupledOperator` +constructor. Power Normalization ------------------- -The reaction rates provided OpenMC are given in units of reactions per source -particle. For depletion, it is necessary to compute an absolute reaction rate in -reactions per second. To do so, the reaction rates are normalized based on a -specified power. A complete description of how this normalization can be -performed is described in :ref:`usersguide_tally_normalization`. Here, we simply -note that the main depletion class, :class:`openmc.deplete.Operator`, allows the -user to choose one of two methods for estimating the heating rate, including: +In transport-coupled depletion, the reaction rates provided OpenMC are given in +units of reactions per source particle. For depletion, it is necessary to +compute an absolute reaction rate in reactions per second. To do so, the +reaction rates are normalized based on a specified power. A complete +description of how this normalization can be performed is described in +:ref:`usersguide_tally_normalization`. Here, we simply note that the main +depletion class, :class:`openmc.deplete.CoupledOperator`, allows the user to +choose one of two methods for estimating the heating rate, including: 1. Using fixed Q values from a depletion chain file (useful for comparisons to other codes that use fixed Q values), or @@ -244,4 +256,4 @@ user to choose one of two methods for estimating the heating rate, including: energy-dependent estimate of the true heating rate. The method for normalization can be chosen through the ``normalization_mode`` -argument to the :class:`openmc.deplete.Operator` class. +argument to the :class:`openmc.deplete.CoupledOperator` class. diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 287738774..c7c65e14a 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -61,11 +61,13 @@ Core Functions atomic_mass atomic_weight + combine_distributions decay_constant dose_coefficients gnd_name half_life isotopes + kalbach_slope linearize thin water_density @@ -116,6 +118,7 @@ Angle-Energy Distributions IncoherentElasticAE IncoherentElasticAEDiscrete IncoherentInelasticAEDiscrete + MixedElasticAE Resonance Data -------------- diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 9f7d8c447..dd679a40a 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -15,16 +15,18 @@ are: 1) A transport operator 2) A time-integration scheme -The former is responsible for executing a transport code, like OpenMC, -and retaining important information required for depletion. The most common examples -are reaction rates and power normalization data. The latter is responsible for -projecting reaction rates and compositions forward in calendar time across -some step size :math:`\Delta t`, and obtaining new compositions given a power -or power density. The :class:`Operator` is provided to handle communicating with -OpenMC. Several classes are provided that implement different time-integration -algorithms for depletion calculations, which are described in detail in Colin -Josey's thesis, `Development and analysis of high order neutron -transport-depletion coupling algorithms `_. +The former is responsible for calculating and retaining important information +required for depletion. The most common examples are reaction rates and power +normalization data. The latter is responsible for projecting reaction rates and +compositions forward in calendar time across some step size :math:`\Delta t`, +and obtaining new compositions given a power or power density. The +:class:`CoupledOperator` class is provided to obtain reaction rates via tallies +through OpenMC's transport solver, and the :class:`IndependentOperator` class is +provided to obtain reaction rates from cross-section data. Several classes are +provided that implement different time-integration algorithms for depletion +calculations, which are described in detail in Colin Josey's thesis, +`Development and analysis of high order neutron transport-depletion coupling +algorithms `_. .. autosummary:: :toctree: generated @@ -40,18 +42,20 @@ transport-depletion coupling algorithms `_. SICELIIntegrator SILEQIIntegrator -Each of these classes expects a "transport operator" to be passed. An operator -specific to OpenMC is available using the following class: +Each of these classes expects a "transport operator" to be passed. OpenMC +provides the following transport operator classes: .. autosummary:: :toctree: generated :nosignatures: :template: mycallable.rst - Operator + CoupledOperator + IndependentOperator -The :class:`Operator` must also have some knowledge of how nuclides transmute -and decay. This is handled by the :class:`Chain`. +The :class:`CoupledOperator` and :class:`IndependentOperator` classes must also +have some knowledge of how nuclides transmute and decay. This is handled by the +:class:`Chain` class. Minimal Example --------------- @@ -64,11 +68,12 @@ A minimal example for performing depletion would be: >>> import openmc.deplete >>> geometry = openmc.Geometry.from_xml() >>> settings = openmc.Settings.from_xml() + >>> model = openmc.model.Model(geometry, settings) # Representation of a depletion chain >>> chain_file = "chain_casl.xml" - >>> operator = openmc.deplete.Operator( - ... geometry, settings, chain_file) + >>> operator = openmc.deplete.CoupledOperator( + ... model, chain_file) # Set up 5 time steps of one day each >>> dt = [24 * 60 * 60] * 5 @@ -131,6 +136,7 @@ data, such as number densities and reaction rates for each material. :template: myclass.rst AtomNumber + MicroXS OperatorResult ReactionRates Results @@ -172,7 +178,7 @@ with :func:`cram.CRAM48` being the default. :class:`multiprocessing.pool.Pool` class. If set to ``None`` (default), the number returned by :func:`os.cpu_count` is used. -The following classes are used to help the :class:`openmc.deplete.Operator` +The following classes are used to help the :class:`openmc.deplete.CoupledOperator` compute quantities like effective fission yields, reaction rates, and total system energy. @@ -189,14 +195,45 @@ total system energy. helpers.FissionYieldCutoffHelper helpers.FluxCollapseHelper +The :class:`openmc.deplete.IndependentOperator` uses inner classes subclassed +from those listed above to perform similar calculations. + +Intermediate Classes +-------------------- + +Specific implementations of abstract base classes may utilize some of +the same methods and data structures. These methods and data are stored +in intermediate classes. + +Methods common to tally-based implementation of :class:`FissionYieldHelper` +are stored in :class:`helpers.TalliedFissionYieldHelper` + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + helpers.TalliedFissionYieldHelper + +Methods common to OpenMC-specific implementations of :class:`TransportOperator` +are stored in :class:`openmc_operator.OpenMCOperator` + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: mycallable.rst + + openmc_operator.OpenMCOperator + + Abstract Base Classes --------------------- A good starting point for extending capabilities in :mod:`openmc.deplete` is to examine the following abstract base classes. Custom classes can inherit from :class:`abc.TransportOperator` to implement alternative -schemes for collecting reaction rates and other data from a transport code -prior to depleting materials +schemes for collecting reaction rates and other data prior to depleting +materials .. autosummary:: :toctree: generated @@ -206,7 +243,9 @@ prior to depleting materials abc.TransportOperator The following classes are abstract classes used to pass information from -OpenMC simulations back on to the :class:`abc.TransportOperator` +transport simulations (in the case of transport-coupled depletion) or to +simply calculate these quantities directly (in the case of +transport-independent depletion) back on to the :class:`abc.TransportOperator` .. autosummary:: :toctree: generated @@ -216,7 +255,6 @@ OpenMC simulations back on to the :class:`abc.TransportOperator` abc.NormalizationHelper abc.FissionYieldHelper abc.ReactionRateHelper - abc.TalliedFissionYieldHelper Custom integrators or depletion solvers can be developed by subclassing from the following abstract base classes: diff --git a/docs/source/pythonapi/mgxs.rst b/docs/source/pythonapi/mgxs.rst index bf5a84599..392914b36 100644 --- a/docs/source/pythonapi/mgxs.rst +++ b/docs/source/pythonapi/mgxs.rst @@ -41,6 +41,7 @@ Multi-group Cross Sections openmc.mgxs.KappaFissionXS openmc.mgxs.MultiplicityMatrixXS openmc.mgxs.NuFissionMatrixXS + openmc.mgxs.ReducedAbsorptionXS openmc.mgxs.ScatterXS openmc.mgxs.ScatterMatrixXS openmc.mgxs.ScatterProbabilityMatrix diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 57619611f..229e90a4c 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -4,50 +4,31 @@ Depletion and Transmutation =========================== -OpenMC supports coupled depletion, or burnup, calculations through the -:mod:`openmc.deplete` Python module. OpenMC solves the transport equation to -obtain transmutation reaction rates, and then the reaction rates are used to -solve a set of transmutation equations that determine the evolution of nuclide -densities within a material. The nuclide densities predicted as some future time -are then used to determine updated reaction rates, and the process is repeated -for as many timesteps as are requested. +OpenMC supports transport-coupled and transport-independent depletion, or +burnup, calculations through the :mod:`openmc.deplete` Python module. OpenMC +uses transmutation reaction rates to solve a set of transmutation equations +that determine the evolution of nuclide densities within a material. The +nuclide densities predicted at some future time are then used to determine +updated reaction rates, and the process is repeated for as many timesteps as +are requested. -The depletion module is designed such that the flux/reaction rate solution (the +The depletion module is designed such that the reaction rate solution (the transport "operator") is completely isolated from the solution of the -transmutation equations and the method used for advancing time. At present, the -:mod:`openmc.deplete` module offers a single transport operator, -:class:`openmc.deplete.Operator` (which uses the OpenMC transport solver), but -in principle additional operator classes based on other transport codes could be -implemented and no changes to the depletion solver itself would be needed. The -operator class requires a :class:`openmc.Geometry` instance and a -:class:`openmc.Settings` instance:: - - geom = openmc.Geometry() - settings = openmc.Settings() - ... - - op = openmc.deplete.Operator(geom, settings) - -Any material that contains a fissionable nuclide is depleted by default, but -this can behavior can be changed with the :attr:`Material.depletable` attribute. - -.. important:: The volume must be specified for each material that is depleted by - setting the :attr:`Material.volume` attribute. This is necessary - in order to calculate the proper normalization of tally results - based on the source rate. +transmutation equations and the method used for advancing time. :mod:`openmc.deplete` supports multiple time-integration methods for determining material compositions over time. Each method appears as a different class. For example, :class:`openmc.deplete.CECMIntegrator` runs a depletion calculation using the CE/CM algorithm (deplete over a timestep using the middle-of-step -reaction rates). An instance of :class:`openmc.deplete.Operator` is passed to -one of these functions along with the timesteps and power level:: +reaction rates). An instance of :class:`~openmc.deplete.abc.TransportOperator` +is passed to one of these Integrator classes along with the timesteps and power +level:: power = 1200.0e6 # watts timesteps = [10.0, 10.0, 10.0] # days openmc.deplete.CECMIntegrator(op, timesteps, power, timestep_units='d').integrate() -The coupled transport-depletion problem is executed, and once it is done a +The depletion problem is executed, and once it is done a ``depletion_results.h5`` file is written. The results can be analyzed using the :class:`openmc.deplete.Results` class. This class has methods that allow for easy retrieval of k-effective, nuclide concentrations, and reaction rates over @@ -56,11 +37,41 @@ time:: results = openmc.deplete.Results("depletion_results.h5") time, keff = results.get_keff() -Note that the coupling between the transport solver and the transmutation solver -happens in-memory rather than by reading/writing files on disk. +Note that the coupling between the reaction rate solver and the transmutation +solver happens in-memory rather than by reading/writing files on disk. OpenMC +has two categories of transport operators for obtaining transmutation reaction +rates. + +.. _coupled-depletion: + +Transport-coupled depletion +=========================== + +This category of operator solves the transport equation to obtain transmutation +reaction rates. At present, the :mod:`openmc.deplete` module offers a single +transport-coupled operator, :class:`openmc.deplete.CoupledOperator` (which uses +the OpenMC transport solver), but in principle additional transport-coupled +operator classes based on other transport codes could be implemented and no +changes to the depletion solver itself would be needed. The +:class:`openmc.deplete.CoupledOperator` class requires a :class:`~openmc.Model` +instance containing material, geometry, and settings information:: + + model = openmc.Model() + ... + + op = openmc.deplete.CoupledOperator(model) + +Any material that contains a fissionable nuclide is depleted by default, but +this can behavior can be changed with the :attr:`Material.depletable` attribute. + +.. important:: + + The volume must be specified for each material that is depleted by setting + the :attr:`Material.volume` attribute. This is necessary in order to + calculate the proper normalization of tally results based on the source rate. Fixed-Source Transmutation -========================== +-------------------------- When the ``power`` or ``power_density`` argument is used for one of the Integrator classes, it is assumed that OpenMC is running in k-eigenvalue mode, @@ -77,11 +88,11 @@ using the :attr:`Material.depletable` attribute:: mat = openmc.Material() mat.depletable = True -When constructing the :class:`~openmc.deplete.Operator`, you should indicate -that normalization of tally results will be done based on the source rate rather -than a power or power density:: +When constructing the :class:`~openmc.deplete.CoupledOperator`, you should +indicate that normalization of tally results will be done based on the source +rate rather than a power or power density:: - op = openmc.deplete.Operator(geometry, settings, normalization_mode='source-rate') + op = openmc.deplete.CoupledOperator(model, normalization_mode='source-rate') Finally, when creating a depletion integrator, use the ``source_rates`` argument:: @@ -92,19 +103,22 @@ timestep in the calculation. A zero source rate for a given timestep will result in a decay-only step, where all reaction rates are zero. Caveats -======= +------- + +.. _energy-deposition: Energy Deposition ------------------ +~~~~~~~~~~~~~~~~~ The default energy deposition mode, ``"fission-q"``, instructs the -:class:`openmc.deplete.Operator` to normalize reaction rates using the product -of fission reaction rates and fission Q values taken from the depletion chain. -This approach does not consider indirect contributions to energy deposition, -such as neutron heating and energy from secondary photons. In doing this, the -energy deposited during a transport calculation will be lower than expected. -This causes the reaction rates to be over-adjusted to hit the user-specific -power, or power density, leading to an over-depletion of burnable materials. +:class:`~openmc.deplete.CoupledOperator` to normalize reaction rates using the +product of fission reaction rates and fission Q values taken from the depletion +chain. This approach does not consider indirect contributions to energy +deposition, such as neutron heating and energy from secondary photons. In doing +this, the energy deposited during a transport calculation will be lower than +expected. This causes the reaction rates to be over-adjusted to hit the +user-specific power, or power density, leading to an over-depletion of burnable +materials. There are some remedies. First, the fission Q values can be directly set in a variety of ways. This requires knowing what the total fission energy release @@ -113,29 +127,32 @@ should be, including indirect components. Some examples are provided below:: # use a dictionary of fission_q values fission_q = {"U235": 202e+6} # energy in eV + # create a Model object + model = openmc.Model(geometry, settings) + # create a modified chain and write it to a new file chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q) chain.export_to_xml("chain_mod_q.xml") - op = openmc.deplete.Operator(geometry, setting, "chain_mod_q.xml") + op = openmc.deplete.CoupledOperator(model, "chain_mod_q.xml") # alternatively, pass the modified fission Q directly to the operator - op = openmc.deplete.Operator(geometry, setting, "chain.xml", + op = openmc.deplete.CoupledOperator(model, "chain.xml", fission_q=fission_q) A more complete way to model the energy deposition is to use the modified -heating reactions described in :ref:`methods_heating`. These values can be used +heating reactions described in :ref:`methods_heating`. These values can be used to normalize reaction rates instead of using the fission reaction rates with:: - op = openmc.deplete.Operator(geometry, settings, "chain.xml", + op = openmc.deplete.CoupledOperator(model, "chain.xml", normalization_mode="energy-deposition") These modified heating libraries can be generated by running the latest version -of :meth:`openmc.data.IncidentNeutron.from_njoy`, and will eventually be bundled +of :meth:`openmc.data.IncidentNeutron.from_njoy()`, and will eventually be bundled into the distributed libraries. Local Spectra and Repeated Materials ------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It is not uncommon to explicitly create a single burnable material across many locations. From a pure transport perspective, there is nothing wrong with @@ -160,7 +177,7 @@ the next transport step. This can be countered by instructing the operator to treat repeated instances of the same material as a unique material definition with:: - op = openmc.deplete.Operator(geometry, settings, chain_file, + op = openmc.deplete.CoupledOperator(model, chain_file, diff_burnable_mats=True) For our example problem, this would deplete fuel on the outer region of the @@ -177,3 +194,164 @@ across all material instances. This will increase the total memory usage and run time due to an increased number of tallies and material definitions. +Transport-independent depletion +=============================== + +.. warning:: + + This feature is still under heavy development and has yet to be rigorously + verified. API changes and feature additions are possible and likely in + the near future. + +This category of operator uses pre-calculated one-group microscopic cross +sections to obtain transmutation reaction rates. OpenMC provides the +:class:`~openmc.deplete.IndependentOperator` for this method of calculation. +While the one-group microscopic cross sections can be calculated using a +transport solver, :class:`~openmc.deplete.IndependentOperator` is not directly +coupled to any transport solver. The +:class:`~openmc.deplete.IndependentOperator` class requires a +:class:`openmc.Materials` object, a :class:`~openmc.deplete.MicroXS` object, +and a path to a depletion chain file:: + + # load in the microscopic cross sections + materials = openmc.Materials() + ... + + micro_xs = openmc.deplete.MicroXS.from_csv(micro_xs_path) + op = openmc.deplete.IndependentOperator(materials, micro_xs, chain_file) + +.. note:: + + The same statements from :ref:`coupled-depletion` about which + materials are depleted and the requirement for depletable materials to have + a specified volume also apply here. + +An alternate constructor, +:meth:`~openmc.deplete.IndependentOperator.from_nuclides`, accepts a volume and +dictionary of nuclide concentrations in place of the :class:`openmc.Materials` +object:: + + nuclides = {'U234': 8.92e18, + 'U235': 9.98e20, + 'U238': 2.22e22, + 'U236': 4.57e18, + 'O16': 4.64e22, + 'O17': 1.76e19} + volume = 0.5 + op = openmc.deplete.IndependentOperator.from_nuclides(volume, + nuclides, + micro_xs, + chain_file, + nuc_units='atom/cm3') + +A user can then define an integrator class as they would for a coupled +transport-depletion calculation and follow the same steps from there. + +.. note:: + + Ideally, one-group cross section data should be available for every + reaction in the depletion chain. If a nuclide that has a reaction + associated with it in the depletion chain is present in the `nuclides` + parameter but not the cross section data, that reaction will not be + simulated. + +Generating Microscopic Cross Sections +------------------------------------- + +Users can generate the one-group microscopic cross sections needed by +:class:`~openmc.deplete.IndependentOperator` using the +:class:`~openmc.deplete.MicroXS` class:: + + import openmc + + model = openmc.Model.from_xml() + + micro_xs = openmc.deplete.MicroXS.from_model(model, + model.materials[0], + chain_file) + +The :meth:`~openmc.deplete.MicroXS.from_model()` method will produce a +:class:`~openmc.deplete.MicroXS` object with microscopic cross section data in +units of barns, which is what :class:`~openmc.deplete.IndependentOperator` +expects the units to be. The :class:`~openmc.deplete.MicroXS` class also +includes functions to read in cross section data directly from a ``.csv`` file +or from data arrays:: + + micro_xs = MicroXS.from_csv(micro_xs_path) + + nuclides = ['U234', 'U235', 'U238'] + reactions = ['fission', '(n,gamma)'] + data = np.array([[0.1, 0.2], + [0.3, 0.4], + [0.01, 0.5]]) + micro_xs = MicroXS.from_array(nuclides, reactions, data) + +.. important:: + + Both :meth:`~openmc.deplete.MicroXS.from_csv()` and + :meth:`~openmc.deplete.MicroXS.from_array()` assume the cross section values + provided are in barns by defualt, but have no way of verifying this. Make + sure your cross sections are in the correct units before passing to a + :class:`~openmc.deplete.IndependentOperator` object. + +Caveats +------- + +Reaction Rate Normalization +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`~openmc.deplete.IndependentOperator` class supports two methods for +normalizing reaction rates: + +.. important:: + + Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` + class. Use the ``source_rates`` parameter when + ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` + when ``normalization_mode == fission-q``. + +1. ``source-rate`` normalization, which assumes the ``source_rate`` provided by + the time integrator is a flux, and obtains the reaction rates by multiplying + the cross-sections by the ``source-rate``. +2. ``fission-q`` normalization, which uses the ``power`` or ``power_density`` + provided by the time integrator to obtain reaction rates by computing a value + for the flux based on this power. The general equation for the flux is + + .. math:: + + \phi = \frac{P}{V \cdot \sum_i (Q_i \cdot \sigma^f_i \cdot \n_i)} + + where :math:`\sum_i` is the sum over all nuclides :math:`i`. This equation + makes the same assumptions and issues as discussed in + :ref:`energy-deposition`. Unfortunately, the proposed solution in that + section does not apply here since we are decoupled from transport code. + However, there is a method to converge to a more accurate value for flux by + using substeps during time integration. + `This paper `_ provides a + good discussion of this method. + +.. warning:: + + The accuracy of results when using ``fission-q`` is entirely dependent on + your depletion chain. Make sure it has sufficient data to resolve the + dynamics of your particular scenario. + +Multiple Materials +~~~~~~~~~~~~~~~~~~ + +Running a depletion simulation with multiple materials using the +``source-rate`` normalization method treats each material as completely +separate with respect to reaction rates. This can be useful for running many +different cases of a particular scenario. However, running a depletion +simulation with multiple materials using the ``fission-q`` normalization method +treats each material as part of the same "reactor" due to how ``fission-q`` +normalization accumulates energy values from each material to a single value. +This behavior may change in the future. + +Time integration +~~~~~~~~~~~~~~~~ + +The one-group microscopic cross sections passed to +:class:`openmc.deplete.IndependentOperator` are fixed values for the entire +depletion simulation. This implicit assumption may produce inaccurate results +for certain scenarios. diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 73f96ff95..a7362ea9e 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -24,7 +24,7 @@ using double_4dvec = vector>>>; constexpr int HDF5_VERSION[] {3, 0}; // Version numbers for binary files -constexpr array VERSION_STATEPOINT {17, 0}; +constexpr array VERSION_STATEPOINT {18, 0}; constexpr array VERSION_PARTICLE_RESTART {2, 0}; constexpr array VERSION_TRACK {3, 0}; constexpr array VERSION_SUMMARY {6, 0}; diff --git a/include/openmc/endf.h b/include/openmc/endf.h index 7beb8e452..e580874b4 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -126,6 +126,26 @@ private: debye_waller_; //!< Debye-Waller integral divided by atomic mass in [eV^-1] }; +//============================================================================== +//! Sum of multiple 1D functions +//============================================================================== + +class Sum1D : public Function1D { +public: + // Constructors + explicit Sum1D(hid_t group); + + //! Evaluate each function and sum results + //! \param[in] x independent variable + //! \return Function evaluated at x + double operator()(double E) const override; + + const unique_ptr& functions(int i) const { return functions_[i]; } + +private: + vector> functions_; //!< individual functions +}; + //! Read 1D function from HDF5 dataset //! \param[in] group HDF5 group containing dataset //! \param[in] name Name of dataset diff --git a/include/openmc/hdf5_interface.h b/include/openmc/hdf5_interface.h index e9d9b4f96..0092c08f8 100644 --- a/include/openmc/hdf5_interface.h +++ b/include/openmc/hdf5_interface.h @@ -61,6 +61,8 @@ void ensure_exists(hid_t obj_id, const char* name, bool attribute = false); vector group_names(hid_t group_id); vector object_shape(hid_t obj_id); std::string object_name(hid_t obj_id); +hid_t open_object(hid_t group_id, const std::string& name); +void close_object(hid_t obj_id); //============================================================================== // Fortran compatibility functions diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 21c58862d..eece0f1ff 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -7,13 +7,14 @@ #include #include "hdf5.h" -#include "pugixml.hpp" #include "xtensor/xtensor.hpp" +#include "pugixml.hpp" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/vector.h" +#include "openmc/xml_interface.h" #ifdef DAGMC #include "moab/AdaptiveKDTree.hpp" @@ -36,6 +37,12 @@ namespace openmc { +//============================================================================== +// Constants +//============================================================================== + +enum class ElementType { UNSUPPORTED=-1, LINEAR_TET, LINEAR_HEX }; + //============================================================================== // Global variables //============================================================================== @@ -53,7 +60,7 @@ extern vector> meshes; #ifdef LIBMESH namespace settings { -// used when creating new libMesh::Mesh instances +// used when creating new libMesh::MeshBase instances extern unique_ptr libmesh_init; extern const libMesh::Parallel::Communicator* libmesh_comm; } // namespace settings @@ -485,6 +492,23 @@ public: //! \return The centroid of the bin virtual Position centroid(int bin) const = 0; + //! Get the number of vertices in the mesh + // + //! \return Number of vertices + virtual int n_vertices() const = 0; + + //! Retrieve a vertex of the mesh + // + //! \param[in] vertex ID + //! \return vertex coordinates + virtual Position vertex(int id) const = 0; + + //! Retrieve connectivity of a mesh element + // + //! \param[in] element ID + //! \return element connectivity as IDs of the vertices + virtual std::vector connectivity(int id) const = 0; + //! Get the volume of a mesh bin // //! \param[in] bin Bin to return the volume for @@ -557,6 +581,12 @@ public: Position centroid(int bin) const override; + int n_vertices() const override; + + Position vertex(int id) const override; + + std::vector connectivity(int id) const override; + double volume(int bin) const override; private: @@ -621,6 +651,9 @@ private: //! \return MOAB EntityHandle of tet moab::EntityHandle get_ent_handle_from_bin(int bin) const; + //! Get a vertex index into the global range from a handle + int get_vert_idx_from_handle(moab::EntityHandle vert) const; + //! Get the bin for a given mesh cell index // //! \param[in] idx Index of the mesh cell. @@ -655,6 +688,7 @@ private: // Data members moab::Range ehs_; //!< Range of tetrahedra EntityHandle's in the mesh + moab::Range verts_; //!< Range of vertex EntityHandle's in the mesh moab::EntityHandle tetset_; //!< EntitySet containing all tetrahedra moab::EntityHandle kdtree_root_; //!< Root of the MOAB KDTree std::shared_ptr mbi_; //!< MOAB instance @@ -671,7 +705,8 @@ class LibMesh : public UnstructuredMesh { public: // Constructors LibMesh(pugi::xml_node node); - LibMesh(const std::string& filename, double length_multiplier = 1.0); + LibMesh(const std::string & filename, double length_multiplier = 1.0); + LibMesh(libMesh::MeshBase & input_mesh, double length_multiplier = 1.0); static const std::string mesh_lib_type; @@ -701,10 +736,19 @@ public: Position centroid(int bin) const override; + int n_vertices() const override; + + Position vertex(int id) const override; + + std::vector connectivity(int id) const override; + double volume(int bin) const override; + libMesh::MeshBase* mesh_ptr() const { return m_; }; + private: void initialize() override; + void set_mesh_pointer_from_filename(const std::string& filename); // Methods @@ -715,7 +759,8 @@ private: int get_bin_from_element(const libMesh::Elem* elem) const; // Data members - unique_ptr m_; //!< pointer to the libMesh mesh instance + unique_ptr unique_m_ = nullptr; //!< pointer to the libMesh MeshBase instance, only used if mesh is created inside OpenMC + libMesh::MeshBase* m_; //!< pointer to libMesh MeshBase instance, always set during intialization vector> pl_; //!< per-thread point locators unique_ptr diff --git a/include/openmc/secondary_thermal.h b/include/openmc/secondary_thermal.h index 84ed68f51..5b18902af 100644 --- a/include/openmc/secondary_thermal.h +++ b/include/openmc/secondary_thermal.h @@ -150,6 +150,34 @@ private: //!< each incident energy }; +//============================================================================== +//! Mixed coherent/incoherent elastic angle-energy distribution +//============================================================================== + +class MixedElasticAE : public AngleEnergy { +public: + //! Construct from HDF5 file + // + //! \param[in] group HDF5 group + explicit MixedElasticAE( + hid_t group, const CoherentElasticXS& coh_xs, const Function1D& incoh_xs); + + //! Sample distribution for an angle and energy + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] mu Outgoing cosine with respect to current direction + //! \param[inout] seed Pseudorandom number seed pointer + void sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const override; + +private: + CoherentElasticAE coherent_dist_; //!< Coherent distribution + unique_ptr incoherent_dist_; //!< Incoherent distribution + + const CoherentElasticXS& coherent_xs_; //!< Ref. to coherent XS + const Function1D& incoherent_xs_; //!< Polymorphic ref. to incoherent XS +}; + } // namespace openmc #endif // OPENMC_SECONDARY_THERMAL_H diff --git a/openmc/data/angle_energy.py b/openmc/data/angle_energy.py index 6009dc748..b8fde5478 100644 --- a/openmc/data/angle_energy.py +++ b/openmc/data/angle_energy.py @@ -44,6 +44,8 @@ class AngleEnergy(EqualityMixin, ABC): return openmc.data.IncoherentInelasticAEDiscrete.from_hdf5(group) elif dist_type == 'incoherent_inelastic': return openmc.data.IncoherentInelasticAE.from_hdf5(group) + elif dist_type == 'mixed_elastic': + return openmc.data.MixedElasticAE.from_hdf5(group) @staticmethod def from_ace(ace, location_dist, location_start, rx=None): diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 51a594bc8..2c2505bf5 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -9,7 +9,9 @@ from uncertainties import ufloat, UFloat import openmc.checkvalue as cv from openmc.mixin import EqualityMixin +from openmc.stats import Discrete, Tabular, combine_distributions from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER +from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -314,6 +316,12 @@ class Decay(EqualityMixin): 'excited_state', 'mass', 'stable', 'spin', and 'parity'. spectra : dict Resulting radiation spectra for each radiation type. + sources : dict + Radioactive decay source distributions represented as a dictionary + mapping particle types (e.g., 'photon') to instances of + :class:`openmc.stats.Univariate`. + + .. versionadded:: 0.13.1 """ def __init__(self, ev_or_filename): @@ -329,6 +337,7 @@ class Decay(EqualityMixin): self.modes = [] self.spectra = {} self.average_energies = {} + self._sources = None # Get head record items = get_head_record(file_obj) @@ -495,3 +504,69 @@ class Decay(EqualityMixin): """ return cls(ev_or_filename) + + @property + def sources(self): + """Radioactive decay source distributions""" + # If property has been computed already, return it + # TODO: Replace with functools.cached_property when support is Python 3.9+ + if self._sources is not None: + return self._sources + + sources = {} + name = self.nuclide['name'] + decay_constant = self.decay_constant.n + for particle, spectra in self.spectra.items(): + # Set particle type based on 'particle' above + particle_type = { + 'gamma': 'photon', + 'beta-': 'electron', + 'ec/beta+': 'positron', + 'alpha': 'alpha', + 'n': 'neutron', + 'sf': 'fragment', + 'p': 'proton', + 'e-': 'electron', + 'xray': 'photon', + 'anti-neutrino': 'anti-neutrino', + 'neutrino': 'neutrino', + }[particle] + + if particle_type not in sources: + sources[particle_type] = [] + + # Create distribution for discrete + if spectra['continuous_flag'] in ('discrete', 'both'): + energies = [] + intensities = [] + for discrete_data in spectra['discrete']: + energies.append(discrete_data['energy'].n) + intensities.append(discrete_data['intensity'].n) + energies = np.array(energies) + intensity = spectra['discrete_normalization'].n + rates = decay_constant * intensity * np.array(intensities) + dist_discrete = Discrete(energies, rates) + sources[particle_type].append(dist_discrete) + + # Create distribution for continuous + if spectra['continuous_flag'] in ('continuous', 'both'): + f = spectra['continuous']['probability'] + if len(f.interpolation) > 1: + raise NotImplementedError("Multiple interpolation regions: {name}, {particle}") + interpolation = INTERPOLATION_SCHEME[f.interpolation[0]] + if interpolation not in ('histogram', 'linear-linear'): + raise NotImplementedError("Continuous spectra with {interpolation} interpolation ({name}, {particle}) not supported") + + intensity = spectra['continuous_normalization'].n + rates = decay_constant * intensity * f.y + dist_continuous = Tabular(f.x, rates, interpolation) + sources[particle_type].append(dist_continuous) + + # Combine discrete distributions + merged_sources = {} + for particle_type, dist_list in sources.items(): + merged_sources[particle_type] = combine_distributions( + dist_list, [1.0]*len(dist_list)) + + self._sources = merged_sources + return self._sources diff --git a/openmc/data/function.py b/openmc/data/function.py index b5aa2117d..b0390d19c 100644 --- a/openmc/data/function.py +++ b/openmc/data/function.py @@ -544,7 +544,7 @@ class Combination(EqualityMixin): self._operations = operations -class Sum(EqualityMixin): +class Sum(Function1D): """Sum of multiple functions. This class allows you to create a callable object which represents the sum @@ -578,6 +578,49 @@ class Sum(EqualityMixin): cv.check_type('functions', functions, Iterable, Callable) self._functions = functions + def to_hdf5(self, group, name='xy'): + """Write sum of functions to an HDF5 group + + .. versionadded:: 0.13.1 + + Parameters + ---------- + group : h5py.Group + HDF5 group to write to + name : str + Name of the dataset to create + + """ + sum_group = group.create_group(name) + sum_group.attrs['type'] = np.string_(type(self).__name__) + sum_group.attrs['n'] = len(self.functions) + for i, f in enumerate(self.functions): + f.to_hdf5(sum_group, f'func_{i+1}') + + @classmethod + def from_hdf5(cls, group): + """Generate sum of functions from an HDF5 group + + .. versionadded:: 0.13.1 + + Parameters + ---------- + group : h5py.Group + Group to read from + + Returns + ------- + openmc.data.Sum + Functions read from the group + + """ + n = group.attrs['n'] + functions = [ + Function1D.from_hdf5(group[f'func_{i+1}']) + for i in range(n) + ] + return cls(functions) + class Regions1D(EqualityMixin): r"""Piecewise composition of multiple functions. diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index f4c914d90..a036da694 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -5,6 +5,7 @@ from warnings import warn import numpy as np import openmc.checkvalue as cv +from openmc.mixin import EqualityMixin from openmc.stats import Tabular, Univariate, Discrete, Mixture from .function import Tabulated1D, INTERPOLATION_SCHEME from .angle_energy import AngleEnergy @@ -12,6 +13,240 @@ from .data import EV_PER_MEV from .endf import get_list_record, get_tab2_record +class _AtomicRepresentation(EqualityMixin): + """Atomic representation of an isotope or a particle. + + Parameters + ---------- + z : int + Number of protons (atomic number) + a : int + Number of nucleons (mass number) + + Raises + ------ + ValueError + When the number of protons (z) declared is higher than the number + of nucleons (a) + + Attributes + ---------- + z : int + Number of protons (atomic number) + a : int + Number of nucleons (mass number) + n : int + Number of neutrons + za : int + ZA identifier, 1000*Z + A, where Z is the atomic number and A the mass + number + + """ + def __init__(self, z, a): + # Sanity checks on values + cv.check_type('z', z, Integral) + cv.check_greater_than('z', z, 0, equality=True) + cv.check_type('a', a, Integral) + cv.check_greater_than('a', a, 0, equality=True) + if z > a: + raise ValueError(f"Number of protons ({z}) must be less than or " + f"equal to number of nucleons ({a}).") + + self._z = z + self._a = a + + def __add__(self, other): + """Add two _AtomicRepresentations""" + z = self.z + other.z + a = self.a + other.a + return _AtomicRepresentation(z=z, a=a) + + def __sub__(self, other): + """Substract two _AtomicRepresentations""" + z = self.z - other.z + a = self.a - other.a + return _AtomicRepresentation(z=z, a=a) + + @property + def a(self): + return self._a + + @property + def z(self): + return self._z + + @property + def n(self): + return self.a - self.z + + @property + def za(self): + return self.z * 1000 + self.a + + @classmethod + def from_za(cls, za): + """Instantiate an _AtomicRepresentation from a ZA identifier. + + Parameters + ---------- + za : int + ZA identifier, 1000*Z + A, where Z is the atomic number and A the + mass number + + Returns + ------- + _AtomicRepresentation + Atomic representation of the isotope/particle + + """ + z, a = divmod(za, 1000) + return cls(z, a) + + +def _separation_energy(compound, nucleus, particle): + """Calculates the separation energy as defined in ENDF-6 manual + BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 + and LANG=2. This function can be used for the incident or emitted + particle of the following reaction: A + a -> C -> B + b + + Parameters + ---------- + compound : _AtomicRepresentation + Atomic representation of the compound (C) + nucleus : _AtomicRepresentation + Atomic representation of the nucleus (A or B) + particle : _AtomicRepresentation + Atomic representation of the particle (a or b) + + Returns + ------- + separation_energy : float + Separation energy in MeV + + """ + # Determine A, Z, and N for compound and nucleus + A_c = compound.a + Z_c = compound.z + N_c = compound.n + A_a = nucleus.a + Z_a = nucleus.z + N_a = nucleus.n + + # Determine breakup energy of incident particle (ENDF-6 Formats Manual, + # Appendix H, Table 3) in MeV + za_to_breaking_energy = { + 1: 0.0, + 1001: 0.0, + 1002: 2.224566, + 1003: 8.481798, + 2003: 7.718043, + 2004: 28.29566 + } + I_a = za_to_breaking_energy[particle.za] + + # Eq. 4 in in doi:10.1103/PhysRevC.37.2350 or ENDF-6 Formats Manual section + # 6.2.3.2 + return ( + 15.68 * (A_c - A_a) - + 28.07 * ((N_c - Z_c)**2 / A_c - (N_a - Z_a)**2 / A_a) - + 18.56 * (A_c**(2./3.) - A_a**(2./3.)) + + 33.22 * ((N_c - Z_c)**2 / A_c**(4./3.) - (N_a - Z_a)**2 / A_a**(4./3.)) - + 0.717 * (Z_c**2 / A_c**(1./3.) - Z_a**2 / A_a**(1./3.)) + + 1.211 * (Z_c**2 / A_c - Z_a**2 / A_a) - + I_a + ) + + +def kalbach_slope(energy_projectile, energy_emitted, za_projectile, + za_emitted, za_target): + """Returns Kalbach-Mann slope from calculations. + + The associated reaction is defined as: + A + a -> C -> B + b + + Where: + + - A is the targeted nucleus, + - a is the projectile, + - C is the compound, + - B is the residual nucleus, + - b is the emitted particle. + + The Kalbach-Mann slope calculation is done as defined in ENDF-6 manual + BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 and + LANG=2. One exception to this, is that the entrance and emission channel + energies are not calculated with the AWR number, but approximated with + the number of mass instead. + + Parameters + ---------- + energy_projectile : float + Energy of the projectile in the laboratory system in eV + energy_emitted : float + Energy of the emitted particle in the center of mass system in eV + za_projectile : int + ZA identifier of the projectile + za_emitted : int + ZA identifier of the emitted particle + za_target : int + ZA identifier of the targeted nucleus + + Raises + ------ + NotImplementedError + When the projectile is not a neutron + + Returns + ------- + slope : float + Kalbach-Mann slope given with the same format as ACE file. + + """ + # TODO: develop for photons as projectile + # TODO: test for other particles than neutron + if za_projectile != 1: + raise NotImplementedError( + "Developed and tested for neutron projectile only." + ) + + # Special handling of elemental carbon + if za_emitted == 6000: + za_emitted = 6012 + if za_target == 6000: + za_target = 6012 + + projectile = _AtomicRepresentation.from_za(za_projectile) + emitted = _AtomicRepresentation.from_za(za_emitted) + target = _AtomicRepresentation.from_za(za_target) + compound = projectile + target + residual = compound - emitted + + # Calculate entrance and emission channel energy in MeV, defined in section + # 6.2.3.2 in the ENDF-6 Formats Manual + epsilon_a = energy_projectile * target.a / (target.a + projectile.a) / EV_PER_MEV + epsilon_b = energy_emitted * (residual.a + emitted.a) \ + / (residual.a * EV_PER_MEV) + + # Calculate separation energies using Eq. 4 in doi:10.1103/PhysRevC.37.2350 + # or ENDF-6 Formats Manual section 6.2.3.2 + s_a = _separation_energy(compound, target, projectile) + s_b = _separation_energy(compound, residual, emitted) + + # See Eq. 10 in doi:10.1103/PhysRevC.37.2350 or section 6.2.3.2 in the + # ENDF-6 Formats Manual + za_to_M = {1: 1.0, 1001: 1.0, 1002: 1.0, 2004: 0.0} + za_to_m = {1: 0.5, 1001: 1.0, 1002: 1.0, 1003: 1.0, 2003: 1.0, 2004: 2.0} + M = za_to_M[projectile.za] + m = za_to_m[emitted.za] + e_a = epsilon_a + s_a + e_b = epsilon_b + s_b + r_1 = min(e_a, 130.) + r_3 = min(e_a, 41.) + x_1 = r_1 * e_b / e_a + x_3 = r_3 * e_b / e_a + return 0.04 * x_1 + 1.8e-6 * x_1**3 + 6.7e-7 * M * m * x_3**4 + + class KalbachMann(AngleEnergy): """Kalbach-Mann distribution @@ -319,7 +554,7 @@ class KalbachMann(AngleEnergy): n_energy_out = int(ace.xss[idx + 1]) data = ace.xss[idx + 2:idx + 2 + 5*n_energy_out].copy() data.shape = (5, n_energy_out) - data[0,:] *= EV_PER_MEV + data[0, :] *= EV_PER_MEV # Create continuous distribution eout_continuous = Tabular(data[0][n_discrete_lines:], @@ -352,13 +587,28 @@ class KalbachMann(AngleEnergy): return cls(breakpoints, interpolation, energy, energy_out, km_r, km_a) @classmethod - def from_endf(cls, file_obj): - """Generate Kalbach-Mann distribution from an ENDF evaluation + def from_endf(cls, file_obj, za_emitted, za_target, projectile_mass): + """Generate Kalbach-Mann distribution from an ENDF evaluation. + + If the projectile is a neutron, the slope is calculated when it is + not given explicitly. Parameters ---------- file_obj : file-like object ENDF file positioned at the start of the Kalbach-Mann distribution + za_emitted : int + ZA identifier of the emitted particle + za_target : int + ZA identifier of the target + projectile_mass : float + Mass of the projectile + + Warns + ----- + UserWarning + If the mass of the projectile is not equal to 1 (other than + a neutron), the slope is not calculated and set to 0 if missing. Returns ------- @@ -374,6 +624,7 @@ class KalbachMann(AngleEnergy): energy_out = [] precompound = [] slope = [] + calculated_slope = [] for i in range(ne): items, values = get_list_record(file_obj) energy[i] = items[1] @@ -385,19 +636,46 @@ class KalbachMann(AngleEnergy): values.shape = (n_energy_out, n_angle + 2) # Outgoing energy distribution at the i-th incoming energy - eout_i = values[:,0] - eout_p_i = values[:,1] + eout_i = values[:, 0] + eout_p_i = values[:, 1] energy_out_i = Tabular(eout_i, eout_p_i, INTERPOLATION_SCHEME[lep]) energy_out.append(energy_out_i) - # Precompound and slope factors for Kalbach-Mann - r_i = values[:,2] + # Precompound factors for Kalbach-Mann + r_i = values[:, 2] + + # Slope factors for Kalbach-Mann if n_angle == 2: - a_i = values[:,3] + a_i = values[:, 3] + calculated_slope.append(False) else: - a_i = np.zeros_like(r_i) + # Check if the projectile is not a neutron + if not np.isclose(projectile_mass, 1.0, atol=1.0e-12, rtol=0.): + warn( + "Kalbach-Mann slope calculation is only available with " + "neutrons as projectile. Slope coefficients are set to 0." + ) + a_i = np.zeros_like(r_i) + calculated_slope.append(False) + + else: + # TODO: retrieve ZA of the projectile + za_projectile = 1 + a_i = [kalbach_slope(energy_projectile=energy[i], + energy_emitted=e, + za_projectile=za_projectile, + za_emitted=za_emitted, + za_target=za_target) + for e in eout_i] + calculated_slope.append(True) + precompound.append(Tabulated1D(eout_i, r_i)) slope.append(Tabulated1D(eout_i, a_i)) - return cls(tab2.breakpoints, tab2.interpolation, energy, - energy_out, precompound, slope) + km_distribution = cls(tab2.breakpoints, tab2.interpolation, energy, + energy_out, precompound, slope) + + # List of bool to indicate slope calculation by OpenMC + km_distribution._calculated_slope = calculated_slope + + return km_distribution diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 305edaf4f..d8ecaae18 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -127,7 +127,7 @@ acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%% 1 0 1 .{ext} / '{library}: {zsymam} at {temperature}'/ {mat} {temperature} -1 1/ +1 1 {ismooth}/ / """ @@ -248,7 +248,8 @@ def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): def make_ace(filename, temperatures=None, acer=True, xsdir=None, output_dir=None, pendf=False, error=0.001, broadr=True, - heatr=True, gaspr=True, purr=True, evaluation=None, **kwargs): + heatr=True, gaspr=True, purr=True, evaluation=None, + smoothing=True, **kwargs): """Generate incident neutron ACE file from an ENDF file File names can be passed to @@ -298,6 +299,8 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, evaluation : openmc.data.endf.Evaluation, optional If the ENDF file contains multiple material evaluations, this argument indicates which evaluation should be used. + smoothing : bool, optional + If the smoothing option (ACER card 6) is on (True) or off (False). **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` @@ -380,6 +383,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # acer if acer: + ismooth = int(smoothing) nacer_in = nlast for i, temperature in enumerate(temperatures): # Extend input with an ACER run for each temperature diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 5e4287f16..a2431cf1c 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -80,6 +80,14 @@ def _get_products(ev, mt): mt : int The MT value of the reaction to get products for + Raises + ------ + IOError + When the Kalbach-Mann systematics is used, but the product + is not defined in the 'center-of-mass' system. The breakup logic + is not implemented which can lead to this error being raised while + the definition of the product is correct. + Returns ------- products : list of openmc.data.Product @@ -141,7 +149,26 @@ def _get_products(ev, mt): if lang == 1: p.distribution = [CorrelatedAngleEnergy.from_endf(file_obj)] elif lang == 2: - p.distribution = [KalbachMann.from_endf(file_obj)] + # Products need to be described in the center-of-mass system + product_center_of_mass = False + if reference_frame == 'center-of-mass': + product_center_of_mass = True + elif reference_frame == 'light-heavy': + product_center_of_mass = (awr <= 4.0) + # TODO: 'breakup' logic not implemented + + if product_center_of_mass is False: + raise IOError( + "Kalbach-Mann representation must be defined in the " + "'center-of-mass' system" + ) + + zat = ev.target["atomic_number"] * 1000 + ev.target["mass_number"] + projectile_mass = ev.projectile["mass"] + p.distribution = [KalbachMann.from_endf(file_obj, + za, + zat, + projectile_mass)] elif law == 2: # Discrete two-body scattering diff --git a/openmc/data/reconstruct.pyx b/openmc/data/reconstruct.pyx index f63a155b1..cd0bbc38b 100644 --- a/openmc/data/reconstruct.pyx +++ b/openmc/data/reconstruct.pyx @@ -299,8 +299,8 @@ def reconstruct_slbw(slbw, double E): # Determine shift and penetration at modified energy if slbw._competitive[i]: Ex = E + slbw.q_value[l]*(A + 1)/A - rhoc = slbw.channel_radius[l](Ex) - rhochat = slbw.scattering_radius[l](Ex) + rhoc = k*slbw.channel_radius[l](Ex) + rhochat = k*slbw.scattering_radius[l](Ex) P_c, S_c = penetration_shift(l, rhoc) if Ex < 0: P_c = 0 diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 4e0a30b64..3d5f2df64 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -19,12 +19,12 @@ from . import HDF5_VERSION, HDF5_VERSION_MAJOR, endf from .data import K_BOLTZMANN, ATOMIC_SYMBOL, EV_PER_MEV, isotopes from .ace import Table, get_table, Library from .angle_energy import AngleEnergy -from .function import Tabulated1D, Function1D +from .function import Tabulated1D, Function1D, Sum from .njoy import make_ace_thermal from .thermal_angle_energy import (CoherentElasticAE, IncoherentElasticAE, IncoherentElasticAEDiscrete, IncoherentInelasticAEDiscrete, - IncoherentInelasticAE) + IncoherentInelasticAE, MixedElasticAE) _THERMAL_NAMES = { @@ -694,29 +694,53 @@ class ThermalScattering(EqualityMixin): # Incoherent/coherent elastic scattering cross section idx = ace.jxs[4] - n_mu = ace.nxs[6] + 1 if idx != 0: - n_energy = int(ace.xss[idx]) - energy = ace.xss[idx + 1: idx + 1 + n_energy]*EV_PER_MEV - P = ace.xss[idx + 1 + n_energy: idx + 1 + 2 * n_energy] - - if ace.nxs[5] == 4: + if ace.nxs[5] in (4, 5): # Coherent elastic - xs = CoherentElastic(energy, P*EV_PER_MEV) - distribution = CoherentElasticAE(xs) + n_energy = int(ace.xss[idx]) + energy = ace.xss[idx + 1: idx + 1 + n_energy]*EV_PER_MEV + P = ace.xss[idx + 1 + n_energy: idx + 1 + 2 * n_energy] + coherent_xs = CoherentElastic(energy, P*EV_PER_MEV) + coherent_dist = CoherentElasticAE(coherent_xs) # Coherent elastic shouldn't have angular distributions listed + n_mu = ace.nxs[6] + 1 assert n_mu == 0 - else: - # Incoherent elastic - xs = Tabulated1D(energy, P) + + if ace.nxs[5] in (3, 5): + # Incoherent elastic scattering -- first determine if both + # incoherent and coherent are present (mixed) + mixed = (ace.nxs[5] == 5) + + # Get cross section values + idx = ace.jxs[7] if mixed else ace.jxs[4] + n_energy = int(ace.xss[idx]) + energy = ace.xss[idx + 1: idx + 1 + n_energy]*EV_PER_MEV + values = ace.xss[idx + 1 + n_energy: idx + 1 + 2 * n_energy] + + incoherent_xs = Tabulated1D(energy, values) # Angular distribution + n_mu = (ace.nxs[8] if mixed else ace.nxs[6]) + 1 assert n_mu > 0 - idx = ace.jxs[6] + idx = ace.jxs[9] if mixed else ace.jxs[6] mu_out = ace.xss[idx:idx + n_energy * n_mu] mu_out.shape = (n_energy, n_mu) - distribution = IncoherentElasticAEDiscrete(mu_out) + incoherent_dist = IncoherentElasticAEDiscrete(mu_out) + + if ace.nxs[5] == 3: + xs = incoherent_xs + dist = incoherent_dist + elif ace.nxs[5] == 4: + xs = coherent_xs + dist = coherent_dist + else: + # Create mixed cross section -- note that coherent must come + # first due to assumption on C++ side + xs = Sum([coherent_xs, incoherent_xs]) + + # Create mixed distribution + distribution = MixedElasticAE(coherent_dist, incoherent_dist) table.elastic = ThermalScatteringReaction({T: xs}, {T: distribution}) @@ -802,7 +826,7 @@ class ThermalScattering(EqualityMixin): # Replace ACE data with ENDF data rx, rx_endf = data.elastic, data_endf.elastic for t in temperatures: - if isinstance(rx_endf.xs[t], IncoherentElastic): + if isinstance(rx_endf.xs[t], (IncoherentElastic, Sum)): rx.xs[t] = rx_endf.xs[t] rx.distribution[t] = rx_endf.distribution[t] @@ -832,20 +856,14 @@ class ThermalScattering(EqualityMixin): # Read coherent/incoherent elastic data elastic = None if (7, 2) in ev.section: - xs = {} - distribution = {} - - file_obj = StringIO(ev.section[7, 2]) - lhtr = endf.get_head_record(file_obj)[2] - if lhtr == 1: - # coherent elastic - + # Define helper functions to avoid duplication + def get_coherent_elastic(file_obj): # Get structure factor at first temperature params, S = endf.get_tab1_record(file_obj) strT = _temperature_str(params[0]) n_temps = params[2] bragg_edges = S.x - xs[strT] = CoherentElastic(bragg_edges, S.y) + xs = {strT: CoherentElastic(bragg_edges, S.y)} distribution = {strT: CoherentElasticAE(xs[strT])} # Get structure factor for subsequent temperatures @@ -854,15 +872,34 @@ class ThermalScattering(EqualityMixin): strT = _temperature_str(params[0]) xs[strT] = CoherentElastic(bragg_edges, S) distribution[strT] = CoherentElasticAE(xs[strT]) + return xs, distribution - elif lhtr == 2: - # incoherent elastic + def get_incoherent_elastic(file_obj): params, W = endf.get_tab1_record(file_obj) bound_xs = params[0] + xs = {} + distribution = {} for T, debye_waller in zip(W.x, W.y): strT = _temperature_str(T) xs[strT] = IncoherentElastic(bound_xs, debye_waller) distribution[strT] = IncoherentElasticAE(debye_waller) + return xs, distribution + + file_obj = StringIO(ev.section[7, 2]) + lhtr = endf.get_head_record(file_obj)[2] + if lhtr == 1: + # coherent elastic + xs, distribution = get_coherent_elastic(file_obj) + elif lhtr == 2: + # incoherent elastic + xs, distribution = get_incoherent_elastic(file_obj) + elif lhtr == 3: + # mixed coherent / incoherent elastic + xs_c, dist_c = get_coherent_elastic(file_obj) + xs_i, dist_i = get_incoherent_elastic(file_obj) + assert sorted(xs_c) == sorted(xs_i) + xs = {T: Sum([xs_c[T], xs_i[T]]) for T in xs_c} + distribution = {T: MixedElasticAE(dist_c[T], dist_i[T]) for T in dist_c} elastic = ThermalScatteringReaction(xs, distribution) diff --git a/openmc/data/thermal_angle_energy.py b/openmc/data/thermal_angle_energy.py index 05393e7bb..17a560092 100644 --- a/openmc/data/thermal_angle_energy.py +++ b/openmc/data/thermal_angle_energy.py @@ -2,6 +2,7 @@ import numpy as np from .angle_energy import AngleEnergy from .correlated import CorrelatedAngleEnergy +import openmc.data class CoherentElasticAE(AngleEnergy): @@ -43,7 +44,27 @@ class CoherentElasticAE(AngleEnergy): """ group.attrs['type'] = np.string_('coherent_elastic') - group['coherent_xs'] = group.parent['xs'] + self.coherent_xs.to_hdf5(group, 'coherent_xs') + + @classmethod + def from_hdf5(cls, group): + """Generate coherent elastic distribution from HDF5 data + + .. versionadded:: 0.13.1 + + Parameters + ---------- + group : h5py.Group + HDF5 group to read from + + Returns + ------- + openmc.data.CoherentElasticAE + Coherent elastic distribution + + """ + coherent_xs = openmc.data.CoherentElastic.from_hdf5(group['coherent_xs']) + return cls(coherent_xs) class IncoherentElasticAE(AngleEnergy): @@ -101,7 +122,7 @@ class IncoherentElasticAE(AngleEnergy): Incoherent elastic distribution """ - return cls(group['debye_waller']) + return cls(group['debye_waller'][()]) class IncoherentElasticAEDiscrete(AngleEnergy): @@ -210,3 +231,62 @@ class IncoherentInelasticAEDiscrete(AngleEnergy): class IncoherentInelasticAE(CorrelatedAngleEnergy): _name = 'incoherent_inelastic' + + +class MixedElasticAE(AngleEnergy): + """Secondary distribution for mixed coherent/incoherent thermal elastic + + .. versionadded:: 0.13.1 + + Parameters + ---------- + coherent : AngleEnergy + Secondary distribution for coherent elastic scattering + incoherent : AngleEnergy + Secondary distribution for incoherent elastic scattering + + Attributes + ---------- + coherent : AngleEnergy + Secondary distribution for coherent elastic scattering + incoherent : AngleEnergy + Secondary distribution for incoherent elastic scattering + + """ + def __init__(self, coherent, incoherent): + self.coherent = coherent + self.incoherent = incoherent + + def to_hdf5(self, group): + """Write mixed elastic distribution to an HDF5 group + + Parameters + ---------- + group : h5py.Group + HDF5 group to write to + + """ + group.attrs['type'] = np.string_('mixed_elastic') + coherent_group = group.create_group('coherent') + self.coherent.to_hdf5(coherent_group) + incoherent_group = group.create_group('incoherent') + self.incoherent.to_hdf5(incoherent_group) + + @classmethod + def from_hdf5(cls, group): + """Generate mixed thermal elastic distribution from HDF5 data + + Parameters + ---------- + group : h5py.Group + HDF5 group to read from + + Returns + ------- + openmc.data.MixedElasticAE + Mixed thermal elastic distribution + + """ + coherent = AngleEnergy.from_hdf5(group['coherent']) + incoherent = AngleEnergy.from_hdf5(group['incoherent']) + return cls(coherent, incoherent) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 5891555a2..329ee8b52 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -7,7 +7,10 @@ A depletion front-end tool. from .nuclide import * from .chain import * -from .operator import * +from .openmc_operator import * +from .coupled_operator import * +from .independent_operator import * +from .microxs import * from .reaction_rates import * from .atom_number import * from .stepresult import * diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8bbe126df..3e2c37ece 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -1,7 +1,6 @@ -"""function module. +"""abc module. -This module contains the Operator class, which is then passed to an integrator -to run a full depletion simulation. +This module contains Abstract Base Classes for implementing operator, integrator, depletion system solver, and operator helper classes """ from abc import ABC, abstractmethod @@ -29,8 +28,8 @@ from .pool import deplete __all__ = [ - "OperatorResult", "TransportOperator", "ReactionRateHelper", - "NormalizationHelper", "FissionYieldHelper", + "OperatorResult", "TransportOperator", + "ReactionRateHelper", "NormalizationHelper", "FissionYieldHelper", "Integrator", "SIIntegrator", "DepSystemSolver", "add_params"] @@ -83,7 +82,8 @@ class TransportOperator(ABC): operator that takes a vector of material compositions and returns an eigenvalue and reaction rates. This abstract class sets the requirements for such a transport operator. Users should instantiate - :class:`openmc.deplete.Operator` rather than this class. + :class:`openmc.deplete.CoupledOperator` or + :class:`openmc.deplete.IndependentOperator` rather than this class. Parameters ---------- @@ -221,9 +221,11 @@ class ReactionRateHelper(ABC): Parameters ---------- n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + Number of burnable nuclides tracked by + :class:`openmc.deplete.abc.TransportOperator` n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` + Number of reactions tracked by + :class:`openmc.deplete.abc.TransportOperator` Attributes ---------- @@ -292,9 +294,9 @@ class NormalizationHelper(ABC): """Abstract class for obtaining normalization factor on tallies This helper class determines how reaction rates calculated by an instance of - :class:`openmc.deplete.Operator` should be normalized for the purpose of - constructing a burnup matrix. Based on the method chosen, the power or - source rate provided by the user, and reaction rates from a + :class:`openmc.deplete.abc.TransportOperator` should be normalized for the + purpose of constructing a burnup matrix. Based on the method chosen, the + power or source rate provided by the user, and reaction rates from a :class:`ReactionRateHelper`, this class will scale reaction rates to the correct values. @@ -302,7 +304,7 @@ class NormalizationHelper(ABC): ---------- nuclides : list of str All nuclides with desired reaction rates. Ordered to be - consistent with :class:`openmc.deplete.Operator` + consistent with :class:`openmc.deplete.abc.TransportOperator` """ @@ -316,9 +318,9 @@ class NormalizationHelper(ABC): def prepare(self, chain_nucs, rate_index): """Perform work needed to obtain energy produced - This method is called prior to the transport simulations - in :meth:`openmc.deplete.Operator.initial_condition`. Only used for - energy-based normalization. + This method is called prior to calculating the reaction rates + in :meth:`openmc.deplete.abc.TransportOperator.initial_condition`. Only + used for energy-based normalization. Parameters ---------- @@ -429,7 +431,7 @@ class FissionYieldHelper(ABC): def unpack(): """Unpack tally data prior to compute fission yields. - Called after a :meth:`openmc.deplete.Operator.__call__` + Called after a :meth:`openmc.deplete.abc.TransportOperator.__call__` routine during the normalization of reaction rates. Not necessary for all subclasses to implement, unless tallies @@ -450,7 +452,7 @@ class FissionYieldHelper(ABC): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -462,14 +464,15 @@ class FissionYieldHelper(ABC): ---------- nuclides : iterable of str Nuclides with non-zero densities from the - :class:`openmc.deplete.Operator` + :class:`openmc.deplete.abc.TransportOperator` Returns ------- nuclides : list of str - Union of nuclides that the :class:`openmc.deplete.Operator` - says have non-zero densities at this stage and those that - have yield data. Sorted by nuclide name + Union of nuclides that the + :class:`openmc.deplete.abc.TransportOperator` says have non-zero + densities at this stage and those that have yield data. Sorted by + nuclide name """ return sorted(self._chain_set & set(nuclides)) @@ -483,7 +486,7 @@ class FissionYieldHelper(ABC): Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator with a depletion chain kwargs: optional Additional keyword arguments to be used in constuction @@ -504,7 +507,7 @@ class Integrator(ABC): _params = r""" Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator to perform transport simulations timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are @@ -522,9 +525,10 @@ class Integrator(ABC): power_density : float or iterable of float, optional Power density of the reactor in [W/gHM]. It is multiplied by initial heavy metal inventory to get total power if ``power`` - is not speficied. + is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] for each interval in :attr:`timesteps` + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for + each interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} @@ -546,7 +550,7 @@ class Integrator(ABC): Attributes ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator to perform transport simulations chain : openmc.deplete.Chain Depletion chain @@ -841,8 +845,8 @@ class SIIntegrator(Integrator): _params = r""" Parameters ---------- - operator : openmc.deplete.TransportOperator - The operator object to simulate on. + operator : openmc.deplete.abc.TransportOperator + Operator to perform transport simulations timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are specified by the `timestep_units` argument when `timesteps` is an @@ -859,9 +863,10 @@ class SIIntegrator(Integrator): power_density : float or iterable of float, optional Power density of the reactor in [W/gHM]. It is multiplied by initial heavy metal inventory to get total power if ``power`` - is not speficied. + is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] for each interval in :attr:`timesteps` + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for + each interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} @@ -886,7 +891,7 @@ class SIIntegrator(Integrator): Attributes ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator to perform transport simulations chain : openmc.deplete.Chain Depletion chain @@ -1006,7 +1011,7 @@ class DepSystemSolver(ABC): Parameters ---------- A : scipy.sparse.csr_matrix - Sparse transmutation matrix ``A[j, i]`` desribing rates at + Sparse transmutation matrix ``A[j, i]`` describing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray Initial compositions, typically given in number of atoms in some diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 5430ddc35..78ceecca6 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -126,6 +126,23 @@ class AtomNumber: return [nuc for nuc, ind in self.index_nuc.items() if ind < self.n_nuc_burn] + def get_mat_volume(self, mat): + """Return material volume + + Parameters + ---------- + mat : str, int, openmc.Material, or slice + Material index. + + Returns + ------- + float + Material volume in [cm^3] + + """ + mat = self._get_mat_index(mat) + return self.volume[mat] + def get_atom_density(self, mat, nuc): """Return atom density of given material and nuclide diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 0c1c34cf7..d63bc6bfb 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -264,7 +264,8 @@ class Chain: requires a list of ENDF incident neutron, decay, and neutron fission product yield sublibrary files. The depletion chain used during a depletion simulation is indicated by either an argument to - :class:`openmc.deplete.Operator` or through the + :class:`openmc.deplete.CoupledOperator` or + :class:`openmc.deplete.IndependentOperator`, or through the ``depletion_chain`` item in the :envvar:`OPENMC_CROSS_SECTIONS` environment variable. diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py new file mode 100644 index 000000000..eba90f60d --- /dev/null +++ b/openmc/deplete/coupled_operator.py @@ -0,0 +1,546 @@ +"""Transport-coupled transport operator for depletion. + +This module implements a transport operator coupled to OpenMC's transport solver +so that it can be used by depletion integrators. The implementation makes use of +the Python bindings to OpenMC's C API so that reading tally results and updating +material number densities is all done in-memory instead of through the +filesystem. + +""" + +import copy +import os +from warnings import warn + +import numpy as np +from uncertainties import ufloat + +import openmc +from openmc.checkvalue import check_value +from openmc.data import DataLibrary +from openmc.exceptions import DataError +import openmc.lib +from openmc.mpi import comm +from .abc import OperatorResult +from .chain import _find_chain_file +from .openmc_operator import OpenMCOperator, _distribute +from .results import Results +from .helpers import ( + DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper, + FissionYieldCutoffHelper, AveragedFissionYieldHelper, EnergyScoreHelper, + SourceRateHelper, FluxCollapseHelper) + + +__all__ = ["CoupledOperator", "Operator", "OperatorResult"] + + +def _find_cross_sections(model): + """Determine cross sections to use for depletion + + Parameters + ---------- + model : openmc.model.Model + Reactor model + + """ + if model.materials and model.materials.cross_sections is not None: + # Prefer info from Model class if available + return model.materials.cross_sections + + # otherwise fallback to environment variable + cross_sections = os.environ.get("OPENMC_CROSS_SECTIONS") + if cross_sections is None: + raise DataError( + "Cross sections were not specified in Model.materials and " + "the OPENMC_CROSS_SECTIONS environment variable is not set." + ) + return cross_sections + +def _get_nuclides_with_data(cross_sections): + """Loads cross_sections.xml file to find nuclides with neutron data + + Parameters + ---------- + cross_sections : str + Path to cross_sections.xml file + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ + nuclides = set() + data_lib = DataLibrary.from_xml(cross_sections) + for library in data_lib.libraries: + if library['type'] != 'neutron': + continue + for name in library['materials']: + if name not in nuclides: + nuclides.add(name) + + return nuclides + +class CoupledOperator(OpenMCOperator): + """Transport-coupled transport operator. + + Instances of this class can be used to perform transport-coupled depletion + using OpenMC's transport solver. Normally, a user needn't call methods of + this class directly. Instead, an instance of this class is passed to an + integrator class, such as :class:`openmc.deplete.CECMIntegrator`. + + .. versionchanged:: 0.13.0 + The geometry and settings parameters have been replaced with a + model parameter that takes a :class:`~openmc.model.Model` object + + Parameters + ---------- + model : openmc.model.Model + OpenMC model object + chain_file : str, optional + Path to the depletion chain XML file. Defaults to the file + listed under ``depletion_chain`` in + :envvar:`OPENMC_CROSS_SECTIONS` environment variable. + prev_results : Results, optional + Results from a previous depletion calculation. If this argument is + specified, the depletion calculation will start from the latest state + in the previous results. + diff_burnable_mats : bool, optional + Whether to differentiate burnable materials with multiple instances. + Volumes are divided equally from the original material volume. + normalization_mode : {"energy-deposition", "fission-q", "source-rate"} + Indicate how tally results should be normalized. ``"energy-deposition"`` + computes the total energy deposited in the system and uses the ratio of + the power to the energy produced as a normalization factor. + ``"fission-q"`` uses the fission Q values from the depletion chain to + compute the total energy deposited. ``"source-rate"`` normalizes + tallies based on the source rate (for fixed source calculations). + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. Only applicable + if ``"normalization_mode" == "fission-q"`` + dilute_initial : float, optional + Initial atom density [atoms/cm^3] to add for nuclides that are zero + in initial condition to ensure they exist in the decay chain. + Only done for nuclides with reaction rates. + fission_yield_mode : {"constant", "cutoff", "average"} + Key indicating what fission product yield scheme to use. The + key determines what fission energy helper is used: + + * "constant": :class:`~openmc.deplete.helpers.ConstantFissionYieldHelper` + * "cutoff": :class:`~openmc.deplete.helpers.FissionYieldCutoffHelper` + * "average": :class:`~openmc.deplete.helpers.AveragedFissionYieldHelper` + + The documentation on these classes describe their methodology + and differences. Default: ``"constant"`` + fission_yield_opts : dict of str to option, optional + Optional arguments to pass to the helper determined by + ``fission_yield_mode``. Will be passed directly on to the + helper. Passing a value of None will use the defaults for + the associated helper. + reaction_rate_mode : {"direct", "flux"}, optional + Indicate how one-group reaction rates should be calculated. The "direct" + method tallies transmutation reaction rates directly. The "flux" method + tallies a multigroup flux spectrum and then collapses one-group reaction + rates after a transport solve (with an option to tally some reaction + rates directly). + + .. versionadded:: 0.12.1 + reaction_rate_opts : dict, optional + Keyword arguments that are passed to the reaction rate helper class. + When ``reaction_rate_mode`` is set to "flux", energy group boundaries + can be set using the "energies" key. See the + :class:`~openmc.deplete.helpers.FluxCollapseHelper` class for all + options. + + .. versionadded:: 0.12.1 + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the + depletion chain up to ``reduce_chain_level``. + + .. versionadded:: 0.12 + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + + .. versionadded:: 0.12 + + Attributes + ---------- + model : openmc.model.Model + OpenMC model object + geometry : openmc.Geometry + OpenMC geometry object + settings : openmc.Settings + OpenMC settings object + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the decay + chain. Only done for nuclides with reaction rates. + output_dir : pathlib.Path + Path to output directory to save results. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. + number : openmc.deplete.AtomNumber + Total number of atoms in simulation. + nuclides_with_data : set of str + A set listing all unique nuclides available from cross_sections.xml. + chain : openmc.deplete.Chain + The depletion chain information necessary to form matrices and tallies. + reaction_rates : openmc.deplete.ReactionRates + Reaction rates from the last operator step. + burnable_mats : list of str + All burnable material IDs + heavy_metal : float + Initial heavy metal inventory [g] + local_mats : list of str + All burnable material IDs being managed by a single process + prev_res : Results or None + Results from a previous depletion calculation. ``None`` if no + results are to be used. + cleanup_when_done : bool + Whether to finalize and clear the shared library memory when the + depletion operation is complete. Defaults to clearing the library. + """ + _fission_helpers = { + "average": AveragedFissionYieldHelper, + "constant": ConstantFissionYieldHelper, + "cutoff": FissionYieldCutoffHelper, + } + + def __init__(self, model, chain_file=None, prev_results=None, + diff_burnable_mats=False, normalization_mode="fission-q", + fission_q=None, dilute_initial=1.0e3, + fission_yield_mode="constant", fission_yield_opts=None, + reaction_rate_mode="direct", reaction_rate_opts=None, + reduce_chain=False, reduce_chain_level=None): + + # check for old call to constructor + if isinstance(model, openmc.Geometry): + msg = "As of version 0.13.0 openmc.deplete.CoupledOperator " \ + "requires an openmc.Model object rather than the " \ + "openmc.Geometry and openmc.Settings parameters. Please use " \ + "the geometry and settings objects passed here to create a " \ + " model with which to generate the transport Operator." + raise TypeError(msg) + + # Determine cross sections / depletion chain + cross_sections = _find_cross_sections(model) + if chain_file is None: + chain_file = _find_chain_file(cross_sections) + + check_value('fission yield mode', fission_yield_mode, + self._fission_helpers.keys()) + check_value('normalization mode', normalization_mode, + ('energy-deposition', 'fission-q', 'source-rate')) + if normalization_mode != "fission-q": + if fission_q is not None: + warn("Fission Q dictionary will not be used") + fission_q = None + self.model = model + self.settings = model.settings + self.geometry = model.geometry + + # determine set of materials in the model + if not model.materials: + model.materials = openmc.Materials( + model.geometry.get_all_materials().values() + ) + + self.cleanup_when_done = True + + if reaction_rate_opts is None: + reaction_rate_opts = {} + if fission_yield_opts is None: + fission_yield_opts = {} + helper_kwargs = { + 'reaction_rate_mode': reaction_rate_mode, + 'normalization_mode': normalization_mode, + 'fission_yield_mode': fission_yield_mode, + 'reaction_rate_opts': reaction_rate_opts, + 'fission_yield_opts': fission_yield_opts + } + + super().__init__( + model.materials, + cross_sections, + chain_file, + prev_results, + diff_burnable_mats, + fission_q, + dilute_initial, + helper_kwargs, + reduce_chain, + reduce_chain_level) + + def _differentiate_burnable_mats(self): + """Assign distribmats for each burnable material""" + + # Count the number of instances for each cell and material + self.geometry.determine_paths(instances_only=True) + + # Extract all burnable materials which have multiple instances + distribmats = set( + [mat for mat in self.materials + if mat.depletable and mat.num_instances > 1]) + + for mat in distribmats: + if mat.volume is None: + raise RuntimeError("Volume not specified for depletable " + "material with ID={}.".format(mat.id)) + mat.volume /= mat.num_instances + + if distribmats: + # Assign distribmats to cells + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill in distribmats: + mat = cell.fill + cell.fill = [mat.clone() + for i in range(cell.num_instances)] + + self.materials = openmc.Materials( + self.model.geometry.get_all_materials().values() + ) + + def _load_previous_results(self): + """Load results from a previous depletion simulation""" + # Reload volumes into geometry + self.prev_res[-1].transfer_volumes(self.model) + + # Store previous results in operator + # Distribute reaction rates according to those tracked + # on this process + if comm.size != 1: + prev_results = self.prev_res + self.prev_res = Results() + mat_indexes = _distribute(range(len(self.burnable_mats))) + for res_obj in prev_results: + new_res = res_obj.distribute(self.local_mats, mat_indexes) + self.prev_res.append(new_res) + + def _get_nuclides_with_data(self, cross_sections): + """Loads cross_sections.xml file to find nuclides with neutron data + + Parameters + ---------- + cross_sections : str + Path to cross_sections.xml file + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ + return _get_nuclides_with_data(cross_sections) + + def _get_helper_classes(self, helper_kwargs): + """Create the ``_rate_helper``, ``_normalization_helper``, and + ``_yield_helper`` objects. + + Parameters + ---------- + helper_kwargs : dict + Keyword arguments for helper classes + + """ + reaction_rate_mode = helper_kwargs['reaction_rate_mode'] + normalization_mode = helper_kwargs['normalization_mode'] + fission_yield_mode = helper_kwargs['fission_yield_mode'] + reaction_rate_opts = helper_kwargs['reaction_rate_opts'] + fission_yield_opts = helper_kwargs['fission_yield_opts'] + + # Get classes to assist working with tallies + if reaction_rate_mode == "direct": + self._rate_helper = DirectReactionRateHelper( + self.reaction_rates.n_nuc, self.reaction_rates.n_react) + elif reaction_rate_mode == "flux": + # Ensure energy group boundaries were specified + if 'energies' not in reaction_rate_opts: + raise ValueError( + "Energy group boundaries must be specified in the " + "reaction_rate_opts argument when reaction_rate_mode is" + "set to 'flux'.") + + self._rate_helper = FluxCollapseHelper( + self.reaction_rates.n_nuc, + self.reaction_rates.n_react, + **reaction_rate_opts + ) + else: + raise ValueError("Invalid reaction rate mode.") + + if normalization_mode == "fission-q": + self._normalization_helper = ChainFissionHelper() + elif normalization_mode == "energy-deposition": + score = "heating" if self.settings.photon_transport else "heating-local" + self._normalization_helper = EnergyScoreHelper(score) + else: + self._normalization_helper = SourceRateHelper() + + # Select and create fission yield helper + fission_helper = self._fission_helpers[fission_yield_mode] + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) + + def initial_condition(self): + """Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.ndarray + Total density for initial conditions. + + """ + + # Create XML files + if comm.rank == 0: + self.geometry.export_to_xml() + self.settings.export_to_xml() + self._generate_materials_xml() + + # Initialize OpenMC library + comm.barrier() + if not openmc.lib.is_initialized: + openmc.lib.init(intracomm=comm) + + # Generate tallies in memory + materials = [openmc.lib.materials[int(i)] for i in self.burnable_mats] + + return super().initial_condition(materials) + + def _generate_materials_xml(self): + """Creates materials.xml from self.number. + + Due to uncertainty with how MPI interacts with OpenMC API, this + constructs the XML manually. The long term goal is to do this + through direct memory writing. + + """ + # Sort nuclides according to order in AtomNumber object + nuclides = list(self.number.nuclides) + for mat in self.materials: + mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) + + self.materials.export_to_xml() + + def __call__(self, vec, source_rate): + """Runs a simulation. + + Simulation will abort under the following circumstances: + + 1) No energy is computed using OpenMC tallies. + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms to be used in function. + source_rate : float + Power in [W] or source rate in [neutron/sec] + + Returns + ------- + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + + """ + # Reset results in OpenMC + openmc.lib.reset() + + self._update_materials_and_nuclides(vec) + + # If the source rate is zero, return zero reaction rates without running + # a transport solve + if source_rate == 0.0: + rates = self.reaction_rates.copy() + rates.fill(0.0) + return OperatorResult(ufloat(0.0, 0.0), rates) + + # Run OpenMC + openmc.lib.run() + openmc.lib.reset_timers() + + # Extract results + rates = self._calculate_reaction_rates(source_rate) + + # Get k and uncertainty + keff = ufloat(*openmc.lib.keff()) + + op_result = OperatorResult(keff, rates) + + return copy.deepcopy(op_result) + + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" + + for rank in range(comm.size): + number_i = comm.bcast(self.number, root=rank) + + for mat in number_i.materials: + nuclides = [] + densities = [] + for nuc in number_i.nuclides: + if nuc in self.nuclides_with_data: + val = 1.0e-24 * number_i.get_atom_density(mat, nuc) + + # If nuclide is zero, do not add to the problem. + if val > 0.0: + if self.round_number: + val_magnitude = np.floor(np.log10(val)) + val_scaled = val / 10**val_magnitude + val_round = round(val_scaled, 8) + + val = val_round * 10**val_magnitude + + nuclides.append(nuc) + densities.append(val) + else: + # Only output warnings if values are significantly + # negative. CRAM does not guarantee positive + # values. + if val < -1.0e-21: + print(f'WARNING: nuclide {nuc} in material' + f'{mat} is negative (density = {val}' + + ' atom/b-cm)') + + number_i[mat, nuc] = 0.0 + + # Update densities on C API side + mat_internal = openmc.lib.materials[int(mat)] + mat_internal.set_densities(nuclides, densities) + + # TODO Update densities on the Python side, otherwise the + # summary.h5 file contains densities at the first time step + + @staticmethod + def write_bos_data(step): + """Write a state-point file with beginning of step data + + Parameters + ---------- + step : int + Current depletion step including restarts + + """ + openmc.lib.statepoint_write( + "openmc_simulation_n{}.h5".format(step), + write_source=False) + + def finalize(self): + """Finalize a depletion simulation and release resources.""" + if self.cleanup_when_done: + openmc.lib.finalize() + +# Retain deprecated name for the time being +def Operator(*args, **kwargs): + # warn of name change + warn( + "The Operator(...) class has been renamed and will " + "be removed in a future version of OpenMC. Use " + "CoupledOperator(...) instead.", + FutureWarning + ) + return CoupledOperator(*args, **kwargs) diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 33bd182d7..3a7928a52 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -1,5 +1,5 @@ """ -Class for normalizing fission energy deposition +Classes for collecting and calculating quantities for reaction rate operators """ import bisect from abc import abstractmethod @@ -68,7 +68,7 @@ class TalliedFissionYieldHelper(FissionYieldHelper): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -133,9 +133,11 @@ class DirectReactionRateHelper(ReactionRateHelper): Parameters ---------- n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + Number of burnable nuclides tracked by + :class:`openmc.deplete.CoupledOperator` n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` + Number of reactions tracked by an instance of + :class:`openmc.deplete.CoupledOperator` Attributes ---------- @@ -217,9 +219,10 @@ class FluxCollapseHelper(ReactionRateHelper): Parameters ---------- n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + Number of burnable nuclides tracked by + :class:`openmc.deplete.CoupledOperator` n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` + Number of reactions tracked by :class:`openmc.deplete.CoupledOperator` energies : iterable of float Energy group boundaries for flux spectrum in [eV] reactions : iterable of str @@ -385,7 +388,7 @@ class ChainFissionHelper(EnergyNormalizationHelper): ---------- nuclides : list of str All nuclides with desired reaction rates. Ordered to be - consistent with :class:`openmc.deplete.Operator` + consistent with :class:`openmc.deplete.CoupledOperator` energy : float Total energy [J/s/source neutron] produced in a transport simulation. Updated in the material iteration with :meth:`update`. @@ -552,7 +555,7 @@ class ConstantFissionYieldHelper(FissionYieldHelper): Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator operator with a depletion chain kwargs: Additional keyword arguments to be used in construction @@ -610,7 +613,6 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Default: 0.0253 [eV] fast_energy : float, optional Energy of yield data corresponding to fast yields. - Default: 500 [kev] Attributes ---------- @@ -632,10 +634,10 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Array of fission rate fractions with shape ``(n_mats, 2, n_nucs)``. ``results[:, 0]`` corresponds to the fraction of all fissions - that occured below ``cutoff``. The number + that occurred below ``cutoff``. The number of materials in the first axis corresponds to the number of materials burned by the - :class:`openmc.deplete.Operator` + :class:`openmc.deplete.CoupledOperator` """ def __init__(self, chain_nuclides, n_bmats, cutoff=112.0, @@ -692,7 +694,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Parameters ---------- - operator : openmc.deplete.Operator + operator : openmc.deplete.CoupledOperator Operator with a chain and burnable materials kwargs: Additional keyword arguments to be used in construction @@ -718,7 +720,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -788,7 +790,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): class AveragedFissionYieldHelper(TalliedFissionYieldHelper): r"""Class that computes fission yields based on average fission energy - Computes average energy at which fission events occured with + Computes average energy at which fission events occurred with .. math:: @@ -843,7 +845,7 @@ class AveragedFissionYieldHelper(TalliedFissionYieldHelper): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -906,7 +908,7 @@ class AveragedFissionYieldHelper(TalliedFissionYieldHelper): Use the computed average energy of fission events to determine fission yields. If average energy is between two sets of yields, linearly - interpolate bewteen the two. + interpolate between the two. Otherwise take the closet set of yields. Parameters @@ -956,7 +958,7 @@ class AveragedFissionYieldHelper(TalliedFissionYieldHelper): Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.CoupledOperator Operator with a depletion chain kwargs : Additional keyword arguments to be used in construction diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py new file mode 100644 index 000000000..a2d89e441 --- /dev/null +++ b/openmc/deplete/independent_operator.py @@ -0,0 +1,425 @@ +"""Transport-independent transport operator for depletion. + +This module implements a transport operator that runs independently of any +transport solver by using user-provided one-group cross sections. + +""" + +import copy +from collections import OrderedDict +from warnings import warn +from itertools import product + +import numpy as np +from uncertainties import ufloat + +import openmc +from openmc.checkvalue import check_type +from openmc.mpi import comm +from .abc import ReactionRateHelper, OperatorResult +from .openmc_operator import OpenMCOperator, _distribute +from .microxs import MicroXS +from .results import Results +from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper + +class IndependentOperator(OpenMCOperator): + """Transport-independent transport operator that uses one-group cross + sections to calculate reaction rates. + + Instances of this class can be used to perform depletion using one-group + cross sections and constant flux or constant power. Normally, a user needn't + call methods of this class directly. Instead, an instance of this class is + passed to an integrator class, such as + :class:`openmc.deplete.CECMIntegrator`. + + Parameters + ---------- + materials : openmc.Materials + Materials to deplete. + micro_xs : MicroXS + One-group microscopic cross sections in [b] . + chain_file : str + Path to the depletion chain XML file. + keff : 2-tuple of float, optional + keff eigenvalue and uncertainty from transport calculation. + Default is None. + prev_results : Results, optional + Results from a previous depletion calculation. + normalization_mode : {"fission-q", "source-rate"} + Indicate how reaction rates should be calculated. + ``"fission-q"`` uses the fission Q values from the depletion chain to + compute the flux based on the power. ``"source-rate"`` uses a the + source rate (assumed to be neutron flux) to calculate the + reaction rates. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. Only applicable + if ``"normalization_mode" == "fission-q"``. + dilute_initial : float, optional + Initial atom density [atoms/cm^3] to add for nuclides that are zero + in initial condition to ensure they exist in the decay chain. + Only done for nuclides with reaction rates. + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the + depletion chain up to ``reduce_chain_level``. + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + fission_yield_opts : dict of str to option, optional + Optional arguments to pass to the + :class:`openmc.deplete.helpers.FissionYieldHelper` object. Will be + passed directly on to the helper. Passing a value of None will use + the defaults for the associated helper. + + Attributes + ---------- + materials : openmc.Materials + All materials present in the model + cross_sections : MicroXS + Object containing one-group cross-sections in [cm^2]. + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the decay + chain. Only done for nuclides with reaction rates. + output_dir : pathlib.Path + Path to output directory to save results. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. + number : openmc.deplete.AtomNumber + Total number of atoms in simulation. + nuclides_with_data : set of str + A set listing all unique nuclides available from cross_sections.xml. + chain : openmc.deplete.Chain + The depletion chain information necessary to form matrices and tallies. + reaction_rates : openmc.deplete.ReactionRates + Reaction rates from the last operator step. + burnable_mats : list of str + All burnable material IDs + heavy_metal : float + Initial heavy metal inventory [g] + local_mats : list of str + All burnable material IDs being managed by a single process + prev_res : Results or None + Results from a previous depletion calculation. ``None`` if no + results are to be used. + + """ + + def __init__(self, + materials, + micro_xs, + chain_file, + keff=None, + normalization_mode='fission-q', + fission_q=None, + dilute_initial=1.0e3, + prev_results=None, + reduce_chain=False, + reduce_chain_level=None, + fission_yield_opts=None): + # Validate micro-xs parameters + check_type('materials', materials, openmc.Materials) + check_type('micro_xs', micro_xs, MicroXS) + if keff is not None: + check_type('keff', keff, tuple, float) + keff = ufloat(*keff) + + self._keff = keff + + if fission_yield_opts is None: + fission_yield_opts = {} + helper_kwargs = {'normalization_mode': normalization_mode, + 'fission_yield_opts': fission_yield_opts} + + cross_sections = micro_xs * 1e-24 + super().__init__( + materials, + cross_sections, + chain_file, + prev_results, + fission_q=fission_q, + dilute_initial=dilute_initial, + helper_kwargs=helper_kwargs, + reduce_chain=reduce_chain, + reduce_chain_level=reduce_chain_level) + + @classmethod + def from_nuclides(cls, volume, nuclides, + micro_xs, + chain_file, + nuc_units='atom/b-cm', + keff=None, + normalization_mode='fission-q', + fission_q=None, + dilute_initial=1.0e3, + prev_results=None, + reduce_chain=False, + reduce_chain_level=None, + fission_yield_opts=None): + """ + Alternate constructor from a dictionary of nuclide concentrations + + volume : float + Volume of the material being depleted in [cm^3] + nuclides : dict of str to float + Dictionary with nuclide names as keys and nuclide concentrations as + values. + micro_xs : MicroXS + One-group microscopic cross sections. + chain_file : str + Path to the depletion chain XML file. + nuc_units : {'atom/cm3', 'atom/b-cm'} + Units for nuclide concentration. + keff : 2-tuple of float, optional + keff eigenvalue and uncertainty from transport calculation. + Default is None. + normalization_mode : {"fission-q", "source-rate"} + Indicate how reaction rates should be calculated. + ``"fission-q"`` uses the fission Q values from the depletion + chain to compute the flux based on the power. ``"source-rate"`` uses + the source rate (assumed to be neutron flux) to calculate the + reaction rates. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not + given, values will be pulled from the ``chain_file``. Only + applicable if ``"normalization_mode" == "fission-q"``. + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the decay + chain. Only done for nuclides with reaction rates. + prev_results : Results, optional + Results from a previous depletion calculation. + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the + depletion chain up to ``reduce_chain_level``. Default is False. + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + fission_yield_opts : dict of str to option, optional + Optional arguments to pass to the + :class:`openmc.deplete.helpers.FissionYieldHelper` class. Will be + passed directly on to the helper. Passing a value of None will use + the defaults for the associated helper. + + """ + check_type('nuclides', nuclides, dict, str) + materials = cls._consolidate_nuclides_to_material(nuclides, nuc_units, volume) + return cls(materials, + micro_xs, + chain_file, + keff=keff, + normalization_mode=normalization_mode, + fission_q=fission_q, + dilute_initial=dilute_initial, + prev_results=prev_results, + reduce_chain=reduce_chain, + reduce_chain_level=reduce_chain_level, + fission_yield_opts=fission_yield_opts) + + + @staticmethod + def _consolidate_nuclides_to_material(nuclides, nuc_units, volume): + """Puts nuclide list into an openmc.Materials object. + + """ + openmc.reset_auto_ids() + mat = openmc.Material() + if nuc_units == 'atom/b-cm': + for nuc, conc in nuclides.items(): + mat.add_nuclide(nuc, conc) + elif nuc_units == 'atom/cm3': + for nuc, conc in nuclides.items(): + mat.add_nuclide(nuc, conc * 1e-24) # convert to at/b-cm + else: + raise ValueError(f"Unit '{nuc_units}' is invalid.") + + mat.volume = volume + mat.depletable = True + + return openmc.Materials([mat]) + + def _load_previous_results(self): + """Load results from a previous depletion simulation""" + # Reload volumes into geometry + model = openmc.Model(materials=self.materials) + self.prev_res[-1].transfer_volumes(model) + self.materials = model.materials + + + # Store previous results in operator + # Distribute reaction rates according to those tracked + # on this process + if comm.size != 1: + prev_results = self.prev_res + self.prev_res = Results() + mat_indexes = _distribute(range(len(self.burnable_mats))) + for res_obj in prev_results: + new_res = res_obj.distribute(self.local_mats, mat_indexes) + self.prev_res.append(new_res) + + + def _get_nuclides_with_data(self, cross_sections): + """Finds nuclides with cross section data""" + return set(cross_sections.index) + + class _IndependentRateHelper(ReactionRateHelper): + """Class for generating one-group reaction rates with flux and + one-group cross sections. + + This class does not generate tallies, and instead stores cross sections + for each nuclide and transmutation reaction relevant for a depletion + calculation. The reaction rate is calculated by multiplying the flux by the + cross sections. + + Parameters + ---------- + op : openmc.deplete.IndependentOperator + Reference to the object encapsulate _IndependentRateHelper. + We pass this so we don't have to duplicate the :attr:`IndependentOperator.number` object. + + + Attributes + ---------- + nuc_ind_map : dict of int to str + Dictionary mapping the nuclide index to nuclide name + rxn_ind_map : dict of int to str + Dictionary mapping reaction index to reaction name + + """ + + def __init__(self, op): + rates = op.reaction_rates + super().__init__(rates.n_nuc, rates.n_react) + + self.nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} + self.rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} + self._op = op + + def generate_tallies(self, materials, scores): + """Unused in this case""" + pass + + def get_material_rates(self, mat_id, nuc_index, react_index): + """Return 2D array of [nuclide, reaction] reaction rates + + Parameters + ---------- + mat_id : int + Unique ID for the requested material + nuc_index : list of str + Ordering of desired nuclides + react_index : list of str + Ordering of reactions + """ + self._results_cache.fill(0.0) + + volume = self._op.number.get_mat_volume(mat_id) + for i_nuc, i_react in product(nuc_index, react_index): + nuc = self.nuc_ind_map[i_nuc] + rxn = self.rxn_ind_map[i_react] + density = self._op.number.get_atom_density(mat_id, nuc) + + # Sigma^j_i * V = sigma^j_i * rho * V + self._results_cache[i_nuc,i_react] = \ + self._op.cross_sections[rxn][nuc] * density * volume + + return self._results_cache + + def _get_helper_classes(self, helper_kwargs): + """Get helper classes for calculating reaction rates and fission yields + + Parameters + ---------- + helper_kwargs : dict + Keyword arguments for helper classes + + """ + + normalization_mode = helper_kwargs['normalization_mode'] + fission_yield_opts = helper_kwargs['fission_yield_opts'] + + self._rate_helper = self._IndependentRateHelper(self) + if normalization_mode == "fission-q": + self._normalization_helper = ChainFissionHelper() + else: + self._normalization_helper = SourceRateHelper() + + # Select and create fission yield helper + fission_helper = ConstantFissionYieldHelper + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) + + def initial_condition(self): + """Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.ndarray + Total density for initial conditions. + """ + + # Return number density vector + return super().initial_condition(self.materials) + + def __call__(self, vec, source_rate): + """Obtain the reaction rates + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms to be used in function. + source_rate : float + Power in [W] or flux in [neutron/cm^2-s] + + Returns + ------- + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + + """ + + self._update_materials_and_nuclides(vec) + + rates = self._calculate_reaction_rates(source_rate) + keff = self._keff + + op_result = OperatorResult(keff, rates) + return copy.deepcopy(op_result) + + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" + + for rank in range(comm.size): + number_i = comm.bcast(self.number, root=rank) + + for mat in number_i.materials: + nuclides = [] + densities = [] + for nuc in number_i.nuclides: + if nuc in self.nuclides_with_data: + val = 1.0e-24 * number_i.get_atom_density(mat, nuc) + + # If nuclide is zero, do not add to the problem. + if val > 0.0: + if self.round_number: + val_magnitude = np.floor(np.log10(val)) + val_scaled = val / 10**val_magnitude + val_round = round(val_scaled, 8) + + val = val_round * 10**val_magnitude + + nuclides.append(nuc) + densities.append(val) + else: + # Only output warnings if values are significantly + # negative. CRAM does not guarantee positive + # values. + if val < -1.0e-21: + print(f'WARNING: nuclide {nuc} in material' + f'{mat} is negative (density = {val}' + + ' atom/b-cm)') + number_i[mat, nuc] = 0.0 diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index c19ef076a..74a3cebdb 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -103,7 +103,7 @@ class CECMIntegrator(Integrator): op_results : list of openmc.deplete.OperatorResult Eigenvalue and reaction rates from transport simulations """ - # deplete across first half of inteval + # deplete across first half of interval time0, x_middle = self._timed_deplete(conc, rates, dt / 2) res_middle = self.operator(x_middle, source_rate) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py new file mode 100644 index 000000000..ccbfab538 --- /dev/null +++ b/openmc/deplete/microxs.py @@ -0,0 +1,238 @@ +"""MicroXS module + +A pandas.DataFrame storing microscopic cross section data with +nuclide names as row indices and reaction names as column indices. +""" + +import tempfile +from pathlib import Path +from copy import deepcopy + +from pandas import DataFrame, read_csv, concat +import numpy as np + +from openmc.checkvalue import check_type, check_value, check_iterable_type +from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS +from openmc.data import DataLibrary +from openmc import Tallies, StatePoint, Materials, Material + + +from .chain import Chain, REACTIONS +from .coupled_operator import _find_cross_sections, _get_nuclides_with_data + +_valid_rxns = list(REACTIONS) +_valid_rxns.append('fission') + + +class MicroXS(DataFrame): + """Stores microscopic cross section data for use in + transport-independent depletion. + """ + + @classmethod + def from_model(cls, + model, + reaction_domain, + chain_file, + dilute_initial=1.0e3, + energy_bounds=(0, 20e6), + run_kwargs=None): + """Generate a one-group cross-section dataframe using + OpenMC. Note that the ``openmc`` executable must be compiled. + + Parameters + ---------- + model : openmc.Model + OpenMC model object. Must contain geometry, materials, and settings. + reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh + Domain in which to tally reaction rates. + chain_file : str + Path to the depletion chain XML file that will be used in depletion + simulation. Used to determine cross sections for materials not + present in the inital composition. + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the cross + section data. Only done for nuclides with reaction rates. + reactions : list of str, optional + Reaction names to tally + energy_bound : 2-tuple of float, optional + Bounds for the energy group. + run_kwargs : dict, optional + Keyword arguments for :meth:`openmc.model.Model.run()` + + Returns + ------- + MicroXS + Cross section data in [b] + + """ + groups = EnergyGroups(energy_bounds) + + # Set up the reaction tallies + original_tallies = model.tallies + original_materials = deepcopy(model.materials) + tallies = Tallies() + xs = {} + reactions, diluted_materials = cls._add_dilute_nuclides(chain_file, + model, + dilute_initial) + model.materials = diluted_materials + + for rx in reactions: + if rx == 'fission': + xs[rx] = FissionXS(domain=reaction_domain, + energy_groups=groups, by_nuclide=True) + else: + xs[rx] = ArbitraryXS(rx, domain=reaction_domain, + energy_groups=groups, by_nuclide=True) + tallies += xs[rx].tallies.values() + + model.tallies = tallies + + # create temporary run + with tempfile.TemporaryDirectory() as temp_dir: + if run_kwargs is None: + run_kwargs = {} + run_kwargs.setdefault('cwd', temp_dir) + statepoint_path = model.run(**run_kwargs) + + with StatePoint(statepoint_path) as sp: + for rx in xs: + xs[rx].load_from_statepoint(sp) + + # Build the DataFrame + series = {} + for rx in xs: + df = xs[rx].get_pandas_dataframe(xs_type='micro') + series[rx] = df.set_index('nuclide')['mean'] + + # Revert to the original tallies and materials + model.tallies = original_tallies + model.materials = original_materials + + return cls(series) + + @classmethod + def _add_dilute_nuclides(cls, chain_file, model, dilute_initial): + """ + Add nuclides not present in burnable materials that have neutron data + and are present in the depletion chain to those materials. This allows + us to tally those specific nuclides for reactions to create one-group + cross sections. + + Parameters + ---------- + chain_file : str + Path to the depletion chain XML file that will be used in depletion + simulation. Used to determine cross sections for materials not + present in the inital composition. + model : openmc.Model + Model object + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the cross + section data. Only done for nuclides with reaction rates. + + Returns + ------- + reactions : list of str + List of reaction names + diluted_materials : openmc.Materials + :class:`openmc.Materials` object with nuclides added to burnable + materials. + """ + chain = Chain.from_xml(chain_file) + reactions = chain.reactions + cross_sections = _find_cross_sections(model) + nuclides_with_data = _get_nuclides_with_data(cross_sections) + burnable_nucs = [nuc.name for nuc in chain.nuclides + if nuc.name in nuclides_with_data] + diluted_materials = Materials() + for material in model.materials: + if material.depletable: + nuc_densities = material.get_nuclide_atom_densities() + dilute_density = 1.0e-24 * dilute_initial + material.set_density('sum') + for nuc, density in nuc_densities.items(): + material.remove_nuclide(nuc) + material.add_nuclide(nuc, density) + for burn_nuc in burnable_nucs: + if burn_nuc not in nuc_densities: + material.add_nuclide(burn_nuc, + dilute_density) + diluted_materials.append(material) + + return reactions, diluted_materials + + @classmethod + def from_array(cls, nuclides, reactions, data): + """ + Creates a ``MicroXS`` object from arrays. + + Parameters + ---------- + nuclides : list of str + List of nuclide symbols for that have data for at least one + reaction. + reactions : list of str + List of reactions. All reactions must match those in + :data:`openmc.deplete.chain.REACTIONS` + data : ndarray of floats + Array containing one-group microscopic cross section values for + each nuclide and reaction. Cross section values are assumed to be + in [b]. + + Returns + ------- + MicroXS + """ + + # Validate inputs + if data.shape != (len(nuclides), len(reactions)): + raise ValueError( + f'Nuclides list of length {len(nuclides)} and ' + f'reactions array of length {len(reactions)} do not ' + f'match dimensions of data array of shape {data.shape}') + + cls._validate_micro_xs_inputs( + nuclides, reactions, data) + micro_xs = cls(index=nuclides, columns=reactions, data=data) + + return micro_xs + + @classmethod + def from_csv(cls, csv_file, **kwargs): + """ + Load a ``MicroXS`` object from a ``.csv`` file. + + Parameters + ---------- + csv_file : str + Relative path to csv-file containing microscopic cross section + data. Cross section values are assumed to be in [b] + **kwargs : dict + Keyword arguments to pass to :func:`pandas.read_csv()`. + + Returns + ------- + MicroXS + + """ + if 'float_precision' not in kwargs: + kwargs['float_precision'] = 'round_trip' + + micro_xs = cls(read_csv(csv_file, index_col=0, **kwargs)) + + cls._validate_micro_xs_inputs(list(micro_xs.index), + list(micro_xs.columns), + micro_xs.to_numpy()) + return micro_xs + + @staticmethod + def _validate_micro_xs_inputs(nuclides, reactions, data): + check_iterable_type('nuclides', nuclides, str) + check_iterable_type('reactions', reactions, str) + check_type('data', data, np.ndarray, expected_iter_type=float) + for reaction in reactions: + check_value('reactions', reaction, _valid_rxns) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py new file mode 100644 index 000000000..ee569bf71 --- /dev/null +++ b/openmc/deplete/openmc_operator.py @@ -0,0 +1,570 @@ +"""OpenMC transport operator + +This module implements functions shared by both OpenMC transport-coupled and +transport-independent transport operators. + +""" + +from abc import abstractmethod +from collections import OrderedDict + +import numpy as np + +import openmc +from openmc.mpi import comm +from .abc import TransportOperator, OperatorResult +from .atom_number import AtomNumber +from .reaction_rates import ReactionRates + +__all__ = ["OpenMCOperator", "OperatorResult"] + + +def _distribute(items): + """Distribute items across MPI communicator + + Parameters + ---------- + items : list + List of items of distribute + + Returns + ------- + list + Items assigned to process that called + + """ + min_size, extra = divmod(len(items), comm.size) + j = 0 + for i in range(comm.size): + chunk_size = min_size + int(i < extra) + if comm.rank == i: + return items[j:j + chunk_size] + j += chunk_size + + +class OpenMCOperator(TransportOperator): + """Abstract class holding OpenMC-specific functions for running + depletion calculations. + + Specific classes for running transport-coupled or transport-independent + depletion calculations are implemented as subclasses of OpenMCOperator. + + Parameters + ---------- + materials : openmc.Materials + List of all materials in the model + cross_sections : str or pandas.DataFrame + Path to continuous energy cross section library, or object containing + one-group cross-sections. + chain_file : str, optional + Path to the depletion chain XML file. Defaults to the file + listed under ``depletion_chain`` in + :envvar:`OPENMC_CROSS_SECTIONS` environment variable. + prev_results : Results, optional + Results from a previous depletion calculation. If this argument is + specified, the depletion calculation will start from the latest state + in the previous results. + diff_burnable_mats : bool, optional + Whether to differentiate burnable materials with multiple instances. + Volumes are divided equally from the original material volume. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. + dilute_initial : float, optional + Initial atom density [atoms/cm^3] to add for nuclides that are zero + in initial condition to ensure they exist in the decay chain. + Only done for nuclides with reaction rates. + helper_kwargs : dict + Keyword arguments for helper classes + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce()` to reduce the + depletion chain up to ``reduce_chain_level``. + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + + + Attributes + ---------- + materials : openmc.Materials + All materials present in the model + cross_sections : str or MicroXS + Path to continuous energy cross section library, or object + containing one-group cross-sections. + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the decay + chain. Only done for nuclides with reaction rates. + output_dir : pathlib.Path + Path to output directory to save results. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. + number : openmc.deplete.AtomNumber + Total number of atoms in simulation. + nuclides_with_data : set of str + A set listing all unique nuclides available from cross_sections.xml. + chain : openmc.deplete.Chain + The depletion chain information necessary to form matrices and tallies. + reaction_rates : openmc.deplete.ReactionRates + Reaction rates from the last operator step. + burnable_mats : list of str + All burnable material IDs + heavy_metal : float + Initial heavy metal inventory [g] + local_mats : list of str + All burnable material IDs being managed by a single process + prev_res : Results or None + Results from a previous depletion calculation. ``None`` if no + results are to be used. + + """ + + def __init__( + self, + materials=None, + cross_sections=None, + chain_file=None, + prev_results=None, + diff_burnable_mats=False, + fission_q=None, + dilute_initial=0.0, + helper_kwargs=None, + reduce_chain=False, + reduce_chain_level=None): + + super().__init__(chain_file, fission_q, dilute_initial, prev_results) + self.round_number = False + self.materials = materials + self.cross_sections = cross_sections + + # Reduce the chain to only those nuclides present + if reduce_chain: + init_nuclides = set() + for material in self.materials: + if not material.depletable: + continue + for name, _dens_percent, _dens_type in material.nuclides: + init_nuclides.add(name) + + self.chain = self.chain.reduce(init_nuclides, reduce_chain_level) + + if diff_burnable_mats: + self._differentiate_burnable_mats() + + # Determine which nuclides have cross section data + # This nuclides variables contains every nuclides + # for which there is an entry in the micro_xs parameter + openmc.reset_auto_ids() + self.burnable_mats, volumes, all_nuclides = self._get_burnable_mats() + self.local_mats = _distribute(self.burnable_mats) + + self._mat_index_map = { + lm: self.burnable_mats.index(lm) for lm in self.local_mats} + + if self.prev_res is not None: + self._load_previous_results() + + self.nuclides_with_data = self._get_nuclides_with_data( + self.cross_sections) + + # Select nuclides with data that are also in the chain + self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides + if nuc.name in self.nuclides_with_data] + + # Extract number densities from the geometry / previous depletion run + self._extract_number(self.local_mats, + volumes, + all_nuclides, + self.prev_res) + + # Create reaction rates array + self.reaction_rates = ReactionRates( + self.local_mats, self._burnable_nucs, self.chain.reactions) + + self._get_helper_classes(helper_kwargs) + + def _differentiate_burnable_mats(self): + """Assign distribmats for each burnable material""" + pass + + def _get_burnable_mats(self): + """Determine depletable materials, volumes, and nuclides + + Returns + ------- + burnable_mats : list of str + List of burnable material IDs + volume : OrderedDict of str to float + Volume of each material in [cm^3] + nuclides : list of str + Nuclides in order of how they'll appear in the simulation. + + """ + + burnable_mats = set() + model_nuclides = set() + volume = OrderedDict() + + self.heavy_metal = 0.0 + + # Iterate once through the geometry to get dictionaries + for mat in self.materials: + for nuclide in mat.get_nuclides(): + model_nuclides.add(nuclide) + if mat.depletable: + burnable_mats.add(str(mat.id)) + if mat.volume is None: + raise RuntimeError("Volume not specified for depletable " + "material with ID={}.".format(mat.id)) + volume[str(mat.id)] = mat.volume + self.heavy_metal += mat.fissionable_mass + + # Make sure there are burnable materials + if not burnable_mats: + raise RuntimeError( + "No depletable materials were found in the model.") + + # Sort the sets + burnable_mats = sorted(burnable_mats, key=int) + model_nuclides = sorted(model_nuclides) + + # Construct a global nuclide dictionary, burned first + nuclides = list(self.chain.nuclide_dict) + for nuc in model_nuclides: + if nuc not in nuclides: + nuclides.append(nuc) + + return burnable_mats, volume, nuclides + + def _load_previous_results(self): + """Load results from a previous depletion simulation""" + pass + + @abstractmethod + def _get_nuclides_with_data(self, cross_sections): + """Find nuclides with cross section data + + Parameters + ---------- + cross_sections : str or pandas.DataFrame + Path to continuous energy cross section library, or object + containing one-group cross-sections. + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ + + def _extract_number(self, local_mats, volume, all_nuclides, prev_res=None): + """Construct AtomNumber using geometry + + Parameters + ---------- + local_mats : list of str + Material IDs to be managed by this process + volume : OrderedDict of str to float + Volumes for the above materials in [cm^3] + all_nuclides : list of str + Nuclides to be used in the simulation. + prev_res : Results, optional + Results from a previous depletion calculation + + """ + self.number = AtomNumber(local_mats, all_nuclides, volume, len(self.chain)) + + if self.dilute_initial != 0.0: + for nuc in self._burnable_nucs: + self.number.set_atom_density( + np.s_[:], nuc, self.dilute_initial) + + # Now extract and store the number densities + # From the geometry if no previous depletion results + if prev_res is None: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_mat(mat) + + # Else from previous depletion results + else: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_results(mat, prev_res) + + def _set_number_from_mat(self, mat): + """Extracts material and number densities from openmc.Material + + Parameters + ---------- + mat : openmc.Material + The material to read from + + """ + mat_id = str(mat.id) + + for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): + atom_per_cc = atom_per_bcm * 1.0e24 + self.number.set_atom_density(mat_id, nuclide, atom_per_cc) + + def _set_number_from_results(self, mat, prev_res): + """Extracts material nuclides and number densities. + + If the nuclide concentration's evolution is tracked, the densities come + from depletion results. Else, densities are extracted from the geometry + in the summary. + + Parameters + ---------- + mat : openmc.Material + The material to read from + prev_res : Results + Results from a previous depletion calculation + + """ + mat_id = str(mat.id) + + # Get nuclide lists from geometry and depletion results + depl_nuc = prev_res[-1].nuc_to_ind + geom_nuc_densities = mat.get_nuclide_atom_densities() + + # Merge lists of nuclides, with the same order for every calculation + geom_nuc_densities.update(depl_nuc) + + for nuclide, atom_per_bcm in geom_nuc_densities.items(): + if nuclide in depl_nuc: + concentration = prev_res.get_atoms(mat_id, nuclide)[1][-1] + volume = prev_res[-1].volume[mat_id] + atom_per_cc = concentration / volume + else: + atom_per_cc = atom_per_bcm * 1.0e24 + + self.number.set_atom_density(mat_id, nuclide, atom_per_cc) + + @abstractmethod + def _get_helper_classes(self, helper_kwargs): + """Create the ``_rate_helper``, ``_normalization_helper``, and + ``_yield_helper`` objects. + + Parameters + ---------- + helper_kwargs : dict + Keyword arguments for helper classes + + """ + + def initial_condition(self, materials): + """Performs final setup and returns initial condition. + + Parameters + ---------- + materials : list of str + list of material IDs + + Returns + ------- + list of numpy.ndarray + Total density for initial conditions. + + """ + + self._rate_helper.generate_tallies(materials, self.chain.reactions) + self._normalization_helper.prepare( + self.chain.nuclides, self.reaction_rates.index_nuc) + # Tell fission yield helper what materials this process is + # responsible for + self._yield_helper.generate_tallies( + materials, tuple(sorted(self._mat_index_map.values()))) + + # Return number density vector + return list(self.number.get_mat_slice(np.s_[:])) + + def _update_materials_and_nuclides(self, vec): + """Update the number density, material compositions, and nuclide + lists in helper objects + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms. + + """ + + # Update the number densities regardless of the source rate + self.number.set_density(vec) + self._update_materials() + + # Prevent OpenMC from complaining about re-creating tallies + openmc.reset_auto_ids() + + # Update tally nuclides data in preparation for transport solve + nuclides = self._get_reaction_nuclides() + self._rate_helper.nuclides = nuclides + self._normalization_helper.nuclides = nuclides + self._yield_helper.update_tally_nuclides(nuclides) + + @abstractmethod + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" + + def write_bos_data(self, step): + """Document beginning of step data for a given step + + Called at the beginning of a depletion step and at + the final point in the simulation. + + Parameters + ---------- + step : int + Current depletion step including restarts + """ + # Since we aren't running a transport simulation, we simply pass + pass + + def _get_reaction_nuclides(self): + """Determine nuclides that should be tallied for reaction rates. + + This method returns a list of all nuclides that have cross section data + and are listed in the depletion chain. Technically, we should count + nuclides that may not appear in the depletion chain because we still + need to get the fission reaction rate for these nuclides in order to + normalize power, but that is left as a future exercise. + + Returns + ------- + list of str + Nuclides with reaction rates + + """ + nuc_set = set() + + # Create the set of all nuclides in the decay chain in materials marked + # for burning in which the number density is greater than zero. + for nuc in self.number.nuclides: + if nuc in self.nuclides_with_data: + if np.sum(self.number[:, nuc]) > 0.0: + nuc_set.add(nuc) + + # Communicate which nuclides have nonzeros to rank 0 + if comm.rank == 0: + for i in range(1, comm.size): + nuc_newset = comm.recv(source=i, tag=i) + nuc_set |= nuc_newset + else: + comm.send(nuc_set, dest=0, tag=comm.rank) + + if comm.rank == 0: + # Sort nuclides in the same order as self.number + nuc_list = [nuc for nuc in self.number.nuclides + if nuc in nuc_set] + else: + nuc_list = None + + # Store list of nuclides on each process + nuc_list = comm.bcast(nuc_list) + return [nuc for nuc in nuc_list if nuc in self.chain] + + def _calculate_reaction_rates(self, source_rate): + """Unpack tallies from OpenMC and return an operator result + + This method uses OpenMC's C API bindings to determine the k-effective + value and reaction rates from the simulation. The reaction rates are + normalized by a helper class depending on the method being used. + + Parameters + ---------- + source_rate : float + Power in [W] or source rate in [neutron/sec] + + Returns + ------- + rates : openmc.deplete.ReactionRates + Reaction rates for nuclides + + """ + rates = self.reaction_rates + rates.fill(0.0) + + # Extract reaction nuclides + rxn_nuclides = self._rate_helper.nuclides + + # Form fast map + nuc_ind = [rates.index_nuc[nuc] for nuc in rxn_nuclides] + react_ind = [rates.index_rx[react] for react in self.chain.reactions] + + # Keep track of energy produced from all reactions in eV per source + # particle + self._normalization_helper.reset() + self._yield_helper.unpack() + + # Store fission yield dictionaries + fission_yields = [] + + # Create arrays to store fission Q values, reaction rates, and nuclide + # numbers, zeroed out in material iteration + number = np.empty(rates.n_nuc) + + fission_ind = rates.index_rx.get("fission") + + # Extract results + for i, mat in enumerate(self.local_mats): + # Get tally index + mat_index = self._mat_index_map[mat] + + # Zero out reaction rates and nuclide numbers + number.fill(0.0) + + # Get new number densities + for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): + number[i_nuc_results] = self.number[mat, nuc] + + tally_rates = self._rate_helper.get_material_rates( + mat_index, nuc_ind, react_ind) + + # Compute fission yields for this material + fission_yields.append(self._yield_helper.weighted_yields(i)) + + # Accumulate energy from fission + if fission_ind is not None: + self._normalization_helper.update( + tally_rates[:, fission_ind]) + + # Divide by total number and store + rates[i] = self._rate_helper.divide_by_adens(number) + + # Scale reaction rates to obtain units of reactions/sec + rates *= self._normalization_helper.factor(source_rate) + + # Store new fission yields on the chain + self.chain.fission_yields = fission_yields + + return rates + + def get_results_info(self): + """Returns volume list, material lists, and nuc lists. + + Returns + ------- + volume : dict of str float + Volumes corresponding to materials in full_burn_dict + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all material IDs to be burned. Used for sorting the simulation. + full_burn_list : list + List of all burnable material IDs + + """ + nuc_list = self.number.burnable_nuclides + burn_list = self.local_mats + + volume = {} + for i, mat in enumerate(burn_list): + volume[mat] = self.number.volume[i] + + # Combine volume dictionaries across processes + volume_list = comm.allgather(volume) + volume = {k: v for d in volume_list for k, v in d.items()} + + return volume, nuc_list, burn_list, self.burnable_mats diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py deleted file mode 100644 index 7ef874e87..000000000 --- a/openmc/deplete/operator.py +++ /dev/null @@ -1,822 +0,0 @@ -"""OpenMC transport operator - -This module implements a transport operator for OpenMC so that it can be used by -depletion integrators. The implementation makes use of the Python bindings to -OpenMC's C API so that reading tally results and updating material number -densities is all done in-memory instead of through the filesystem. - -""" - -import copy -from collections import OrderedDict -import os -from warnings import warn - -import numpy as np -from uncertainties import ufloat - -import openmc -from openmc.checkvalue import check_value -from openmc.data import DataLibrary -from openmc.exceptions import DataError -import openmc.lib -from openmc.mpi import comm -from .abc import TransportOperator, OperatorResult -from .atom_number import AtomNumber -from .chain import _find_chain_file -from .reaction_rates import ReactionRates -from .results import Results -from .helpers import ( - DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper, - FissionYieldCutoffHelper, AveragedFissionYieldHelper, EnergyScoreHelper, - SourceRateHelper, FluxCollapseHelper) - - -__all__ = ["Operator", "OperatorResult"] - - -def _distribute(items): - """Distribute items across MPI communicator - - Parameters - ---------- - items : list - List of items of distribute - - Returns - ------- - list - Items assigned to process that called - - """ - min_size, extra = divmod(len(items), comm.size) - j = 0 - for i in range(comm.size): - chunk_size = min_size + int(i < extra) - if comm.rank == i: - return items[j:j + chunk_size] - j += chunk_size - - -def _find_cross_sections(model): - """Determine cross sections to use for depletion""" - if model.materials and model.materials.cross_sections is not None: - # Prefer info from Model class if available - return model.materials.cross_sections - - # otherwise fallback to environment variable - cross_sections = os.environ.get("OPENMC_CROSS_SECTIONS") - if cross_sections is None: - raise DataError( - "Cross sections were not specified in Model.materials and " - "the OPENMC_CROSS_SECTIONS environment variable is not set." - ) - return cross_sections - - -class Operator(TransportOperator): - """OpenMC transport operator for depletion. - - Instances of this class can be used to perform depletion using OpenMC as the - transport operator. Normally, a user needn't call methods of this class - directly. Instead, an instance of this class is passed to an integrator - class, such as :class:`openmc.deplete.CECMIntegrator`. - - .. versionchanged:: 0.13.0 - The geometry and settings parameters have been replaced with a - model parameter that takes a :class:`~openmc.model.Model` object - - Parameters - ---------- - model : openmc.model.Model - OpenMC model object - chain_file : str, optional - Path to the depletion chain XML file. Defaults to the file - listed under ``depletion_chain`` in - :envvar:`OPENMC_CROSS_SECTIONS` environment variable. - prev_results : Results, optional - Results from a previous depletion calculation. If this argument is - specified, the depletion calculation will start from the latest state - in the previous results. - diff_burnable_mats : bool, optional - Whether to differentiate burnable materials with multiple instances. - Volumes are divided equally from the original material volume. - Default: False. - normalization_mode : {"energy-deposition", "fission-q", "source-rate"} - Indicate how tally results should be normalized. ``"energy-deposition"`` - computes the total energy deposited in the system and uses the ratio of - the power to the energy produced as a normalization factor. - ``"fission-q"`` uses the fission Q values from the depletion chain to - compute the total energy deposited. ``"source-rate"`` normalizes - tallies based on the source rate (for fixed source calculations). - fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. If not given, - values will be pulled from the ``chain_file``. Only applicable - if ``"normalization_mode" == "fission-q"`` - dilute_initial : float, optional - Initial atom density [atoms/cm^3] to add for nuclides that are zero - in initial condition to ensure they exist in the decay chain. - Only done for nuclides with reaction rates. - Defaults to 1.0e3. - fission_yield_mode : {"constant", "cutoff", "average"} - Key indicating what fission product yield scheme to use. The - key determines what fission energy helper is used: - - * "constant": :class:`~openmc.deplete.helpers.ConstantFissionYieldHelper` - * "cutoff": :class:`~openmc.deplete.helpers.FissionYieldCutoffHelper` - * "average": :class:`~openmc.deplete.helpers.AveragedFissionYieldHelper` - - The documentation on these classes describe their methodology - and differences. Default: ``"constant"`` - fission_yield_opts : dict of str to option, optional - Optional arguments to pass to the helper determined by - ``fission_yield_mode``. Will be passed directly on to the - helper. Passing a value of None will use the defaults for - the associated helper. - reaction_rate_mode : {"direct", "flux"}, optional - Indicate how one-group reaction rates should be calculated. The "direct" - method tallies transmutation reaction rates directly. The "flux" method - tallies a multigroup flux spectrum and then collapses one-group reaction - rates after a transport solve (with an option to tally some reaction - rates directly). - - .. versionadded:: 0.12.1 - reaction_rate_opts : dict, optional - Keyword arguments that are passed to the reaction rate helper class. - When ``reaction_rate_mode`` is set to "flux", energy group boundaries - can be set using the "energies" key. See the - :class:`~openmc.deplete.helpers.FluxCollapseHelper` class for all - options. - - .. versionadded:: 0.12.1 - reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. - - .. versionadded:: 0.12 - reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used - if ``reduce_chain`` evaluates to true. The default value of - ``None`` implies no limit on the depth. - - .. versionadded:: 0.12 - - Attributes - ---------- - model : openmc.model.Model - OpenMC model object - geometry : openmc.Geometry - OpenMC geometry object - settings : openmc.Settings - OpenMC settings object - dilute_initial : float - Initial atom density [atoms/cm^3] to add for nuclides that - are zero in initial condition to ensure they exist in the decay - chain. Only done for nuclides with reaction rates. - output_dir : pathlib.Path - Path to output directory to save results. - round_number : bool - Whether or not to round output to OpenMC to 8 digits. - Useful in testing, as OpenMC is incredibly sensitive to exact values. - number : openmc.deplete.AtomNumber - Total number of atoms in simulation. - nuclides_with_data : set of str - A set listing all unique nuclides available from cross_sections.xml. - chain : openmc.deplete.Chain - The depletion chain information necessary to form matrices and tallies. - reaction_rates : openmc.deplete.ReactionRates - Reaction rates from the last operator step. - burnable_mats : list of str - All burnable material IDs - heavy_metal : float - Initial heavy metal inventory [g] - local_mats : list of str - All burnable material IDs being managed by a single process - prev_res : Results or None - Results from a previous depletion calculation. ``None`` if no - results are to be used. - cleanup_when_done : bool - Whether to finalize and clear the shared library memory when the - depletion operation is complete. Defaults to clearing the library. - """ - _fission_helpers = { - "average": AveragedFissionYieldHelper, - "constant": ConstantFissionYieldHelper, - "cutoff": FissionYieldCutoffHelper, - } - - def __init__(self, model, chain_file=None, prev_results=None, - diff_burnable_mats=False, normalization_mode="fission-q", - fission_q=None, dilute_initial=1.0e3, - fission_yield_mode="constant", fission_yield_opts=None, - reaction_rate_mode="direct", reaction_rate_opts=None, - reduce_chain=False, reduce_chain_level=None): - # check for old call to constructor - if isinstance(model, openmc.Geometry): - msg = "As of version 0.13.0 openmc.deplete.Operator requires an " \ - "openmc.Model object rather than the openmc.Geometry and " \ - "openmc.Settings parameters. Please use the geometry and " \ - "settings objects passed here to create a model with which " \ - "to generate the depletion Operator." - raise TypeError(msg) - - # Determine cross sections / depletion chain - cross_sections = _find_cross_sections(model) - if chain_file is None: - chain_file = _find_chain_file(cross_sections) - - check_value('fission yield mode', fission_yield_mode, - self._fission_helpers.keys()) - check_value('normalization mode', normalization_mode, - ('energy-deposition', 'fission-q', 'source-rate')) - if normalization_mode != "fission-q": - if fission_q is not None: - warn("Fission Q dictionary will not be used") - fission_q = None - super().__init__(chain_file, fission_q, dilute_initial, prev_results) - self.round_number = False - self.model = model - self.settings = model.settings - self.geometry = model.geometry - - # determine set of materials in the model - if not model.materials: - model.materials = openmc.Materials( - model.geometry.get_all_materials().values() - ) - self.materials = model.materials - - self.cleanup_when_done = True - - # Reduce the chain before we create more materials - if reduce_chain: - all_isotopes = set() - for material in self.materials: - if not material.depletable: - continue - for name, _dens_percent, _dens_type in material.nuclides: - all_isotopes.add(name) - self.chain = self.chain.reduce(all_isotopes, reduce_chain_level) - - # Differentiate burnable materials with multiple instances - if diff_burnable_mats: - self._differentiate_burnable_mats() - self.materials = openmc.Materials( - model.geometry.get_all_materials().values() - ) - - # Clear out OpenMC, create task lists, distribute - openmc.reset_auto_ids() - self.burnable_mats, volume, nuclides = self._get_burnable_mats() - self.local_mats = _distribute(self.burnable_mats) - - # Generate map from local materials => material index - self._mat_index_map = { - lm: self.burnable_mats.index(lm) for lm in self.local_mats} - - if self.prev_res is not None: - # Reload volumes into geometry - prev_results[-1].transfer_volumes(self.model) - - # Store previous results in operator - # Distribute reaction rates according to those tracked - # on this process - if comm.size == 1: - self.prev_res = prev_results - else: - self.prev_res = Results() - mat_indexes = _distribute(range(len(self.burnable_mats))) - for res_obj in prev_results: - new_res = res_obj.distribute(self.local_mats, mat_indexes) - self.prev_res.append(new_res) - - # Determine which nuclides have incident neutron data - self.nuclides_with_data = self._get_nuclides_with_data(cross_sections) - - # Select nuclides with data that are also in the chain - self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides - if nuc.name in self.nuclides_with_data] - - # Extract number densities from the geometry / previous depletion run - self._extract_number(self.local_mats, volume, nuclides, self.prev_res) - - # Create reaction rates array - self.reaction_rates = ReactionRates( - self.local_mats, self._burnable_nucs, self.chain.reactions) - - # Get classes to assist working with tallies - if reaction_rate_mode == "direct": - self._rate_helper = DirectReactionRateHelper( - self.reaction_rates.n_nuc, self.reaction_rates.n_react) - elif reaction_rate_mode == "flux": - if reaction_rate_opts is None: - reaction_rate_opts = {} - - # Ensure energy group boundaries were specified - if 'energies' not in reaction_rate_opts: - raise ValueError( - "Energy group boundaries must be specified in the " - "reaction_rate_opts argument when reaction_rate_mode is" - "set to 'flux'.") - - self._rate_helper = FluxCollapseHelper( - self.reaction_rates.n_nuc, - self.reaction_rates.n_react, - **reaction_rate_opts - ) - else: - raise ValueError("Invalid reaction rate mode.") - - if normalization_mode == "fission-q": - self._normalization_helper = ChainFissionHelper() - elif normalization_mode == "energy-deposition": - score = "heating" if self.settings.photon_transport else "heating-local" - self._normalization_helper = EnergyScoreHelper(score) - else: - self._normalization_helper = SourceRateHelper() - - # Select and create fission yield helper - fission_helper = self._fission_helpers[fission_yield_mode] - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) - self._yield_helper = fission_helper.from_operator( - self, **fission_yield_opts) - - def __call__(self, vec, source_rate): - """Runs a simulation. - - Simulation will abort under the following circumstances: - - 1) No energy is computed using OpenMC tallies. - - Parameters - ---------- - vec : list of numpy.ndarray - Total atoms to be used in function. - source_rate : float - Power in [W] or source rate in [neutron/sec] - - Returns - ------- - openmc.deplete.OperatorResult - Eigenvalue and reaction rates resulting from transport operator - - """ - # Reset results in OpenMC - openmc.lib.reset() - - # Update the number densities regardless of the source rate - self.number.set_density(vec) - self._update_materials() - - # If the source rate is zero, return zero reaction rates without running - # a transport solve - if source_rate == 0.0: - rates = self.reaction_rates.copy() - rates.fill(0.0) - return OperatorResult(ufloat(0.0, 0.0), rates) - - # Prevent OpenMC from complaining about re-creating tallies - openmc.reset_auto_ids() - - # Update tally nuclides data in preparation for transport solve - nuclides = self._get_tally_nuclides() - self._rate_helper.nuclides = nuclides - self._normalization_helper.nuclides = nuclides - self._yield_helper.update_tally_nuclides(nuclides) - - # Run OpenMC - openmc.lib.run() - openmc.lib.reset_timers() - - # Extract results - op_result = self._unpack_tallies_and_normalize(source_rate) - - return copy.deepcopy(op_result) - - @staticmethod - def write_bos_data(step): - """Write a state-point file with beginning of step data - - Parameters - ---------- - step : int - Current depletion step including restarts - """ - openmc.lib.statepoint_write( - "openmc_simulation_n{}.h5".format(step), - write_source=False) - - def _differentiate_burnable_mats(self): - """Assign distribmats for each burnable material - - """ - - # Count the number of instances for each cell and material - self.geometry.determine_paths(instances_only=True) - - # Extract all burnable materials which have multiple instances - distribmats = set( - [mat for mat in self.materials - if mat.depletable and mat.num_instances > 1]) - - for mat in distribmats: - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - "material with ID={}.".format(mat.id)) - mat.volume /= mat.num_instances - - if distribmats: - # Assign distribmats to cells - for cell in self.geometry.get_all_material_cells().values(): - if cell.fill in distribmats: - mat = cell.fill - cell.fill = [mat.clone() - for i in range(cell.num_instances)] - - def _get_burnable_mats(self): - """Determine depletable materials, volumes, and nuclides - - Returns - ------- - burnable_mats : list of str - List of burnable material IDs - volume : OrderedDict of str to float - Volume of each material in [cm^3] - nuclides : list of str - Nuclides in order of how they'll appear in the simulation. - - """ - - burnable_mats = set() - model_nuclides = set() - volume = OrderedDict() - - self.heavy_metal = 0.0 - - # Iterate once through the geometry to get dictionaries - for mat in self.materials: - for nuclide in mat.get_nuclides(): - model_nuclides.add(nuclide) - if mat.depletable: - burnable_mats.add(str(mat.id)) - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - "material with ID={}.".format(mat.id)) - volume[str(mat.id)] = mat.volume - self.heavy_metal += mat.fissionable_mass - - # Make sure there are burnable materials - if not burnable_mats: - raise RuntimeError( - "No depletable materials were found in the model.") - - # Sort the sets - burnable_mats = sorted(burnable_mats, key=int) - model_nuclides = sorted(model_nuclides) - - # Construct a global nuclide dictionary, burned first - nuclides = list(self.chain.nuclide_dict) - for nuc in model_nuclides: - if nuc not in nuclides: - nuclides.append(nuc) - - return burnable_mats, volume, nuclides - - def _extract_number(self, local_mats, volume, nuclides, prev_res=None): - """Construct AtomNumber using geometry - - Parameters - ---------- - local_mats : list of str - Material IDs to be managed by this process - volume : OrderedDict of str to float - Volumes for the above materials in [cm^3] - nuclides : list of str - Nuclides to be used in the simulation. - prev_res : Results, optional - Results from a previous depletion calculation - - """ - self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) - - if self.dilute_initial != 0.0: - for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) - - # Now extract and store the number densities - # From the geometry if no previous depletion results - if prev_res is None: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_mat(mat) - - # Else from previous depletion results - else: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_results(mat, prev_res) - - def _set_number_from_mat(self, mat): - """Extracts material and number densities from openmc.Material - - Parameters - ---------- - mat : openmc.Material - The material to read from - - """ - mat_id = str(mat.id) - - for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): - atom_per_cc = atom_per_bcm * 1.0e24 - self.number.set_atom_density(mat_id, nuclide, atom_per_cc) - - def _set_number_from_results(self, mat, prev_res): - """Extracts material nuclides and number densities. - - If the nuclide concentration's evolution is tracked, the densities come - from depletion results. Else, densities are extracted from the geometry - in the summary. - - Parameters - ---------- - mat : openmc.Material - The material to read from - prev_res : Results - Results from a previous depletion calculation - - """ - mat_id = str(mat.id) - - # Get nuclide lists from geometry and depletion results - depl_nuc = prev_res[-1].nuc_to_ind - geom_nuc_densities = mat.get_nuclide_atom_densities() - - # Merge lists of nuclides, with the same order for every calculation - geom_nuc_densities.update(depl_nuc) - - for nuclide, atom_per_bcm in geom_nuc_densities.items(): - if nuclide in depl_nuc: - concentration = prev_res.get_atoms(mat_id, nuclide)[1][-1] - volume = prev_res[-1].volume[mat_id] - atom_per_cc = concentration / volume - else: - atom_per_cc = atom_per_bcm * 1.0e24 - - self.number.set_atom_density(mat_id, nuclide, atom_per_cc) - - def initial_condition(self): - """Performs final setup and returns initial condition. - - Returns - ------- - list of numpy.ndarray - Total density for initial conditions. - """ - - # Create XML files - if comm.rank == 0: - self.geometry.export_to_xml() - self.settings.export_to_xml() - self._generate_materials_xml() - - # Initialize OpenMC library - comm.barrier() - if not openmc.lib.is_initialized: - openmc.lib.init(intracomm=comm) - - # Generate tallies in memory - materials = [openmc.lib.materials[int(i)] - for i in self.burnable_mats] - self._rate_helper.generate_tallies(materials, self.chain.reactions) - self._normalization_helper.prepare( - self.chain.nuclides, self.reaction_rates.index_nuc) - # Tell fission yield helper what materials this process is - # responsible for - self._yield_helper.generate_tallies( - materials, tuple(sorted(self._mat_index_map.values()))) - - # Return number density vector - return list(self.number.get_mat_slice(np.s_[:])) - - def finalize(self): - """Finalize a depletion simulation and release resources.""" - if self.cleanup_when_done: - openmc.lib.finalize() - - def _update_materials(self): - """Updates material compositions in OpenMC on all processes.""" - - for rank in range(comm.size): - number_i = comm.bcast(self.number, root=rank) - - for mat in number_i.materials: - nuclides = [] - densities = [] - for nuc in number_i.nuclides: - if nuc in self.nuclides_with_data: - val = 1.0e-24 * number_i.get_atom_density(mat, nuc) - - # If nuclide is zero, do not add to the problem. - if val > 0.0: - if self.round_number: - val_magnitude = np.floor(np.log10(val)) - val_scaled = val / 10**val_magnitude - val_round = round(val_scaled, 8) - - val = val_round * 10**val_magnitude - - nuclides.append(nuc) - densities.append(val) - else: - # Only output warnings if values are significantly - # negative. CRAM does not guarantee positive values. - if val < -1.0e-21: - print("WARNING: nuclide ", nuc, " in material ", mat, - " is negative (density = ", val, " at/barn-cm)") - number_i[mat, nuc] = 0.0 - - # Update densities on C API side - mat_internal = openmc.lib.materials[int(mat)] - mat_internal.set_densities(nuclides, densities) - - #TODO Update densities on the Python side, otherwise the - # summary.h5 file contains densities at the first time step - - def _generate_materials_xml(self): - """Creates materials.xml from self.number. - - Due to uncertainty with how MPI interacts with OpenMC API, this - constructs the XML manually. The long term goal is to do this - through direct memory writing. - - """ - # Sort nuclides according to order in AtomNumber object - nuclides = list(self.number.nuclides) - for mat in self.materials: - mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) - - self.materials.export_to_xml() - - def _get_tally_nuclides(self): - """Determine nuclides that should be tallied for reaction rates. - - This method returns a list of all nuclides that have neutron data and - are listed in the depletion chain. Technically, we should tally nuclides - that may not appear in the depletion chain because we still need to get - the fission reaction rate for these nuclides in order to normalize - power, but that is left as a future exercise. - - Returns - ------- - list of str - Tally nuclides - - """ - nuc_set = set() - - # Create the set of all nuclides in the decay chain in materials marked - # for burning in which the number density is greater than zero. - for nuc in self.number.nuclides: - if nuc in self.nuclides_with_data: - if np.sum(self.number[:, nuc]) > 0.0: - nuc_set.add(nuc) - - # Communicate which nuclides have nonzeros to rank 0 - if comm.rank == 0: - for i in range(1, comm.size): - nuc_newset = comm.recv(source=i, tag=i) - nuc_set |= nuc_newset - else: - comm.send(nuc_set, dest=0, tag=comm.rank) - - if comm.rank == 0: - # Sort nuclides in the same order as self.number - nuc_list = [nuc for nuc in self.number.nuclides - if nuc in nuc_set] - else: - nuc_list = None - - # Store list of tally nuclides on each process - nuc_list = comm.bcast(nuc_list) - return [nuc for nuc in nuc_list if nuc in self.chain] - - def _unpack_tallies_and_normalize(self, source_rate): - """Unpack tallies from OpenMC and return an operator result - - This method uses OpenMC's C API bindings to determine the k-effective - value and reaction rates from the simulation. The reaction rates are - normalized by a helper class depending on the method being used. - - Parameters - ---------- - source_rate : float - Power in [W] or source rate in [neutron/sec] - - Returns - ------- - openmc.deplete.OperatorResult - Eigenvalue and reaction rates resulting from transport operator - - """ - rates = self.reaction_rates - rates.fill(0.0) - - # Get k and uncertainty - keff = ufloat(*openmc.lib.keff()) - - # Extract tally bins - nuclides = self._rate_helper.nuclides - - # Form fast map - nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] - react_ind = [rates.index_rx[react] for react in self.chain.reactions] - - # Keep track of energy produced from all reactions in eV per source - # particle - self._normalization_helper.reset() - self._yield_helper.unpack() - - # Store fission yield dictionaries - fission_yields = [] - - # Create arrays to store fission Q values, reaction rates, and nuclide - # numbers, zeroed out in material iteration - number = np.empty(rates.n_nuc) - - fission_ind = rates.index_rx.get("fission") - - # Extract results - for i, mat in enumerate(self.local_mats): - # Get tally index - mat_index = self._mat_index_map[mat] - - # Zero out reaction rates and nuclide numbers - number.fill(0.0) - - # Get new number densities - for nuc, i_nuc_results in zip(nuclides, nuc_ind): - number[i_nuc_results] = self.number[mat, nuc] - - tally_rates = self._rate_helper.get_material_rates( - mat_index, nuc_ind, react_ind) - - # Compute fission yields for this material - fission_yields.append(self._yield_helper.weighted_yields(i)) - - # Accumulate energy from fission - if fission_ind is not None: - self._normalization_helper.update(tally_rates[:, fission_ind]) - - # Divide by total number and store - rates[i] = self._rate_helper.divide_by_adens(number) - - # Scale reaction rates to obtain units of reactions/sec - rates *= self._normalization_helper.factor(source_rate) - - # Store new fission yields on the chain - self.chain.fission_yields = fission_yields - - return OperatorResult(keff, rates) - - def _get_nuclides_with_data(self, cross_sections): - """Loads cross_sections.xml file to find nuclides with neutron data""" - nuclides = set() - data_lib = DataLibrary.from_xml(cross_sections) - for library in data_lib.libraries: - if library['type'] != 'neutron': - continue - for name in library['materials']: - if name not in nuclides: - nuclides.add(name) - - return nuclides - - def get_results_info(self): - """Returns volume list, material lists, and nuc lists. - - Returns - ------- - volume : dict of str float - Volumes corresponding to materials in full_burn_dict - nuc_list : list of str - A list of all nuclide names. Used for sorting the simulation. - burn_list : list of int - A list of all material IDs to be burned. Used for sorting the simulation. - full_burn_list : list - List of all burnable material IDs - - """ - nuc_list = self.number.burnable_nuclides - burn_list = self.local_mats - - volume = {} - for i, mat in enumerate(burn_list): - volume[mat] = self.number.volume[i] - - # Combine volume dictionaries across processes - volume_list = comm.allgather(volume) - volume = {k: v for d in volume_list for k, v in d.items()} - - return volume, nuc_list, burn_list, self.burnable_mats diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index c238002fe..c2138b55c 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -16,6 +16,19 @@ __all__ = ["Results", "ResultsList"] def _get_time_as(seconds, units): + """Converts the time in seconds to time in different units + + Parameters + ---------- + seconds : float + The time to convert expressed in seconds + units : {"s", "min", "h", "d", "a"} + The units to convert time into. Available options are seconds ``"s"``, + minutes ``"min"``, hours ``"h"`` days ``"d"``, Julian years ``"a"`` + + """ + if units == "a": + return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year if units == "d": return seconds / (60 * 60 * 24) elif units == "h": @@ -81,9 +94,9 @@ class Results(list): .. note:: Initial values for some isotopes that do not appear in initial concentrations may be non-zero, depending on the - value of the :attr:`openmc.deplete.Operator.dilute_initial` - attribute. The :class:`openmc.deplete.Operator` class adds isotopes - according to this setting, which can be set to zero. + value of the :attr:`openmc.deplete.CoupledOperator.dilute_initial` + attribute. The :class:`openmc.deplete.CoupledOperator` class adds + isotopes according to this setting, which can be set to zero. Parameters ---------- @@ -95,9 +108,10 @@ class Results(list): Units for the returned concentration. Default is ``"atoms"`` .. versionadded:: 0.12 - time_units : {"s", "min", "h", "d"}, optional + time_units : {"s", "min", "h", "d", "a"}, optional Units for the returned time array. Default is ``"s"`` to - return the value in seconds. + return the value in seconds. Other options are minutes ``"min"``, + hours ``"h"``, days ``"d"``, and Julian years ``"a"``. .. versionadded:: 0.12 @@ -109,7 +123,7 @@ class Results(list): Concentration of specified nuclide in units of ``nuc_units`` """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "a"}) cv.check_value("nuc_units", nuc_units, {"atoms", "atom/b-cm", "atom/cm3"}) @@ -145,8 +159,8 @@ class Results(list): Initial values for some isotopes that do not appear in initial concentrations may be non-zero, depending on the - value of :class:`openmc.deplete.Operator` ``dilute_initial`` - The :class:`openmc.deplete.Operator` adds isotopes according + value of :class:`openmc.deplete.CoupledOperator` ``dilute_initial`` + The :class:`openmc.deplete.CoupledOperator` adds isotopes according to this setting, which can be set to zero. Parameters @@ -190,8 +204,10 @@ class Results(list): Parameters ---------- - time_units : {"s", "d", "h", "min"}, optional - Desired units for the times array + time_units : {"s", "d", "min", "h", "a"}, optional + Desired units for the times array. Options are seconds ``"s"``, + minutes ``"min"``, hours ``"h"``, days ``"d"``, and Julian years + ``"a"``. Returns ------- @@ -203,7 +219,7 @@ class Results(list): 1 contains the associated uncertainty """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "a"}) times = np.empty_like(self, dtype=float) eigenvalues = np.empty((len(self), 2), dtype=float) @@ -257,9 +273,10 @@ class Results(list): Parameters ---------- - time_units : {"s", "d", "h", "min"}, optional + time_units : {"s", "d", "min", "h", "a"}, optional Return the vector in these units. Default is to - convert to days + convert to days ``"d"``. Other options are seconds ``"s"``, minutes + ``"min"``, hours ``"h"``, days ``"d"``, and Julian years ``"a"``. Returns ------- @@ -267,7 +284,7 @@ class Results(list): 1-D vector of time points """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "a"}) times = np.fromiter( (r.time[0] for r in self), @@ -296,8 +313,9 @@ class Results(list): ---------- time : float Desired point in time - time_units : {"s", "d", "min", "h"}, optional - Units on ``time``. Default: days + time_units : {"s", "d", "min", "h", "a"}, optional + Units on ``time``. Default: days ``"d"``. Other options are seconds + ``"s"``, minutes ``"min"``, hours ``"h"`` and Julian years ``"a"``. atol : float, optional Absolute tolerance (in ``time_units``) if ``time`` is not found. @@ -399,7 +417,16 @@ class Results(list): mat_id = str(mat.id) if mat_id in result.mat_to_ind: mat.volume = result.volume[mat_id] + + # Change density of all nuclides in material to atom/b-cm + atoms_per_barn_cm = mat.get_nuclide_atom_densities() + for nuc, value in atoms_per_barn_cm.items(): + mat.remove_nuclide(nuc) + mat.add_nuclide(nuc, value) mat.set_density('sum') + + # For nuclides in chain that have cross sections, replace + # density in original material with new density from results for nuc in result.nuc_to_ind: if nuc not in available_cross_sections: continue diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 6e2373c45..c54f9edcf 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -462,7 +462,7 @@ class StepResult: Parameters ---------- - op : openmc.deplete.TransportOperator + 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. @@ -495,7 +495,13 @@ class StepResult: for mat_i in range(n_mat): results[i, mat_i, :] = x[i][mat_i] - results.k = [(r.k.nominal_value, r.k.std_dev) for r in op_results] + 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] results.time = t results.source_rate = source_rate diff --git a/openmc/filter.py b/openmc/filter.py index be3d97679..d7ab3f27e 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1324,6 +1324,18 @@ class EnergyFilter(RealFilter): cv.check_greater_than('filter value', v0, 0., equality=True) cv.check_greater_than('filter value', v1, 0., equality=True) + @property + def lethargy_bin_width(self): + """Calculates the base 10 log width of energy bins which is useful when + plotting the normalized flux. + + Returns + ------- + numpy.array + Array of bin widths + """ + return np.log10(self.bins[:, 1]/self.bins[:, 0]) + @classmethod def from_group_structure(cls, group_structure): """Construct an EnergyFilter instance from a standard group structure. diff --git a/openmc/material.py b/openmc/material.py index a837b1f17..6c23d3319 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -34,7 +34,9 @@ class Material(IDManagerMixin): To create a material, one should create an instance of this class, add nuclides or elements with :meth:`Material.add_nuclide` or :meth:`Material.add_element`, respectively, and set the total material - density with :meth:`Material.set_density()`. The material can then be + density with :meth:`Material.set_density()`. Alternatively, you can + use :meth:`Material.add_components()` to pass a dictionary + containing all the component information. The material can then be assigned to a cell using the :attr:`Cell.fill` attribute. Parameters @@ -89,10 +91,6 @@ class Material(IDManagerMixin): fissionable_mass : float Mass of fissionable nuclides in the material in [g]. Requires that the :attr:`volume` attribute is set. - activity : float - Activity of the material in [Bq]. Requires that the :attr:`volume` - attribute is set. - """ next_id = 1 @@ -148,11 +146,6 @@ class Material(IDManagerMixin): return string - @property - def activity(self): - """Returns the total activity of the material in Becquerels.""" - return sum(self.get_nuclide_activity().values()) - @property def name(self): return self._name @@ -403,6 +396,54 @@ class Material(IDManagerMixin): self._nuclides.append(NuclideTuple(nuclide, percent, percent_type)) + def add_components(self, components: dict, percent_type: str = 'ao'): + """ Add multiple elements or nuclides to a material + + .. versionadded:: 0.13.1 + + Parameters + ---------- + components : dict of str to float or dict + Dictionary mapping element or nuclide names to their atom or weight + percent. To specify enrichment of an element, the entry of + ``components`` for that element must instead be a dictionary + containing the keyword arguments as well as a value for + ``'percent'`` + percent_type : {'ao', 'wo'} + 'ao' for atom percent and 'wo' for weight percent + + Examples + -------- + >>> mat = openmc.Material() + >>> components = {'Li': {'percent': 1.0, + >>> 'enrichment': 60.0, + >>> 'enrichment_target': 'Li7'}, + >>> 'Fl': 1.0, + >>> 'Be6': 0.5} + >>> mat.add_components(components) + + """ + + for component, params in components.items(): + cv.check_type('component', component, str) + if isinstance(params, float): + params = {'percent': params} + + else: + cv.check_type('params', params, dict) + if 'percent' not in params: + raise ValueError("An entry in the dictionary does not have " + "a required key: 'percent'") + + params['percent_type'] = percent_type + + ## check if nuclide + if str.isdigit(component[-1]): + self.add_nuclide(component, **params) + else: # is element + kwargs = params + self.add_element(component, **params) + def remove_nuclide(self, nuclide: str): """Remove a nuclide from the material @@ -811,7 +852,7 @@ class Material(IDManagerMixin): elif self.density_units == 'atom/b-cm': density = self.density elif self.density_units == 'atom/cm3' or self.density_units == 'atom/cc': - density = 1.E-24 * self.density + density = 1.e-24 * self.density # For ease of processing split out nuc, nuc_density, # and nuc_density_type into separate arrays @@ -847,7 +888,7 @@ class Material(IDManagerMixin): # Convert the mass density to an atom density if not density_in_atom: - density = -density / self.average_molar_mass * 1.E-24 \ + density = -density / self.average_molar_mass * 1.e-24 \ * openmc.data.AVOGADRO nuc_densities = density * nuc_densities @@ -858,22 +899,46 @@ class Material(IDManagerMixin): return nuclides - def get_nuclide_activity(self): - """Return activity in [Bq] for each nuclide in the material + def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False): + """Returns the activity of the material or for each nuclide in the + material in units of [Bq], [Bq/g] or [Bq/cm3]. .. versionadded:: 0.13.1 + Parameters + ---------- + units : {'Bq', 'Bq/g', 'Bq/cm3'} + Specifies the type of activity to return, options include total + activity [Bq], specific [Bq/g] or volumetric activity [Bq/cm3]. + Default is volumetric activity [Bq/cm3]. + by_nuclide : bool + Specifies if the activity should be returned for the material as a + whole or per nuclide. Default is False. + Returns ------- - dict - Dictionary whose keys are nuclide names and values are activity in - [Bq]. + Union[dict, float] + If by_nuclide is True then a dictionary whose keys are nuclide + names and values are activity is returned. Otherwise the activity + of the material is returned as a float. """ + + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) + cv.check_type('by_nuclide', by_nuclide, bool) + + if units == 'Bq': + multiplier = self.volume + elif units == 'Bq/cm3': + multiplier = 1 + elif units == 'Bq/g': + multiplier = 1.0 / self.get_mass_density() + activity = {} - for nuclide, atoms in self.get_nuclide_atoms().items(): + for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): inv_seconds = openmc.data.decay_constant(nuclide) - activity[nuclide] = inv_seconds * atoms - return activity + activity[nuclide] = inv_seconds * 1e24 * atoms_per_bcm * multiplier + + return activity if by_nuclide else sum(activity.values()) def get_nuclide_atoms(self): """Return number of atoms of each nuclide in the material diff --git a/openmc/mesh.py b/openmc/mesh.py index 4af701892..af830158c 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from math import pi from numbers import Real, Integral +from pathlib import Path import warnings from xml.etree import ElementTree as ET @@ -1440,7 +1441,7 @@ class UnstructuredMesh(MeshBase): Parameters ---------- - filename : str + filename : str or pathlib.Path Location of the unstructured mesh file library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally @@ -1468,20 +1469,39 @@ class UnstructuredMesh(MeshBase): be generated for this mesh volumes : Iterable of float Volumes of the unstructured mesh elements + centroids : numpy.ndarray + Centroids of the mesh elements with array shape (n_elements, 3) + + vertices : numpy.ndarray + Coordinates of the mesh vertices with array shape (n_elements, 3) + + .. versionadded:: 0.13.1 + connectivity : numpy.ndarray + Connectivity of the elements with array shape (n_elements, 8) + + .. versionadded:: 0.13.1 + element_types : Iterable of integers + Mesh element types + + .. versionadded:: 0.13.1 total_volume : float Volume of the unstructured mesh in total - centroids : Iterable of tuple - An iterable of element centroid coordinates, e.g. [(0.0, 0.0, 0.0), - (1.0, 1.0, 1.0), ...] """ + + _UNSUPPORTED_ELEM = -1 + _LINEAR_TET = 0 + _LINEAR_HEX = 1 + def __init__(self, filename, library, mesh_id=None, name='', length_multiplier=1.0): super().__init__(mesh_id, name) self.filename = filename self._volumes = None - self._centroids = None + self._n_elements = None + self._conectivity = None + self._vertices = None self.library = library - self._output = True + self._output = False self.length_multiplier = length_multiplier @property @@ -1490,7 +1510,7 @@ class UnstructuredMesh(MeshBase): @filename.setter def filename(self, filename): - cv.check_type('Unstructured Mesh filename', filename, str) + cv.check_type('Unstructured Mesh filename', filename, (str, Path)) self._filename = filename @property @@ -1542,23 +1562,33 @@ class UnstructuredMesh(MeshBase): def total_volume(self): return np.sum(self.volumes) + @property + def vertices(self): + return self._vertices + + @property + def connectivity(self): + return self._connectivity + + @property + def element_types(self): + return self._element_types + @property def centroids(self): - return self._centroids + return np.array([self.centroid(i) for i in range(self.n_elements)]) @property def n_elements(self): - if self._centroids is None: + if self._n_elements is None: raise RuntimeError("No information about this mesh has " "been loaded from a statepoint file.") - return len(self._centroids) + return self._n_elements - - @centroids.setter - def centroids(self, centroids): - cv.check_type("Unstructured mesh centroids", centroids, - Iterable, Real) - self._centroids = centroids + @n_elements.setter + def n_elements(self, val): + cv.check_type('Number of elements', val, Integral) + self._n_elements = val @property def length_multiplier(self): @@ -1573,29 +1603,46 @@ class UnstructuredMesh(MeshBase): @property def dimension(self): - return self.n_elements + return (self.n_elements,) @property def n_dimension(self): return 3 - @property - def vertices(self): - raise NotImplementedError("Vertices for UnstructuredMesh objects are " - "not yet available") - def __repr__(self): string = super().__repr__() string += '{: <16}=\t{}\n'.format('\tFilename', self.filename) - string += '{: <16}=\t{}\n'.format('\tMesh Library', self.mesh_lib) + string += '{: <16}=\t{}\n'.format('\tMesh Library', self.library) if self.length_multiplier != 1.0: string += '{: <16}=\t{}\n'.format('\tLength multiplier', self.length_multiplier) return string - def write_data_to_vtk(self, filename, datasets, volume_normalization=True): - """Map data to the unstructured mesh element centroids - to create a VTK point-cloud dataset. + def centroid(self, bin): + """Return the vertex averaged centroid of an element + + Parameters + ---------- + bin : int + Bin ID for the returned centroid + + Returns + ------- + numpy.ndarray + x, y, z values of the element centroid + + """ + conn = self.connectivity[bin] + # remove invalid connectivity values + conn = conn[conn >= 0] + coords = self.vertices[conn] + return coords.mean(axis=0) + + def write_vtk_mesh(self, **kwargs): + """Map data to unstructured VTK mesh elements. + + .. deprecated:: 0.13 + Use :func:`UnstructuredMesh.write_data_to_vtk` instead. Parameters ---------- @@ -1607,83 +1654,103 @@ class UnstructuredMesh(MeshBase): volume_normalization : bool Whether or not to normalize the data by the volume of the mesh elements - - Raises - ------ - RuntimeError - when the size of a dataset doesn't match the number of cells """ + 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 + ) + self.write_data_to_vtk(**kwargs) + def write_data_to_vtk(self, filename=None, datasets=None, volume_normalization=True): + """Map data to unstructured VTK mesh elements. + + Parameters + ---------- + filename : str or pathlib.Path + Name of the VTK file to write + datasets : dict + Dictionary whose keys are the data labels + and values are numpy appropriately sized arrays + of the data + volume_normalization : bool + Whether or not to normalize the data by the + volume of the mesh elements + """ import vtk - from vtk.util import numpy_support as vtk_npsup + from vtk.util import numpy_support as nps - if self.centroids is None: - raise RuntimeError("No centroid information is present on this " - "unstructured mesh. Please load this " - "information from a relevant statepoint file.") + if self.connectivity is None or self.vertices is None: + raise RuntimeError('This mesh has not been ' + 'loaded from a statepoint file.') - if self.volumes is None and volume_normalization: - raise RuntimeError("No volume data is present on this " - "unstructured mesh. Please load the " - " mesh information from a statepoint file.") + if filename is None: + filename = f'mesh_{self.id}.vtk' - # check that the data sets are appropriately sized - errmsg = "The size of the dataset {} should be equal to the number of cells" - for label, dataset in datasets.items(): - if isinstance(dataset, np.ndarray): - if not dataset.size == self.dimension[0] * self.dimension[1]* self.dimension[2]: - raise RuntimeError(errmsg.format(label)) + writer = vtk.vtkUnstructuredGridWriter() + + writer.SetFileName(str(filename)) + + grid = vtk.vtkUnstructuredGrid() + + vtk_pnts = vtk.vtkPoints() + vtk_pnts.SetData(nps.numpy_to_vtk(self.vertices)) + grid.SetPoints(vtk_pnts) + + n_skipped = 0 + elems = [] + for elem_type, conn in zip(self.element_types, self.connectivity): + if elem_type == self._LINEAR_TET: + elem = vtk.vtkTetra() + elif elem_type == self._LINEAR_HEX: + elem = vtk.vtkHexahedron() + elif elem_type == self._UNSUPPORTED_ELEM: + n_skipped += 1 else: - if len(dataset) == self.dimension[0] * self.dimension[1]* self.dimension[2]: - raise RuntimeError(errmsg.format(label)) - cv.check_type('label', label, str) + raise RuntimeError(f'Invalid element type {elem_type} found') + for i, c in enumerate(conn): + if c == -1: + break + elem.GetPointIds().SetId(i, c) + elems.append(elem) - # create data arrays for the cells/points - cell_dim = 1 - vertices = vtk.vtkCellArray() - points = vtk.vtkPoints() + if n_skipped > 0: + warnings.warn(f'{n_skipped} elements were not written because ' + 'they are not of type linear tet/hex') - for centroid in self.centroids: - # create a point for each centroid - point_id = points.InsertNextPoint(centroid * self.length_multiplier) - # create a cell of type "Vertex" for each point - cell_id = vertices.InsertNextCell(cell_dim, (point_id,)) + for elem in elems: + grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) - # create a VTK data object - poly_data = vtk.vtkPolyData() - poly_data.SetPoints(points) - poly_data.SetVerts(vertices) - - # strange VTK nuance: - # data must be held in some container - # until the vtk file is written - data_holder = [] - - # create VTK arrays for each of - # the data sets - for label, dataset in datasets.items(): - dataset = np.asarray(dataset).flatten() + # check that datasets are the correct size + datasets_out = [] + if datasets is not None: + 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}') if volume_normalization: - dataset /= self.volumes.flatten() + for name, data in datasets.items(): + if np.issubdtype(data.dtype, np.integer): + warnings.warn(f'Integer data set "{name}" will ' + 'not be volume-normalized.') + continue + data /= self.volumes - array = vtk.vtkDoubleArray() - array.SetName(label) - array.SetNumberOfComponents(1) - array.SetArray(vtk_npsup.numpy_to_vtk(dataset), - dataset.size, - True) + # add data to the mesh + for name, data in datasets.items(): + datasets_out.append(data) + arr = vtk.vtkDoubleArray() + arr.SetName(name) + arr.SetNumberOfTuples(data.size) - data_holder.append(dataset) - poly_data.GetPointData().AddArray(array) + for i in range(data.size): + arr.SetTuple1(i, data.flat[i]) + grid.GetCellData().AddArray(arr) - # set filename - if not filename.endswith(".vtk"): - filename += ".vtk" + writer.SetInputData(grid) - writer = vtk.vtkGenericDataObjectWriter() - writer.SetFileName(filename) - writer.SetInputData(poly_data) writer.Write() @classmethod @@ -1694,10 +1761,14 @@ class UnstructuredMesh(MeshBase): mesh = cls(filename, library, mesh_id=mesh_id) vol_data = group['volumes'][()] - centroids = group['centroids'][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) - mesh.centroids = np.reshape(centroids, (vol_data.shape[0], 3)) - mesh.size = mesh.volumes.size + mesh.n_elements = mesh.volumes.size + + vertices = group['vertices'][()] + mesh._vertices = vertices.reshape((-1, 3)) + connectivity = group['connectivity'][()] + mesh._connectivity = connectivity.reshape((-1, 8)) + mesh._element_types = group['element_types'][()] if 'length_multiplier' in group: mesh.length_multiplier = group['length_multiplier'][()] @@ -1719,7 +1790,7 @@ class UnstructuredMesh(MeshBase): element.set("type", "unstructured") element.set("library", self._library) subelement = ET.SubElement(element, "filename") - subelement.text = self.filename + subelement.text = str(self.filename) if self._length_multiplier != 1.0: element.set("length_multiplier", str(self.length_multiplier)) diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 453c1d099..f9798c24a 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -20,6 +20,7 @@ MGXS_TYPES = ( 'transport', 'nu-transport', 'absorption', + 'reduced absorption', 'capture', 'fission', 'nu-fission', @@ -147,6 +148,11 @@ def _df_column_convert_to_bin(df, current_name, new_name, values_to_bin, df.rename(columns={current_name: new_name}, inplace=True) +def add_params(cls): + cls.__doc__ += cls._params + return cls + +@add_params class MGXS: """An abstract multi-group cross section for some energy group structure within some spatial domain. @@ -157,6 +163,9 @@ class MGXS: .. note:: Users should instantiate the subclasses of this abstract class. + """ + + _params = """ Parameters ---------- domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh @@ -764,6 +773,8 @@ class MGXS: mgxs = TransportXS(domain, domain_type, energy_groups, nu=True) elif mgxs_type == 'absorption': mgxs = AbsorptionXS(domain, domain_type, energy_groups) + elif mgxs_type == 'reduced absorption': + mgxs = ReducedAbsorptionXS(domain, domain_type, energy_groups) elif mgxs_type == 'capture': mgxs = CaptureXS(domain, domain_type, energy_groups) elif mgxs_type == 'fission': @@ -816,6 +827,8 @@ class MGXS: elif mgxs_type in ARBITRARY_MATRIX_TYPES: mgxs = ArbitraryMatrixXS(mgxs_type, domain, domain_type, energy_groups) + else: + raise ValueError(f"Unknown MGXS type: {mgxs_type}") mgxs.by_nuclide = by_nuclide mgxs.name = name @@ -2148,6 +2161,7 @@ class MGXS: return 'cm^-1' if xs_type == 'macro' else 'barns' +@add_params class MatrixMGXS(MGXS): """An abstract multi-group cross section for some energy group structure within some spatial domain. This class is specifically intended for @@ -2161,92 +2175,6 @@ class MatrixMGXS(MGXS): .. note:: Users should instantiate the subclasses of this abstract class. - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file) and the number of mesh cells for - 'mesh' domain types. - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ @property def _dont_squeeze(self): @@ -2631,6 +2559,7 @@ class MatrixMGXS(MGXS): print(string) +@add_params class TotalXS(MGXS): r"""A total multi-group cross section. @@ -2657,98 +2586,11 @@ class TotalXS(MGXS): \sigma_t (r, E) \psi (r, E, \Omega)}{\int_{r \in V} dr \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; \psi (r, E, \Omega)}. - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`TotalXS.tally_keys` property and values - are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'total' @@ -2887,9 +2729,9 @@ class TransportXS(MGXS): """ - def __init__(self, domain=None, domain_type=None, groups=None, nu=False, + def __init__(self, domain=None, domain_type=None, energy_groups=None, nu=False, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) # Use tracklength estimators for the total MGXS term, and @@ -3128,9 +2970,9 @@ class DiffusionCoefficient(TransportXS): """ - def __init__(self, domain=None, domain_type=None, groups=None, nu=False, + def __init__(self, domain=None, domain_type=None, energy_groups=None, nu=False, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super(DiffusionCoefficient, self).__init__(domain, domain_type, groups, + super(DiffusionCoefficient, self).__init__(domain, domain_type, energy_groups, nu, by_nuclide, name, num_polar, num_azimuthal) if not nu: @@ -3194,6 +3036,7 @@ class DiffusionCoefficient(TransportXS): return self._xs_tally +@add_params class AbsorptionXS(MGXS): r"""An absorption multi-group cross section. @@ -3224,103 +3067,72 @@ class AbsorptionXS(MGXS): \sigma_a (r, E) \psi (r, E, \Omega)}{\int_{r \in V} dr \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; \psi (r, E, \Omega)}. - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`AbsorptionXS.tally_keys` property and - values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file) and the number of mesh cells for - 'mesh' domain types. - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'absorption' +@add_params +class ReducedAbsorptionXS(MGXS): + r"""A reduced absorption multi-group cross section. + + The reduced absorption reaction rate is defined as the difference between + absorption and the production of neutrons due to (n,xn) reactions. + + This class can be used for both OpenMC input generation and tally data + post-processing to compute spatially-homogenized and energy-integrated + multi-group capture cross sections for multi-group neutronics calculations. + At a minimum, one needs to set the :attr:`CaptureXS.energy_groups` and + :attr:`CaptureXS.domain` properties. Tallies for the flux and appropriate + reaction rates over the specified domain are generated automatically via the + :attr:`CaptureXS.tallies` property, which can then be appended to a + :class:`openmc.Tallies` instance. + + For post-processing, the :meth:`MGXS.load_from_statepoint` will pull in the + necessary data to compute multi-group cross sections from a + :class:`openmc.StatePoint` instance. The derived multi-group cross section + can then be obtained from the :attr:`CaptureXS.xs_tally` property. + + For a spatial domain :math:`V` and energy group :math:`[E_g,E_{g-1}]`, the + reduced absorption cross section is calculated as: + + .. math:: + + \frac{\int_{r \in V} dr \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; + \left(\sigma_a (r, E) - \sigma_{n,2n}(r,E) - 2\sigma_{n,3n}(r,E) - + 3\sigma_{n,4n}(r,E) \right) \psi (r, E, \Omega)}{\int_{r \in V} dr + \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; \psi (r, E, \Omega)}. + + """ + + def __init__(self, domain=None, domain_type=None, energy_groups=None, + by_nuclide=False, name='', num_polar=1, num_azimuthal=1): + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, + num_polar, num_azimuthal) + self._rxn_type = 'reduced absorption' + + @property + def scores(self): + return ['flux', 'absorption', '(n,2n)', '(n,3n)', '(n,4n)'] + + @property + def rxn_rate_tally(self): + if self._rxn_rate_tally is None: + self._rxn_rate_tally = ( + self.tallies['absorption'] + - self.tallies['(n,2n)'] + - 2*self.tallies['(n,3n)'] + - 3*self.tallies['(n,4n)'] + ) + self._rxn_rate_tally.sparse = self.sparse + return self._rxn_rate_tally + + +@add_params class CaptureXS(MGXS): r"""A capture multi-group cross section. @@ -3354,98 +3166,11 @@ class CaptureXS(MGXS): \Omega) \right ]}{\int_{r \in V} dr \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; \psi (r, E, \Omega)}. - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`CaptureXS.tally_keys` property and - values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'capture' @@ -3599,10 +3324,10 @@ class FissionXS(MGXS): """ - def __init__(self, domain=None, domain_type=None, groups=None, nu=False, + def __init__(self, domain=None, domain_type=None, energy_groups=None, nu=False, prompt=False, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._nu = False self._prompt = False @@ -3648,6 +3373,7 @@ class FissionXS(MGXS): self._rxn_type = 'prompt-nu-fission' +@add_params class KappaFissionXS(MGXS): r"""A recoverable fission energy production rate multi-group cross section. @@ -3681,98 +3407,11 @@ class KappaFissionXS(MGXS): \kappa\sigma_f (r, E) \psi (r, E, \Omega)}{\int_{r \in V} dr \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; \psi (r, E, \Omega)}. - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`KappaFissionXS.tally_keys` property and - values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'kappa-fission' @@ -3904,10 +3543,10 @@ class ScatterXS(MGXS): """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1, nu=False): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self.nu = nu @@ -3932,6 +3571,7 @@ class ScatterXS(MGXS): self._valid_estimators = ['analog'] +@add_params class ArbitraryXS(MGXS): r"""A multi-group cross section for an arbitrary reaction type. @@ -3960,105 +3600,17 @@ class ArbitraryXS(MGXS): where :math:`\sigma_X` is the requested reaction type of interest. - Parameters - ---------- - rxn_type : str - Reaction type (e.g., '(n,2n)', '(n,Xt)', etc.) - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., '(n,2n)', '(n,Xt)', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`TotalXS.tally_keys` property and values - are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ - def __init__(self, rxn_type, domain=None, domain_type=None, groups=None, + def __init__(self, rxn_type, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): cv.check_value("rxn_type", rxn_type, ARBITRARY_VECTOR_TYPES) - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = rxn_type +@add_params class ArbitraryMatrixXS(MatrixMGXS): r"""A multi-group matrix cross section for an arbitrary reaction type. @@ -4094,103 +3646,13 @@ class ArbitraryMatrixXS(MatrixMGXS): where :math:`\sigma_X` is the requested reaction type of interest. - Parameters - ---------- - rxn_type : str - Reaction type (e.g., '(n,2n)', '(n,nta)', etc.). Valid names have - neutrons as a product. - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : 'analog' - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`NuFissionMatrixXS.tally_keys` - property and values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ - def __init__(self, rxn_type, domain=None, domain_type=None, groups=None, + def __init__(self, rxn_type, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): cv.check_value("rxn_type", rxn_type, ARBITRARY_MATRIX_TYPES) - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = rxn_type.split(" ")[0] self._estimator = 'analog' @@ -4391,10 +3853,10 @@ class ScatterMatrixXS(MatrixMGXS): """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1, nu=False): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._formulation = 'simple' self._correction = 'P0' @@ -5352,6 +4814,7 @@ class ScatterMatrixXS(MatrixMGXS): print(string) +@add_params class MultiplicityMatrixXS(MatrixMGXS): r"""The scattering multiplicity matrix. @@ -5392,93 +4855,6 @@ class MultiplicityMatrixXS(MatrixMGXS): where :math:`\upsilon_i` is the multiplicity for the :math:`i`-th reaction. - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : 'analog' - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`MultiplicityMatrixXS.tally_keys` - property and values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ # Store whether or not the number density should be removed for microscopic @@ -5487,9 +4863,9 @@ class MultiplicityMatrixXS(MatrixMGXS): # for microscopic data _divide_by_density = False - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'multiplicity matrix' self._estimator = 'analog' @@ -5530,6 +4906,7 @@ class MultiplicityMatrixXS(MatrixMGXS): return self._xs_tally +@add_params class ScatterProbabilityMatrix(MatrixMGXS): r"""The group-to-group scattering probability matrix. @@ -5568,93 +4945,6 @@ class ScatterProbabilityMatrix(MatrixMGXS): \sigma_{s,g'} \phi \rangle} \end{aligned} - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : 'analog' - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`ScatterProbabilityMatrix.tally_keys` - property and values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file). - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ # Store whether or not the number density should be removed for microscopic @@ -5662,9 +4952,9 @@ class ScatterProbabilityMatrix(MatrixMGXS): # to 1.0, this density division is not necessary _divide_by_density = False - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'scatter' self._mgxs_type = 'scatter probability matrix' @@ -5833,10 +5123,10 @@ class NuFissionMatrixXS(MatrixMGXS): """ - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1, prompt=False): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) if not prompt: self._rxn_type = 'nu-fission' @@ -5999,10 +5289,10 @@ class Chi(MGXS): # data should not be divided by the number density _divide_by_density = False - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, prompt=False, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._estimator = 'analog' self._valid_estimators = ['analog'] @@ -6401,6 +5691,7 @@ class Chi(MGXS): return '%' +@add_params class InverseVelocity(MGXS): r"""An inverse velocity multi-group cross section. @@ -6430,94 +5721,6 @@ class InverseVelocity(MGXS): \frac{\psi (r, E, \Omega)}{v (r, E)}}{\int_{r \in V} dr \int_{4\pi} d\Omega \int_{E_g}^{E_{g-1}} dE \; \psi (r, E, \Omega)} - Parameters - ---------- - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - The domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - The domain type for spatial homogenization - groups : openmc.mgxs.EnergyGroups - The energy group structure for energy condensation - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - name : str, optional - Name of the multi-group cross section. Used as a label to identify - tallies in OpenMC 'tallies.xml' file. - num_polar : Integral, optional - Number of equi-width polar angle bins for angle discretization; - defaults to one bin - num_azimuthal : Integral, optional - Number of equi-width azimuthal angle bins for angle discretization; - defaults to one bin - - Attributes - ---------- - name : str, optional - Name of the multi-group cross section - rxn_type : str - Reaction type (e.g., 'total', 'nu-fission', etc.) - by_nuclide : bool - If true, computes cross sections for each nuclide in domain - domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain for spatial homogenization - domain_type : {'material', 'cell', 'distribcell', 'universe', 'mesh'} - Domain type for spatial homogenization - energy_groups : openmc.mgxs.EnergyGroups - Energy group structure for energy condensation - num_polar : Integral - Number of equi-width polar angle bins for angle discretization - num_azimuthal : Integral - Number of equi-width azimuthal angle bins for angle discretization - tally_trigger : openmc.Trigger - An (optional) tally precision trigger given to each tally used to - compute the cross section - scores : list of str - The scores in each tally used to compute the multi-group cross section - filters : list of openmc.Filter - The filters in each tally used to compute the multi-group cross section - tally_keys : list of str - The keys into the tallies dictionary for each tally used to compute - the multi-group cross section - estimator : {'tracklength', 'collision', 'analog'} - The tally estimator used to compute the multi-group cross section - tallies : collections.OrderedDict - OpenMC tallies needed to compute the multi-group cross section. The keys - are strings listed in the :attr:`InverseVelocity.tally_keys` property - and values are instances of :class:`openmc.Tally`. - rxn_rate_tally : openmc.Tally - Derived tally for the reaction rate tally used in the numerator to - compute the multi-group cross section. This attribute is None - unless the multi-group cross section has been computed. - xs_tally : openmc.Tally - Derived tally for the multi-group cross section. This attribute - is None unless the multi-group cross section has been computed. - num_subdomains : int - The number of subdomains is unity for 'material', 'cell' and 'universe' - domain types. This is equal to the number of cell instances - for 'distribcell' domain types (it is equal to unity prior to loading - tally data from a statepoint file) and the number of mesh cells for - 'mesh' domain types. - num_nuclides : int - The number of nuclides for which the multi-group cross section is - being tracked. This is unity if the by_nuclide attribute is False. - nuclides : Iterable of str or 'sum' - The optional user-specified nuclides for which to compute cross - sections (e.g., 'U238', 'O16'). If by_nuclide is True but nuclides - are not specified by the user, all nuclides in the spatial domain - are included. This attribute is 'sum' if by_nuclide is false. - sparse : bool - Whether or not the MGXS' tallies use SciPy's LIL sparse matrix format - for compressed data storage - loaded_sp : bool - Whether or not a statepoint file has been loaded with tally data - derived : bool - Whether or not the MGXS is merged from one or more other MGXS - mgxs_type : str - The name of this MGXS type, to be used when printing and - indexing in an HDF5 data store - - .. versionadded:: 0.13.1 - """ # Store whether or not the number density should be removed for microscopic @@ -6526,9 +5729,9 @@ class InverseVelocity(MGXS): # values _divide_by_density = False - def __init__(self, domain=None, domain_type=None, groups=None, + def __init__(self, domain=None, domain_type=None, energy_groups=None, by_nuclide=False, name='', num_polar=1, num_azimuthal=1): - super().__init__(domain, domain_type, groups, by_nuclide, name, + super().__init__(domain, domain_type, energy_groups, by_nuclide, name, num_polar, num_azimuthal) self._rxn_type = 'inverse-velocity' @@ -7031,7 +6234,7 @@ class Current(MeshSurfaceMGXS): """ def __init__(self, domain=None, domain_type=None, - groups=None, by_nuclide=False, name=''): + energy_groups=None, by_nuclide=False, name=''): super(Current, self).__init__(domain, domain_type, - groups, by_nuclide, name) + energy_groups, by_nuclide, name) self._rxn_type = 'current' diff --git a/openmc/mixin.py b/openmc/mixin.py index 516162464..31c26ec76 100644 --- a/openmc/mixin.py +++ b/openmc/mixin.py @@ -14,8 +14,11 @@ class EqualityMixin: def __eq__(self, other): if isinstance(other, type(self)): for key, value in self.__dict__.items(): - if not np.array_equal(value, other.__dict__.get(key)): - return False + if isinstance(value, np.ndarray): + if not np.array_equal(value, other.__dict__.get(key)): + return False + else: + return value == other.__dict__.get(key) else: return False diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 6271154fb..51019b0d8 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -451,10 +451,10 @@ class RectangularParallelepiped(CompositeSurface): self.zmax = openmc.ZPlane(z0=zmax, **kwargs) def __neg__(self): - return +self.xmin & -self.xmax & +self.ymin & -self.ymax & +self.zmin & -self.zmax + return -self.xmax & +self.xmin & -self.ymax & +self.ymin & -self.zmax & +self.zmin def __pos__(self): - return -self.xmin | +self.xmax | -self.ymin | +self.ymax | -self.zmin | +self.zmax + return +self.xmax | -self.xmin | +self.ymax | -self.ymin | +self.zmax | -self.zmin class XConeOneSided(CompositeSurface): diff --git a/openmc/region.py b/openmc/region.py index 56926187d..4e74a08ad 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -259,13 +259,16 @@ class Region(ABC): clone[:] = [n.clone(memo) for n in self] return clone - def translate(self, vector, memo=None): + def translate(self, vector, inplace=False, memo=None): """Translate region in given direction Parameters ---------- vector : iterable of float Direction in which region should be translated + inplace : bool + Whether or not to return a region based on new surfaces or one based + on the original surfaces that have been modified. memo : dict or None Dictionary used for memoization. This parameter is used internally and should not be specified by the user. @@ -279,7 +282,7 @@ class Region(ABC): if memo is None: memo = {} - return type(self)(n.translate(vector, memo) for n in self) + return type(self)(n.translate(vector, inplace, memo) for n in self) def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, memo=None): @@ -308,7 +311,7 @@ class Region(ABC): :math:`\psi` about z. This corresponds to an x-y-z extrinsic rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`. - inplace : boolean + inplace : bool Whether or not to return a new instance of Surface or to modify the coefficients of this Surface in place. Defaults to False. memo : dict or None @@ -622,10 +625,10 @@ class Complement(Region): clone.node = self.node.clone(memo) return clone - def translate(self, vector, memo=None): + def translate(self, vector, inplace=False, memo=None): if memo is None: memo = {} - return type(self)(self.node.translate(vector, memo)) + return type(self)(self.node.translate(vector, inplace, memo)) def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, memo=None): diff --git a/openmc/settings.py b/openmc/settings.py index 47c61e6ae..4372e679f 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -29,6 +29,11 @@ _RES_SCAT_METHODS = ['dbrc', 'rvs'] class Settings: """Settings used for an OpenMC simulation. + Parameters + ---------- + **kwargs : dict, optional + Any keyword arguments are used to set attributes on the instance. + Attributes ---------- batches : int @@ -222,7 +227,7 @@ class Settings: Indicate whether to write the initial source distribution to file """ - def __init__(self): + def __init__(self, **kwargs): self._run_mode = RunMode.EIGENVALUE self._batches = None self._generations_per_batch = None @@ -297,6 +302,9 @@ class Settings: self._max_splits = None self._max_tracks = None + for key, value in kwargs.items(): + setattr(self, key, value) + @property def run_mode(self) -> str: return self._run_mode.value diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 630675816..138c0c718 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -11,7 +11,7 @@ from uncertainties import ufloat import openmc import openmc.checkvalue as cv -_VERSION_STATEPOINT = 17 +_VERSION_STATEPOINT = 18 class StatePoint: diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 5528bdb30..0c3052625 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable +from copy import deepcopy from numbers import Real from xml.etree import ElementTree as ET @@ -78,6 +79,18 @@ class Univariate(EqualityMixin, ABC): """ pass + def integral(self): + """Return integral of distribution + + .. versionadded:: 0.13.1 + + Returns + ------- + float + Integral of distribution + """ + return 1.0 + class Discrete(Univariate): """Distribution characterized by a probability mass function. @@ -95,9 +108,9 @@ class Discrete(Univariate): Attributes ---------- - x : Iterable of float + x : numpy.ndarray Values of the random variable - p : Iterable of float + p : numpy.ndarray Discrete probability for each value """ @@ -122,7 +135,7 @@ class Discrete(Univariate): if isinstance(x, Real): x = [x] cv.check_type('discrete values', x, Iterable, Real) - self._x = x + self._x = np.array(x, dtype=float) @p.setter def p(self, p): @@ -131,14 +144,15 @@ class Discrete(Univariate): cv.check_type('discrete probabilities', p, Iterable, Real) for pk in p: cv.check_greater_than('discrete probability', pk, 0.0, True) - self._p = p + self._p = np.array(p, dtype=float) def cdf(self): return np.insert(np.cumsum(self.p), 0, 0.0) def sample(self, n_samples=1, seed=None): np.random.seed(seed) - return np.random.choice(self.x, n_samples, p=self.p) + p = self.p / self.p.sum() + return np.random.choice(self.x, n_samples, p=p) def normalize(self): """Normalize the probabilities stored on the distribution""" @@ -220,6 +234,18 @@ class Discrete(Univariate): p_arr = np.array([p_merged[x] for x in x_arr]) return cls(x_arr, p_arr) + def integral(self): + """Return integral of distribution + + .. versionadded:: 0.13.1 + + Returns + ------- + float + Integral of discrete distribution + """ + return np.sum(self.p) + class Uniform(Univariate): """Distribution with constant probability over a finite interval [a,b] @@ -825,9 +851,9 @@ class Tabular(Univariate): Attributes ---------- - x : Iterable of float + x : numpy.ndarray Tabulated values of the random variable - p : Iterable of float + p : numpy.ndarray Tabulated probabilities interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional Indicate whether the density function is constant between tabulated @@ -860,7 +886,7 @@ class Tabular(Univariate): @x.setter def x(self, x): cv.check_type('tabulated values', x, Iterable, Real) - self._x = x + self._x = np.array(x, dtype=float) @p.setter def p(self, p): @@ -868,7 +894,7 @@ class Tabular(Univariate): if not self._ignore_negative: for pk in p: cv.check_greater_than('tabulated probability', pk, 0.0, True) - self._p = p + self._p = np.array(p, dtype=float) @interpolation.setter def interpolation(self, interpolation): @@ -881,8 +907,8 @@ class Tabular(Univariate): 'distributions using histogram or ' 'linear-linear interpolation') c = np.zeros_like(self.x) - x = np.asarray(self.x) - p = np.asarray(self.p) + x = self.x + p = self.p if self.interpolation == 'histogram': c[1:] = p[:-1] * np.diff(x) @@ -922,7 +948,7 @@ class Tabular(Univariate): def normalize(self): """Normalize the probabilities stored on the distribution""" - self.p = np.asarray(self.p) / self.cdf().max() + self.p /= self.cdf().max() def sample(self, n_samples=1, seed=None): if not self.interpolation in ('histogram', 'linear-linear'): @@ -931,10 +957,11 @@ class Tabular(Univariate): 'linear-linear interpolation') np.random.seed(seed) xi = np.random.rand(n_samples) - cdf = self.cdf() - cdf /= cdf.max() + # always use normalized probabilities when sampling + cdf = self.cdf() p = self.p / cdf.max() + cdf /= cdf.max() # get CDF bins that are above the # sampled values @@ -949,7 +976,7 @@ class Tabular(Univariate): # the random number is less than the next cdf # entry x_i = self.x[cdf_idx] - p_i = self.p[cdf_idx] + p_i = p[cdf_idx] if self.interpolation == 'histogram': # mask where probability is greater than zero @@ -967,7 +994,7 @@ class Tabular(Univariate): # get variable and probability values for the # next entry x_i1 = self.x[cdf_idx + 1] - p_i1 = self.p[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] # compute slope between entries m = (p_i1 - p_i) / (x_i1 - x_i) # set values for zero slope @@ -1027,6 +1054,24 @@ class Tabular(Univariate): p = params[len(params)//2:] return cls(x, p, interpolation) + def integral(self): + """Return integral of distribution + + .. versionadded: 0.13.1 + + Returns + ------- + float + Integral of tabular distrbution + """ + if self.interpolation == 'histogram': + return np.sum(np.diff(self.x) * self.p[:-1]) + elif self.interpolation == 'linear-linear': + return np.trapz(self.p, self.x) + else: + raise NotImplementedError( + f'integral() not supported for {self.inteprolation} interpolation') + class Legendre(Univariate): r"""Probability density given by a Legendre polynomial expansion @@ -1135,9 +1180,18 @@ class Mixture(Univariate): def sample(self, n_samples=1, seed=None): np.random.seed(seed) - idx = np.random.choice(self.distribution, n_samples, p=self.probability) - out = np.zeros_like(idx) + # Get probability of each distribution accounting for its intensity + p = np.array([prob*dist.integral() for prob, dist in + zip(self.probability, self.distribution)]) + p /= p.sum() + + # Sample from the distributions + idx = np.random.choice(range(len(self.distribution)), + n_samples, p=p) + + # Draw samples from the distributions sampled above + out = np.empty_like(idx, dtype=float) for i in np.unique(idx): n_dist_samples = np.count_nonzero(idx == i) samples = self.distribution[i].sample(n_dist_samples) @@ -1199,3 +1253,68 @@ class Mixture(Univariate): distribution.append(Univariate.from_xml_element(pair.find("dist"))) return cls(probability, distribution) + + def integral(self): + """Return integral of the distribution + + .. versionadded:: 0.13.1 + + Returns + ------- + float + Integral of the distribution + """ + return sum([ + p*dist.integral() + for p, dist in zip(self.probability, self.distribution) + ]) + + +def combine_distributions(dists, probs): + """Combine distributions with specified probabilities + + This function can be used to combine multiple instances of + :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular`. Multiple + discrete distributions are merged into a single distribution and the + remainder of the distributions are put into a :class:`~openmc.stats.Mixture` + distribution. + + .. versionadded:: 0.13.1 + + Parameters + ---------- + dists : iterable of openmc.stats.Univariate + Distributions to combine + probs : iterable of float + Probability (or intensity) of each distribution + + """ + # Get copy of distribution list so as not to modify the argument + dist_list = deepcopy(dists) + + # Get list of discrete/continuous distribution indices + discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] + cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)] + + # Apply probabilites to continuous distributions + for i in cont_index: + dist = dist_list[i] + dist.p *= probs[i] + + if discrete_index: + # Create combined discrete distribution + dist_discrete = [dist_list[i] for i in discrete_index] + discrete_probs = [probs[i] for i in discrete_index] + combined_dist = Discrete.merge(dist_discrete, discrete_probs) + + # Replace multiple discrete distributions with merged + for idx in reversed(discrete_index): + dist_list.pop(idx) + dist_list.append(combined_dist) + + # Combine discrete and continuous if present + if len(dist_list) > 1: + probs = [1.0]*len(dist_list) + dist_list[:] = [Mixture(probs, dist_list.copy())] + + return dist_list[0] diff --git a/openmc/surface.py b/openmc/surface.py index b3bcebb11..2996897a4 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -336,9 +336,9 @@ class Surface(IDManagerMixin, ABC): ---------- vector : iterable of float Direction in which surface should be translated - inplace : boolean + inplace : bool Whether or not to return a new instance of this Surface or to - modify the coefficients of this Surface. Defaults to False + modify the coefficients of this Surface. Returns ------- @@ -374,7 +374,7 @@ class Surface(IDManagerMixin, ABC): :math:`\psi` about z. This corresponds to an x-y-z extrinsic rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`. - inplace : boolean + inplace : bool Whether or not to return a new instance of Surface or to modify the coefficients of this Surface in place. Defaults to False. @@ -568,9 +568,9 @@ class PlaneMixin: ---------- vector : iterable of float Direction in which surface should be translated - inplace : boolean + inplace : bool Whether or not to return a new instance of a Plane or to modify the - coefficients of this plane. Defaults to False + coefficients of this plane. Returns ------- @@ -1012,9 +1012,8 @@ class QuadricMixin: ---------- vector : iterable of float Direction in which surface should be translated - inplace : boolean + inplace : bool Whether to return a clone of the Surface or the Surface itself. - Defaults to False Returns ------- @@ -2563,7 +2562,7 @@ class Halfspace(Region): clone.surface = self.surface.clone(memo) return clone - def translate(self, vector, memo=None): + def translate(self, vector, inplace=False, memo=None): """Translate half-space in given direction Parameters @@ -2585,7 +2584,7 @@ class Halfspace(Region): # If translated surface not in memo, add it key = (self.surface, tuple(vector)) if key not in memo: - memo[key] = self.surface.translate(vector) + memo[key] = self.surface.translate(vector, inplace) # Return translated half-space return type(self)(memo[key], self.side) @@ -2617,7 +2616,7 @@ class Halfspace(Region): :math:`\psi` about z. This corresponds to an x-y-z extrinsic rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`. - inplace : boolean + inplace : bool Whether or not to return a new instance of Surface or to modify the coefficients of this Surface in place. Defaults to False. memo : dict or None diff --git a/openmc/universe.py b/openmc/universe.py index 78f75bbfc..4aa1775fb 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from collections import OrderedDict from collections.abc import Iterable from copy import deepcopy -from numbers import Real +from numbers import Integral, Real from pathlib import Path from tempfile import TemporaryDirectory from xml.etree import ElementTree as ET @@ -334,6 +334,13 @@ class Universe(UniverseBase): if seed is not None: model.settings.seed = seed + # Determine whether any materials contains macroscopic data and if + # so, set energy mode accordingly + for mat in self.get_all_materials().values(): + if mat._macroscopic is not None: + model.settings.energy_mode = 'multi-group' + break + # Create plot object matching passed arguments plot = openmc.Plot() plot.origin = origin @@ -671,7 +678,7 @@ class DAGMCUniverse(UniverseBase): @filename.setter def filename(self, val): - cv.check_type('DAGMC filename', val, str) + cv.check_type('DAGMC filename', val, (Path, str)) self._filename = val @property @@ -713,10 +720,10 @@ class DAGMCUniverse(UniverseBase): dagmc_element.set('auto_geom_ids', 'true') if self.auto_mat_ids: dagmc_element.set('auto_mat_ids', 'true') - dagmc_element.set('filename', self.filename) + dagmc_element.set('filename', str(self.filename)) xml_element.append(dagmc_element) - def bounding_region(self, bounded_type='box', boundary_type='vacuum'): + def bounding_region(self, bounded_type='box', boundary_type='vacuum', starting_id=10000): """Creates a either a spherical or box shaped bounding region around the DAGMC geometry. Parameters @@ -729,6 +736,10 @@ class DAGMCUniverse(UniverseBase): Boundary condition that defines the behavior for particles hitting the surface. Defaults to vacuum boundary condition. Passed into the surface construction. + starting_id : int + Starting ID of the surface(s) used in the region. For bounded_type + 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce + the chance of an overlap of surface IDs with the DAGMC geometry. Returns ------- openmc.Region @@ -737,19 +748,20 @@ class DAGMCUniverse(UniverseBase): check_type('boundary type', boundary_type, str) check_value('boundary type', boundary_type, _BOUNDARY_TYPES) + check_type('starting surface id', starting_id, Integral) check_type('bounded type', bounded_type, str) check_value('bounded type', bounded_type, ('box', 'sphere')) - bounding_box = self.bounding_box + bbox = self.bounding_box if bounded_type == 'sphere': - import math - bounding_box_center = (bounding_box[0] + bounding_box[1])/2 - radius = math.dist(bounding_box[0], bounding_box[1]) + bbox_center = (bbox[0] + bbox[1])/2 + radius = np.linalg.norm(np.asarray(bbox)) bounding_surface = openmc.Sphere( - x0=bounding_box_center[0], - y0=bounding_box_center[1], - z0=bounding_box_center[2], + surface_id=starting_id, + x0=bbox_center[0], + y0=bbox_center[1], + z0=bbox_center[2], boundary_type=boundary_type, r=radius, ) @@ -758,14 +770,19 @@ class DAGMCUniverse(UniverseBase): if bounded_type == 'box': # defines plane surfaces for all six faces of the bounding box - lower_x = openmc.XPlane(bounding_box[0][0], boundary_type=boundary_type) - upper_x = openmc.XPlane(bounding_box[1][0], boundary_type=boundary_type) - lower_y = openmc.YPlane(bounding_box[0][1], boundary_type=boundary_type) - upper_y = openmc.YPlane(bounding_box[1][1], boundary_type=boundary_type) - lower_z = openmc.ZPlane(bounding_box[0][2], boundary_type=boundary_type) - upper_z = openmc.ZPlane(bounding_box[1][2], boundary_type=boundary_type) + lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) + upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) + lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) + upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) + lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) + upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) - return +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z + region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z + + for surface in region.get_surfaces().values(): + surface.boundary_type = boundary_type + + return region def bounded_universe(self, bounding_cell_id=10000, **kwargs): """Returns an openmc.Universe filled with this DAGMCUniverse and bounded @@ -776,7 +793,7 @@ class DAGMCUniverse(UniverseBase): Parameters ---------- bounding_cell_id : int - The cell ID number to use for the bounding cell, defaults to 1000 to reduce + The cell ID number to use for the bounding cell, defaults to 10000 to reduce the chance of overlapping ID numbers with the DAGMC geometry. Returns @@ -784,8 +801,7 @@ class DAGMCUniverse(UniverseBase): openmc.Universe Universe instance """ - - bounding_cell = openmc.Cell(fill=self, cell_id =bounding_cell_id, region=self.bounding_region(**kwargs)) + bounding_cell = openmc.Cell(fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) return openmc.Universe(cells=[bounding_cell]) @classmethod diff --git a/src/dagmc.cpp b/src/dagmc.cpp index dc3bc2b2c..0e04869fd 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -745,7 +745,7 @@ namespace openmc { void read_dagmc_universes(pugi::xml_node node) { if (check_for_node(node, "dagmc_universe")) { - fatal_error("DAGMC Universes are present but OpenMC was not configured" + fatal_error("DAGMC Universes are present but OpenMC was not configured " "with DAGMC"); } }; diff --git a/src/endf.cpp b/src/endf.cpp index b42c8641d..c0c1d2e7e 100644 --- a/src/endf.cpp +++ b/src/endf.cpp @@ -90,23 +90,25 @@ bool is_inelastic_scatter(int mt) unique_ptr read_function(hid_t group, const char* name) { - hid_t dset = open_dataset(group, name); + hid_t obj_id = open_object(group, name); std::string func_type; - read_attribute(dset, "type", func_type); + read_attribute(obj_id, "type", func_type); unique_ptr func; if (func_type == "Tabulated1D") { - func = make_unique(dset); + func = make_unique(obj_id); } else if (func_type == "Polynomial") { - func = make_unique(dset); + func = make_unique(obj_id); } else if (func_type == "CoherentElastic") { - func = make_unique(dset); + func = make_unique(obj_id); } else if (func_type == "IncoherentElastic") { - func = make_unique(dset); + func = make_unique(obj_id); + } else if (func_type == "Sum") { + func = make_unique(obj_id); } else { throw std::runtime_error {"Unknown function type " + func_type + - " for dataset " + object_name(dset)}; + " for dataset " + object_name(obj_id)}; } - close_dataset(dset); + close_object(obj_id); return func; } @@ -271,4 +273,30 @@ double IncoherentElasticXS::operator()(double E) const return bound_xs_ / 2.0 * ((1 - std::exp(-4.0 * E * W)) / (2.0 * E * W)); } +//============================================================================== +// Sum1D implementation +//============================================================================== + +Sum1D::Sum1D(hid_t group) +{ + // Get number of functions + int n; + read_attribute(group, "n", n); + + // Get each function + for (int i = 0; i < n; ++i) { + auto dset_name = fmt::format("func_{}", i + 1); + functions_.push_back(read_function(group, dset_name.c_str())); + } +} + +double Sum1D::operator()(double x) const +{ + double result = 0.0; + for (auto& func : functions_) { + result += (*func)(x); + } + return result; +} + } // namespace openmc diff --git a/src/hdf5_interface.cpp b/src/hdf5_interface.cpp index d53ad3879..27b750b93 100644 --- a/src/hdf5_interface.cpp +++ b/src/hdf5_interface.cpp @@ -118,6 +118,12 @@ void close_group(hid_t group_id) fatal_error("Failed to close group"); } +void close_object(hid_t obj_id) +{ + if (H5Oclose(obj_id) < 0) + fatal_error("Failed to close object"); +} + int dataset_ndims(hid_t dset) { hid_t dspace = H5Dget_space(dset); @@ -394,6 +400,12 @@ hid_t open_group(hid_t group_id, const char* name) return H5Gopen(group_id, name, H5P_DEFAULT); } +hid_t open_object(hid_t group_id, const std::string& name) +{ + ensure_exists(group_id, name.c_str()); + return H5Oopen(group_id, name.c_str(), H5P_DEFAULT); +} + void read_attr(hid_t obj_id, const char* name, hid_t mem_type_id, void* buffer) { hid_t attr = H5Aopen(obj_id, name, H5P_DEFAULT); diff --git a/src/mesh.cpp b/src/mesh.cpp index e8f79aa37..55fbc8e29 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -220,21 +220,58 @@ void UnstructuredMesh::to_hdf5(hid_t group) const write_dataset(mesh_group, "type", mesh_type); write_dataset(mesh_group, "filename", filename_); write_dataset(mesh_group, "library", this->library()); - // write volume of each element - vector tet_vols; - xt::xtensor centroids({static_cast(this->n_bins()), 3}); - for (int i = 0; i < this->n_bins(); i++) { - tet_vols.emplace_back(this->volume(i)); - auto c = this->centroid(i); - xt::view(centroids, i, xt::all()) = xt::xarray({c.x, c.y, c.z}); - } - - write_dataset(mesh_group, "volumes", tet_vols); - write_dataset(mesh_group, "centroids", centroids); if (specified_length_multiplier_) write_dataset(mesh_group, "length_multiplier", length_multiplier_); + // write vertex coordinates + xt::xtensor vertices({static_cast(this->n_vertices()), 3}); + for (int i = 0; i < this->n_vertices(); i++) { + auto v = this->vertex(i); + xt::view(vertices, i, xt::all()) = xt::xarray({v.x, v.y, v.z}); + } + write_dataset(mesh_group, "vertices", vertices); + + int num_elem_skipped = 0; + + // write element types and connectivity + vector volumes; + xt::xtensor connectivity ({static_cast(this->n_bins()), 8}); + xt::xtensor elem_types ({static_cast(this->n_bins()), 1}); + for (int i = 0; i < this->n_bins(); i++) { + auto conn = this->connectivity(i); + + volumes.emplace_back(this->volume(i)); + + // write linear tet element + if (conn.size() == 4) { + xt::view(elem_types, i, xt::all()) = static_cast(ElementType::LINEAR_TET); + xt::view(connectivity, i, xt::all()) = xt::xarray({conn[0], conn[1], conn[2], conn[3], + -1, -1, -1, -1}); + // write linear hex element + } else if (conn.size() == 8) { + xt::view(elem_types, i, xt::all()) = static_cast(ElementType::LINEAR_HEX); + xt::view(connectivity, i, xt::all()) = xt::xarray({conn[0], conn[1], conn[2], conn[3], + conn[4], conn[5], conn[6], conn[7]}); + } else { + num_elem_skipped++; + xt::view(elem_types, i, xt::all()) = static_cast(ElementType::UNSUPPORTED); + xt::view(connectivity, i, xt::all()) = -1; + } + } + + // warn users that some elements were skipped + if (num_elem_skipped > 0) { + warning(fmt::format("The connectivity of {} elements " + "on mesh {} were not written " + "because they are not of type linear tet/hex.", + num_elem_skipped, this->id_)); + } + + write_dataset(mesh_group, "volumes", volumes); + write_dataset(mesh_group, "connectivity", connectivity); + write_dataset(mesh_group, "element_types", elem_types); + close_group(mesh_group); } @@ -1775,6 +1812,14 @@ void MOABMesh::initialize() filename_); } + // set member range of vertices + int vertex_dim = 0; + rval = mbi_->get_entities_by_dimension(0, vertex_dim, verts_); + if (rval != moab::MB_SUCCESS) { + fatal_error("Failed to get all vertex handles"); + } + + // make an entity set for all tetrahedra // this is used for convenience later in output rval = mbi_->create_meshset(moab::MESHSET_SET, tetset_); @@ -2124,6 +2169,14 @@ std::pair, vector> MOABMesh::plot( return {}; } +int MOABMesh::get_vert_idx_from_handle(moab::EntityHandle vert) const { + int idx = vert - verts_[0]; + if (idx >= n_vertices()) { + fatal_error(fmt::format("Invalid vertex idx {} (# vertices {})", idx, n_vertices())); + } + return idx; +} + int MOABMesh::get_bin_from_ent_handle(moab::EntityHandle eh) const { int bin = eh - ehs_[0]; @@ -2191,6 +2244,46 @@ Position MOABMesh::centroid(int bin) const return {centroid[0], centroid[1], centroid[2]}; } +int MOABMesh::n_vertices() const { + return verts_.size(); +} + +Position MOABMesh::vertex(int id) const { + + moab::ErrorCode rval; + + moab::EntityHandle vert = verts_[id]; + + moab::CartVect coords; + rval = mbi_->get_coords(&vert, 1, coords.array()); + if (rval != moab::MB_SUCCESS) { + fatal_error("Failed to get the coordinates of a vertex."); + } + + return {coords[0], coords[1], coords[2]}; +} + +std::vector MOABMesh::connectivity(int bin) const { + moab::ErrorCode rval; + + auto tet = get_ent_handle_from_bin(bin); + + // look up the tet connectivity + vector conn; + rval = mbi_->get_connectivity(&tet, 1, conn); + if (rval != moab::MB_SUCCESS) { + fatal_error("Failed to get connectivity of a mesh element."); + return {}; + } + + std::vector verts(4); + for (int i = 0; i < verts.size(); i++) { + verts[i] = get_vert_idx_from_handle(conn[i]); + } + + return verts; +} + std::pair MOABMesh::get_score_tags( std::string score) const { @@ -2324,16 +2417,37 @@ const std::string LibMesh::mesh_lib_type = "libmesh"; LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) { + // filename_ and length_multiplier_ will already be set by the UnstructuredMesh constructor + set_mesh_pointer_from_filename(filename_); + set_length_multiplier(length_multiplier_); initialize(); } -LibMesh::LibMesh(const std::string& filename, double length_multiplier) +// create the mesh from a pointer to a libMesh Mesh +LibMesh::LibMesh(libMesh::MeshBase & input_mesh, double length_multiplier) { - filename_ = filename; + m_ = &input_mesh; set_length_multiplier(length_multiplier); initialize(); } +// create the mesh from an input file +LibMesh::LibMesh(const std::string& filename, double length_multiplier) +{ + set_mesh_pointer_from_filename(filename); + set_length_multiplier(length_multiplier); + initialize(); +} + +void LibMesh::set_mesh_pointer_from_filename(const std::string& filename) +{ + filename_ = filename; + unique_m_ = make_unique(*settings::libmesh_comm, n_dimension_); + m_ = unique_m_.get(); + m_->read(filename_); +} + +// intialize from mesh file void LibMesh::initialize() { if (!settings::libmesh_comm) { @@ -2344,14 +2458,14 @@ void LibMesh::initialize() // assuming that unstructured meshes used in OpenMC are 3D n_dimension_ = 3; - m_ = make_unique(*settings::libmesh_comm, n_dimension_); - m_->read(filename_); - if (specified_length_multiplier_) { libMesh::MeshTools::Modification::scale(*m_, length_multiplier_); } - - m_->prepare_for_use(); + // if OpenMC is managing the libMesh::MeshBase instance, prepare the mesh. + // Otherwise assume that it is prepared by its owning application + if (unique_m_) { + m_->prepare_for_use(); + } // ensure that the loaded mesh is 3 dimensional if (m_->mesh_dimension() != n_dimension_) { @@ -2394,6 +2508,27 @@ Position LibMesh::centroid(int bin) const return {centroid(0), centroid(1), centroid(2)}; } +int LibMesh::n_vertices() const +{ + return m_->n_nodes(); +} + +Position LibMesh::vertex(int vertex_id) const +{ + const auto node_ref = m_->node_ref(vertex_id); + return {node_ref(0), node_ref(1), node_ref(2)}; +} + +std::vector LibMesh::connectivity(int elem_id) const +{ + std::vector conn; + const auto* elem_ptr = m_->elem_ptr(elem_id); + for (int i = 0; i < elem_ptr->n_nodes(); i++) { + conn.push_back(elem_ptr->node_id(i)); + } + return conn; +} + std::string LibMesh::library() const { return mesh_lib_type; diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 3677f6ed3..0b8e1ab42 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -332,4 +332,40 @@ void IncoherentInelasticAE::sample( mu += std::min(mu - mu_left, mu_right - mu) * (prn(seed) - 0.5); } +//============================================================================== +// MixedElasticAE implementation +//============================================================================== + +MixedElasticAE::MixedElasticAE( + hid_t group, const CoherentElasticXS& coh_xs, const Function1D& incoh_xs) + : coherent_dist_(coh_xs), coherent_xs_(coh_xs), incoherent_xs_(incoh_xs) +{ + // Read incoherent elastic distribution + hid_t incoherent_group = open_group(group, "incoherent"); + std::string temp; + read_attribute(incoherent_group, "type", temp); + if (temp == "incoherent_elastic") { + incoherent_dist_ = make_unique(incoherent_group); + } else if (temp == "incoherent_elastic_discrete") { + auto xs = dynamic_cast(&incoh_xs); + incoherent_dist_ = + make_unique(incoherent_group, xs->x()); + } + close_group(incoherent_group); +} + +void MixedElasticAE::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + // Evaluate coherent and incoherent elastic cross sections + double xs_coh = coherent_xs_(E_in); + double xs_incoh = incoherent_xs_(E_in); + + if (prn(seed) * (xs_coh + xs_incoh) < xs_coh) { + coherent_dist_.sample(E_in, E_out, mu, seed); + } else { + incoherent_dist_->sample(E_in, E_out, mu, seed); + } +} + } // namespace openmc diff --git a/src/state_point.cpp b/src/state_point.cpp index 9ea045915..648d1a249 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -818,6 +818,16 @@ void write_unstructured_mesh_results() if (!umesh->output_) continue; + if (umesh->library() == "moab") { + if (mpi::master) + warning(fmt::format( + "Output for a MOAB mesh (mesh {}) was " + "requested but will not be written. Please use the Python " + "API to generated the desired VTK tetrahedral mesh.", + umesh->id_)); + continue; + } + // if this tally has more than one filter, print // warning and skip writing the mesh if (tally->filters().size() > 1) { @@ -889,12 +899,10 @@ void write_unstructured_mesh_results() std::string filename = fmt::format("tally_{0}.{1:0{2}}", tally->id_, simulation::current_batch, batch_width); - if (umesh->library() == "moab" && !mpi::master) - continue; - // Write the unstructured mesh and data to file umesh->write(filename); + // remove score data added for this mesh write umesh->remove_scores(); } } diff --git a/src/thermal.cpp b/src/thermal.cpp index 1101d5485..1a9592d0a 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -210,14 +210,22 @@ ThermalData::ThermalData(hid_t group) if (temp == "coherent_elastic") { auto xs = dynamic_cast(elastic_.xs.get()); elastic_.distribution = make_unique(*xs); - } else { - if (temp == "incoherent_elastic") { - elastic_.distribution = make_unique(dgroup); - } else if (temp == "incoherent_elastic_discrete") { - auto xs = dynamic_cast(elastic_.xs.get()); - elastic_.distribution = - make_unique(dgroup, xs->x()); - } + } else if (temp == "incoherent_elastic") { + elastic_.distribution = make_unique(dgroup); + } else if (temp == "incoherent_elastic_discrete") { + auto xs = dynamic_cast(elastic_.xs.get()); + elastic_.distribution = + make_unique(dgroup, xs->x()); + } else if (temp == "mixed_elastic") { + // Get coherent/incoherent cross sections + auto mixed_xs = dynamic_cast(elastic_.xs.get()); + const auto& coh_xs = + dynamic_cast(mixed_xs->functions(0).get()); + const auto& incoh_xs = mixed_xs->functions(1).get(); + + // Create mixed elastic distribution + elastic_.distribution = + make_unique(dgroup, *coh_xs, *incoh_xs); } close_group(elastic_group); diff --git a/tests/micro_xs_simple.csv b/tests/micro_xs_simple.csv new file mode 100644 index 000000000..71981c718 --- /dev/null +++ b/tests/micro_xs_simple.csv @@ -0,0 +1,13 @@ +nuclide,"(n,gamma)",fission +U234,22.231989822002454,0.4962074466374984 +U235,10.479008971197121,48.41787337164606 +U238,0.8673334105437321,0.1046788058876236 +U236,8.651710446071224,0.31948392400019293 +O16,7.497851000107522e-05,0.0 +O17,0.0004079227797153372,0.0 +I135,6.842395323713929,0.0 +Xe135,227463.8642699061,0.0 +Xe136,0.023178960347535887,0.0 +Cs135,2.1721665580713623,0.0 +Gd157,12786.099392370175,0.0 +Gd156,3.4006085445846983,0.0 diff --git a/tests/regression_tests/cpp_driver/test.py b/tests/regression_tests/cpp_driver/test.py index 95912b3f7..e8b1c62ac 100644 --- a/tests/regression_tests/cpp_driver/test.py +++ b/tests/regression_tests/cpp_driver/test.py @@ -48,8 +48,8 @@ def cpp_driver(request): finally: # Remove local build directory when test is complete - shutil.rmtree('build') - os.remove('CMakeLists.txt') + shutil.rmtree(request.node.path.parent / 'build') + os.remove(request.node.path.parent / 'CMakeLists.txt') @pytest.fixture diff --git a/tests/regression_tests/dagmc/legacy/test.py b/tests/regression_tests/dagmc/legacy/test.py index e0fccb934..dacbc19ee 100644 --- a/tests/regression_tests/dagmc/legacy/test.py +++ b/tests/regression_tests/dagmc/legacy/test.py @@ -1,6 +1,7 @@ import openmc import openmc.lib +from pathlib import Path import pytest from tests.testing_harness import PyAPITestHarness @@ -27,7 +28,7 @@ def model(): model.settings.dagmc = True # geometry - dag_univ = openmc.DAGMCUniverse("dagmc.h5m") + dag_univ = openmc.DAGMCUniverse(Path("dagmc.h5m")) model.geometry = openmc.Geometry(dag_univ) # tally diff --git a/tests/regression_tests/dagmc/universes/inputs_true.dat b/tests/regression_tests/dagmc/universes/inputs_true.dat index 2165a78b6..ea6ad519b 100644 --- a/tests/regression_tests/dagmc/universes/inputs_true.dat +++ b/tests/regression_tests/dagmc/universes/inputs_true.dat @@ -1,6 +1,6 @@ - + 24.0 24.0 @@ -10,12 +10,12 @@ 9 9 9 9 - - - - - - + + + + + + diff --git a/tests/regression_tests/dagmc/universes/test.py b/tests/regression_tests/dagmc/universes/test.py index f2eb3882f..47897ad56 100644 --- a/tests/regression_tests/dagmc/universes/test.py +++ b/tests/regression_tests/dagmc/universes/test.py @@ -53,25 +53,6 @@ class DAGMCUniverseTest(PyAPITestHarness): # assigns the bound_dag_geometry to the model to test the type checks in model.Geometry setter model.Geometry = bound_pincell_geometry - # checks that the bounding box is calculated correctly - bounding_box = pincell_univ.bounding_box - assert bounding_box[0].tolist() == [-25., -25., -25.] - assert bounding_box[1].tolist() == [25., 25., 25.] - - # checks that the bounding region is six surfaces each with a vacuum boundary type - b_region = pincell_univ.bounding_region(bounded_type='box', boundary_type='vacuum') - assert isinstance(b_region, openmc.Region) - assert len(b_region.get_surfaces()) == 6 - for surface in list(b_region.get_surfaces().values()): - assert surface.boundary_type == 'vacuum' - - # checks that the bounding region is a single surface with a reflective boundary type - b_region = pincell_univ.bounding_region(bounded_type='sphere', boundary_type='reflective') - assert isinstance(b_region, openmc.Region) - assert len(b_region.get_surfaces()) == 1 - for surface in list(b_region.get_surfaces().values()): - assert surface.boundary_type == 'reflective' - # create a 2 x 2 lattice using the DAGMC pincell pitch = np.asarray((24.0, 24.0)) lattice = openmc.RectLattice() diff --git a/tests/regression_tests/deplete_no_transport/__init__.py b/tests/regression_tests/deplete_no_transport/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py new file mode 100644 index 000000000..f5ee4bf1d --- /dev/null +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -0,0 +1,217 @@ +""" Transport-free depletion test suite """ + +from pathlib import Path + +import numpy as np +import pytest +import openmc +import openmc.deplete +from openmc.deplete import IndependentOperator, MicroXS + +@pytest.fixture(scope="module") +def fuel(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + fuel.depletable = True + + fuel.volume = np.pi * 0.42 ** 2 + + return fuel + +@pytest.fixture(scope="module") +def micro_xs(): + micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' + return MicroXS.from_csv(micro_xs_file) + +@pytest.fixture(scope="module") +def chain_file(): + return Path(__file__).parents[2] / 'chain_simple.xml' + +@pytest.mark.parametrize("multiproc, from_nuclides, normalization_mode, power, flux", [ + (True, True,'source-rate', None, 1164719970082145.0), + (False, True, 'source-rate', None, 1164719970082145.0), + (True, True, 'fission-q', 174, None), + (False, True, 'fission-q', 174, None), + (True, False,'source-rate', None, 1164719970082145.0), + (False, False, 'source-rate', None, 1164719970082145.0), + (True, False, 'fission-q', 174, None), + (False, False, 'fission-q', 174, None)]) +def test_against_self(run_in_tmpdir, + fuel, + micro_xs, + chain_file, + multiproc, + from_nuclides, + normalization_mode, + power, + flux): + """Transport free system test suite. + + Runs an OpenMC transport-free depletion calculation and verifies + that the outputs match a reference file. + + """ + # Create operator + op = _create_operator(from_nuclides, + fuel, + micro_xs, + chain_file, + normalization_mode) + + # Power and timesteps + dt = [360] # single step + + # Perform simulation using the predictor algorithm + openmc.deplete.pool.USE_MULTIPROCESSING = multiproc + openmc.deplete.PredictorIntegrator(op, + dt, + power=power, + source_rates=flux, + timestep_units='s').integrate() + + # Get path to test and reference results + path_test = op.output_dir / 'depletion_results.h5' + if flux is not None: + ref_path = 'test_reference_source_rate.h5' + else: + ref_path = 'test_reference_fission_q.h5' + path_reference = Path(__file__).with_name(ref_path) + + # Load the reference/test results + res_test = openmc.deplete.Results(path_test) + res_ref = openmc.deplete.Results(path_reference) + + # Assert same mats + _assert_same_mats(res_test, res_ref) + + tol = 1.0e-14 + _assert_atoms_equal(res_test, res_ref, tol) + _assert_reaction_rates_equal(res_test, res_ref, tol) + +@pytest.mark.parametrize("multiproc, dt, time_units, time_type, atom_tol, rx_tol ", [ + (True, 360, 's', 'minutes', 2.0e-3, 3.0e-2), + (False, 360, 's', 'minutes', 2.0e-3, 3.0e-2), + (True, 4, 'h', 'hours', 2.0e-3, 6.0e-2), + (False,4, 'h', 'hours', 2.0e-3, 6.0e-2), + (True, 5, 'd', 'days', 2.0e-3, 5.0e-2), + (False,5, 'd', 'days', 2.0e-3, 5.0e-2), + (True, 100, 'd', 'months', 4.0e-3, 9.0e-2), + (False, 100, 'd', 'months', 4.0e-3, 9.0e-2)]) +def test_against_coupled(run_in_tmpdir, + fuel, + micro_xs, + chain_file, + multiproc, + dt, + time_units, + time_type, + atom_tol, + rx_tol): + # Create operator + op = _create_operator(False, fuel, micro_xs, chain_file, 'fission-q') + + # Power and timesteps + dt = [dt] # single step + + # Perform simulation using the predictor algorithm + openmc.deplete.pool.USE_MULTIPROCESSING = multiproc + openmc.deplete.PredictorIntegrator( + op, dt, power=174, timestep_units=time_units).integrate() + + # Get path to test and reference results + path_test = op.output_dir / 'depletion_results.h5' + + ref_path = f'test_reference_coupled_{time_type}.h5' + path_reference = Path(__file__).with_name(ref_path) + + # Load the reference/test results + res_test = openmc.deplete.Results(path_test) + res_ref = openmc.deplete.Results(path_reference) + + # Assert same mats + _assert_same_mats(res_test, res_ref) + + _assert_atoms_equal(res_test, res_ref, atom_tol) + _assert_reaction_rates_equal(res_test, res_ref, rx_tol) + +def _create_operator(from_nuclides, + fuel, + micro_xs, + chain_file, + normalization_mode): + if from_nuclides: + nuclides = {} + for nuc, dens in fuel.get_nuclide_atom_densities().items(): + nuclides[nuc] = dens + + op = IndependentOperator.from_nuclides(fuel.volume, + nuclides, + micro_xs, + chain_file, + normalization_mode=normalization_mode) + + else: + op = IndependentOperator(openmc.Materials([fuel]), + micro_xs, + chain_file, + normalization_mode=normalization_mode) + + return op + +def _assert_same_mats(res_ref, res_test): + for mat in res_ref[0].mat_to_ind: + assert mat in res_test[0].mat_to_ind, \ + "Material {} not in new results.".format(mat) + for nuc in res_ref[0].nuc_to_ind: + assert nuc in res_test[0].nuc_to_ind, \ + "Nuclide {} not in new results.".format(nuc) + + for mat in res_test[0].mat_to_ind: + assert mat in res_ref[0].mat_to_ind, \ + "Material {} not in old results.".format(mat) + for nuc in res_test[0].nuc_to_ind: + assert nuc in res_ref[0].nuc_to_ind, \ + "Nuclide {} not in old results.".format(nuc) + +def _assert_atoms_equal(res_ref, res_test, tol): + for mat in res_test[0].mat_to_ind: + for nuc in res_test[0].nuc_to_ind: + _, y_test = res_test.get_atoms(mat, nuc) + _, y_old = res_ref.get_atoms(mat, nuc) + + # Test each point + correct = True + for i, ref in enumerate(y_old): + if ref != y_test[i]: + if ref != 0.0: + correct = np.abs(y_test[i] - ref) / ref <= tol + else: + correct = False + + assert correct, "Discrepancy in mat {} and nuc {}\n{}\n{}".format( + mat, nuc, y_old, y_test) + +def _assert_reaction_rates_equal(res_ref, res_test, tol): + for reactions in res_test[0].rates: + for mat in reactions.index_mat: + for nuc in reactions.index_nuc: + for rx in reactions.index_rx: + y_test = res_test.get_reaction_rate(mat, nuc, rx)[1] / \ + res_test.get_atoms(mat, nuc)[1] + y_old = res_ref.get_reaction_rate(mat, nuc, rx)[1] / \ + res_ref.get_atoms(mat, nuc)[1] + + # Test each point + correct = True + for i, ref in enumerate(y_old): + if ref != y_test[i]: + if ref != 0.0: + correct = np.abs(y_test[i] - ref) / ref <= tol + else: + if y_test[i] != 0.0: + correct = False + + assert correct, "Discrepancy in mat {}, nuc {}, and rx {}\n{}\n{}".format( + mat, nuc, rx, y_old, y_test) diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 new file mode 100644 index 000000000..d97acffec Binary files /dev/null and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 differ diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5 new file mode 100644 index 000000000..6d9bcc91f Binary files /dev/null and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5 differ diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5 new file mode 100644 index 000000000..1e22bf2b7 Binary files /dev/null and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5 differ diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5 new file mode 100644 index 000000000..43b45cb37 Binary files /dev/null and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5 differ diff --git a/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 b/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 new file mode 100644 index 000000000..826c5f3b0 Binary files /dev/null and b/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 differ diff --git a/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 b/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 new file mode 100644 index 000000000..f5161d370 Binary files /dev/null and b/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 differ diff --git a/tests/regression_tests/deplete_with_transport/__init__.py b/tests/regression_tests/deplete_with_transport/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/deplete/example_geometry.py b/tests/regression_tests/deplete_with_transport/example_geometry.py similarity index 100% rename from tests/regression_tests/deplete/example_geometry.py rename to tests/regression_tests/deplete_with_transport/example_geometry.py diff --git a/tests/regression_tests/deplete/last_step_reference_materials.xml b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml similarity index 100% rename from tests/regression_tests/deplete/last_step_reference_materials.xml rename to tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml diff --git a/tests/regression_tests/deplete/test.py b/tests/regression_tests/deplete_with_transport/test.py similarity index 99% rename from tests/regression_tests/deplete/test.py rename to tests/regression_tests/deplete_with_transport/test.py index 6c431fb33..0c2a4cd97 100644 --- a/tests/regression_tests/deplete/test.py +++ b/tests/regression_tests/deplete_with_transport/test.py @@ -12,7 +12,7 @@ from openmc.data import JOULE_PER_EV import openmc.deplete from tests.regression_tests import config -from example_geometry import generate_problem +from .example_geometry import generate_problem @pytest.fixture(scope="module") diff --git a/tests/regression_tests/deplete/test_reference.h5 b/tests/regression_tests/deplete_with_transport/test_reference.h5 similarity index 100% rename from tests/regression_tests/deplete/test_reference.h5 rename to tests/regression_tests/deplete_with_transport/test_reference.h5 diff --git a/tests/regression_tests/mgxs_library_condense/inputs_true.dat b/tests/regression_tests/mgxs_library_condense/inputs_true.dat index 9661cd46b..cac18ffcd 100644 --- a/tests/regression_tests/mgxs_library_condense/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_condense/inputs_true.dat @@ -67,16 +67,16 @@ 1 - + 3 - + 0.0 20000000.0 - + 1 - + 1 2 3 4 5 6 @@ -166,19 +166,19 @@ 1 2 total - fission + (n,2n) tracklength 1 2 total - flux + (n,3n) tracklength 1 2 total - fission + (n,4n) tracklength @@ -190,207 +190,207 @@ 1 2 total - nu-fission + absorption tracklength 1 2 total - flux + fission tracklength 1 2 total - kappa-fission + flux tracklength 1 2 total - flux + fission tracklength 1 2 total - scatter + flux tracklength 1 2 total - flux - analog + nu-fission + tracklength 1 2 total - nu-scatter - analog + flux + tracklength 1 2 total - flux - analog + kappa-fission + tracklength - 1 2 5 28 + 1 2 total - scatter - analog + flux + tracklength + 1 2 + total + scatter + tracklength + + 1 2 total flux analog - - 1 2 5 28 - total - nu-scatter - analog - - 1 2 5 + 1 2 total nu-scatter analog - 1 2 5 + 1 2 total - scatter + flux analog - 1 2 + 1 2 5 30 total - flux + scatter analog - 1 2 5 + 1 2 total - nu-fission + flux analog - 1 2 5 + 1 2 5 30 total - scatter + nu-scatter analog - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - 1 2 5 total nu-scatter analog - - 1 52 + + 1 2 5 + total + scatter + analog + + + 1 2 + total + flux + analog + + + 1 2 5 total nu-fission analog - - 1 5 + + 1 2 5 total - nu-fission + scatter analog - - 1 52 - total - prompt-nu-fission - analog - - - 1 5 - total - prompt-nu-fission - analog - - + 1 2 total flux tracklength + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 5 + total + nu-scatter + analog + + 1 54 + total + nu-fission + analog + + + 1 5 + total + nu-fission + analog + + + 1 54 + total + prompt-nu-fission + analog + + + 1 5 + total + prompt-nu-fission + analog + + + 1 2 + total + flux + tracklength + + 1 2 total inverse-velocity tracklength - - 1 2 - total - flux - tracklength - - - 1 2 - total - prompt-nu-fission - tracklength - - - 1 2 - total - flux - analog - - - 1 2 5 - total - prompt-nu-fission - analog - - - 66 2 - total - current - analog - 1 2 total @@ -400,7 +400,7 @@ 1 2 total - total + prompt-nu-fission tracklength @@ -410,91 +410,121 @@ analog - 1 5 6 + 1 2 5 total - scatter + prompt-nu-fission analog - 1 2 + 68 2 total - flux - tracklength + current + analog 1 2 total - total + flux tracklength + 1 2 + total + total + tracklength + + 1 2 total flux analog - + + 1 5 6 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + total + tracklength + + + 1 2 + total + flux + analog + + 1 5 6 total nu-scatter analog - + 1 2 total flux tracklength - - 1 77 2 - total - delayed-nu-fission - tracklength - - - 1 77 52 - total - delayed-nu-fission - analog - - - 1 77 5 - total - delayed-nu-fission - analog - - - 1 2 - total - nu-fission - tracklength - - - 1 77 2 - total - delayed-nu-fission - tracklength - - 1 77 + 1 79 2 total delayed-nu-fission tracklength - 1 77 + 1 79 54 + total + delayed-nu-fission + analog + + + 1 79 5 + total + delayed-nu-fission + analog + + + 1 2 + total + nu-fission + tracklength + + + 1 79 2 + total + delayed-nu-fission + tracklength + + + 1 79 + total + delayed-nu-fission + tracklength + + + 1 79 total decay-rate tracklength - + 1 2 total flux analog - - 1 77 2 5 + + 1 79 2 5 total delayed-nu-fission analog diff --git a/tests/regression_tests/mgxs_library_condense/results_true.dat b/tests/regression_tests/mgxs_library_condense/results_true.dat index 508984e54..e0b47a78b 100644 --- a/tests/regression_tests/mgxs_library_condense/results_true.dat +++ b/tests/regression_tests/mgxs_library_condense/results_true.dat @@ -24,6 +24,12 @@ 3 2 2 1 1 total 0.022046 0.001594 mesh 1 group in nuclide mean std. dev. x y z +0 1 1 1 1 total 0.021489 0.001396 +2 1 2 1 1 total 0.020837 0.001436 +1 2 1 1 1 total 0.021454 0.001983 +3 2 2 1 1 total 0.022036 0.001594 + mesh 1 group in nuclide mean std. dev. + x y z 0 1 1 1 1 total 0.011598 0.001508 2 1 2 1 1 total 0.010856 0.001613 1 2 1 1 1 total 0.011622 0.002259 diff --git a/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat b/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat index 602e04abe..bd6abbb7c 100644 --- a/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat @@ -89,10 +89,10 @@ 1 - + 3 - + 1 2 3 4 5 6 @@ -182,19 +182,19 @@ 1 2 total - fission + (n,2n) tracklength 1 2 total - flux + (n,3n) tracklength 1 2 total - fission + (n,4n) tracklength @@ -206,305 +206,335 @@ 1 2 total - nu-fission + absorption tracklength 1 2 total - flux + fission tracklength 1 2 total - kappa-fission + flux tracklength 1 2 total - flux + fission tracklength 1 2 total - scatter + flux tracklength 1 2 total - flux - analog + nu-fission + tracklength 1 2 total - nu-scatter - analog + flux + tracklength 1 2 total - flux - analog + kappa-fission + tracklength - 1 2 5 28 + 1 2 total - scatter - analog + flux + tracklength + 1 2 + total + scatter + tracklength + + 1 2 total flux analog - - 1 2 5 28 - total - nu-scatter - analog - - 1 2 5 + 1 2 total nu-scatter analog - 1 2 5 + 1 2 total - scatter + flux analog - 1 2 + 1 2 5 30 total - flux + scatter analog - 1 2 5 + 1 2 total - nu-fission + flux analog - 1 2 5 + 1 2 5 30 total - scatter + nu-scatter analog - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - 1 2 5 total nu-scatter analog - + + 1 2 5 + total + scatter + analog + + 1 2 total + flux + analog + + + 1 2 5 + total nu-fission analog - - 1 5 + + 1 2 5 total - nu-fission + scatter analog + + 1 2 + total + flux + tracklength + + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 + total + flux + tracklength + 1 2 total - prompt-nu-fission - analog + scatter + tracklength - 1 5 + 1 2 5 30 total - prompt-nu-fission + scatter analog - 1 2 + 1 2 5 total - flux - tracklength + nu-scatter + analog 1 2 total - inverse-velocity - tracklength + nu-fission + analog - 1 2 + 1 5 total - flux - tracklength + nu-fission + analog 1 2 total prompt-nu-fission - tracklength - - - 1 2 - total - flux analog - - 1 2 5 + + 1 5 total prompt-nu-fission analog - + 1 2 total flux tracklength + + 1 2 + total + inverse-velocity + tracklength + 1 2 total - total + flux tracklength 1 2 total - flux - analog + prompt-nu-fission + tracklength - 1 5 6 - total - scatter - analog - - 1 2 total flux - tracklength + analog + + + 1 2 5 + total + prompt-nu-fission + analog 1 2 total - total + flux tracklength + 1 2 + total + total + tracklength + + 1 2 total flux analog - + + 1 5 6 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + total + tracklength + + + 1 2 + total + flux + analog + + 1 5 6 total nu-scatter analog - + 1 2 total flux tracklength - - 1 73 2 - total - delayed-nu-fission - tracklength - - - 1 73 2 - total - delayed-nu-fission - analog - - - 1 73 5 - total - delayed-nu-fission - analog - - - 1 2 - total - nu-fission - tracklength - - - 1 73 2 - total - delayed-nu-fission - tracklength - - 1 73 + 1 75 2 total delayed-nu-fission tracklength - 1 73 + 1 75 2 + total + delayed-nu-fission + analog + + + 1 75 5 + total + delayed-nu-fission + analog + + + 1 2 + total + nu-fission + tracklength + + + 1 75 2 + total + delayed-nu-fission + tracklength + + + 1 75 + total + delayed-nu-fission + tracklength + + + 1 75 total decay-rate tracklength - + 1 2 total flux analog - - 1 73 2 5 + + 1 75 2 5 total delayed-nu-fission analog diff --git a/tests/regression_tests/mgxs_library_distribcell/results_true.dat b/tests/regression_tests/mgxs_library_distribcell/results_true.dat index e6e4b99e4..3d362c1bd 100644 --- a/tests/regression_tests/mgxs_library_distribcell/results_true.dat +++ b/tests/regression_tests/mgxs_library_distribcell/results_true.dat @@ -7,6 +7,8 @@ sum(distribcell) group in nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 0.06484 0.002514 sum(distribcell) group in nuclide mean std. dev. +0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 0.064761 0.002514 + sum(distribcell) group in nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 0.028638 0.002713 sum(distribcell) group in nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 0.036203 0.001449 diff --git a/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat b/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat index 9661cd46b..cac18ffcd 100644 --- a/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat @@ -67,16 +67,16 @@ 1 - + 3 - + 0.0 20000000.0 - + 1 - + 1 2 3 4 5 6 @@ -166,19 +166,19 @@ 1 2 total - fission + (n,2n) tracklength 1 2 total - flux + (n,3n) tracklength 1 2 total - fission + (n,4n) tracklength @@ -190,207 +190,207 @@ 1 2 total - nu-fission + absorption tracklength 1 2 total - flux + fission tracklength 1 2 total - kappa-fission + flux tracklength 1 2 total - flux + fission tracklength 1 2 total - scatter + flux tracklength 1 2 total - flux - analog + nu-fission + tracklength 1 2 total - nu-scatter - analog + flux + tracklength 1 2 total - flux - analog + kappa-fission + tracklength - 1 2 5 28 + 1 2 total - scatter - analog + flux + tracklength + 1 2 + total + scatter + tracklength + + 1 2 total flux analog - - 1 2 5 28 - total - nu-scatter - analog - - 1 2 5 + 1 2 total nu-scatter analog - 1 2 5 + 1 2 total - scatter + flux analog - 1 2 + 1 2 5 30 total - flux + scatter analog - 1 2 5 + 1 2 total - nu-fission + flux analog - 1 2 5 + 1 2 5 30 total - scatter + nu-scatter analog - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - 1 2 5 total nu-scatter analog - - 1 52 + + 1 2 5 + total + scatter + analog + + + 1 2 + total + flux + analog + + + 1 2 5 total nu-fission analog - - 1 5 + + 1 2 5 total - nu-fission + scatter analog - - 1 52 - total - prompt-nu-fission - analog - - - 1 5 - total - prompt-nu-fission - analog - - + 1 2 total flux tracklength + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 5 + total + nu-scatter + analog + + 1 54 + total + nu-fission + analog + + + 1 5 + total + nu-fission + analog + + + 1 54 + total + prompt-nu-fission + analog + + + 1 5 + total + prompt-nu-fission + analog + + + 1 2 + total + flux + tracklength + + 1 2 total inverse-velocity tracklength - - 1 2 - total - flux - tracklength - - - 1 2 - total - prompt-nu-fission - tracklength - - - 1 2 - total - flux - analog - - - 1 2 5 - total - prompt-nu-fission - analog - - - 66 2 - total - current - analog - 1 2 total @@ -400,7 +400,7 @@ 1 2 total - total + prompt-nu-fission tracklength @@ -410,91 +410,121 @@ analog - 1 5 6 + 1 2 5 total - scatter + prompt-nu-fission analog - 1 2 + 68 2 total - flux - tracklength + current + analog 1 2 total - total + flux tracklength + 1 2 + total + total + tracklength + + 1 2 total flux analog - + + 1 5 6 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + total + tracklength + + + 1 2 + total + flux + analog + + 1 5 6 total nu-scatter analog - + 1 2 total flux tracklength - - 1 77 2 - total - delayed-nu-fission - tracklength - - - 1 77 52 - total - delayed-nu-fission - analog - - - 1 77 5 - total - delayed-nu-fission - analog - - - 1 2 - total - nu-fission - tracklength - - - 1 77 2 - total - delayed-nu-fission - tracklength - - 1 77 + 1 79 2 total delayed-nu-fission tracklength - 1 77 + 1 79 54 + total + delayed-nu-fission + analog + + + 1 79 5 + total + delayed-nu-fission + analog + + + 1 2 + total + nu-fission + tracklength + + + 1 79 2 + total + delayed-nu-fission + tracklength + + + 1 79 + total + delayed-nu-fission + tracklength + + + 1 79 total decay-rate tracklength - + 1 2 total flux analog - - 1 77 2 5 + + 1 79 2 5 total delayed-nu-fission analog diff --git a/tests/regression_tests/mgxs_library_hdf5/results_true.dat b/tests/regression_tests/mgxs_library_hdf5/results_true.dat index 6da73ec9b..b075939f2 100644 --- a/tests/regression_tests/mgxs_library_hdf5/results_true.dat +++ b/tests/regression_tests/mgxs_library_hdf5/results_true.dat @@ -10,6 +10,9 @@ domain=1 type=nu-transport domain=1 type=absorption [9.18204614e-03 9.50890834e-02] [1.02291781e-03 9.51211716e-03] +domain=1 type=reduced absorption +[9.15806809e-03 9.50890834e-02] +[1.02286279e-03 9.51211716e-03] domain=1 type=capture [6.76780024e-03 4.04282690e-02] [1.01848835e-03 8.89313901e-03] diff --git a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat index 5cac8ecf4..ac2ff0c81 100644 --- a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat @@ -50,13 +50,13 @@ 1 - + 3 - + 1 - + 1 2 3 4 5 6 @@ -146,19 +146,19 @@ 1 2 total - fission + (n,2n) tracklength 1 2 total - flux + (n,3n) tracklength 1 2 total - fission + (n,4n) tracklength @@ -170,206 +170,206 @@ 1 2 total - nu-fission + absorption tracklength 1 2 total - flux + fission tracklength 1 2 total - kappa-fission + flux tracklength 1 2 total - flux + fission tracklength 1 2 total - scatter + flux tracklength 1 2 total - flux - analog + nu-fission + tracklength 1 2 total - nu-scatter - analog + flux + tracklength 1 2 total - flux - analog + kappa-fission + tracklength - 1 2 5 28 + 1 2 total - scatter - analog + flux + tracklength + 1 2 + total + scatter + tracklength + + 1 2 total flux analog - - 1 2 5 28 - total - nu-scatter - analog - - 1 2 5 + 1 2 total nu-scatter analog - 1 2 5 + 1 2 total - scatter + flux analog - 1 2 + 1 2 5 30 total - flux + scatter analog - 1 2 5 + 1 2 total - nu-fission + flux analog - 1 2 5 + 1 2 5 30 total - scatter + nu-scatter analog - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - 1 2 5 total nu-scatter analog - + + 1 2 5 + total + scatter + analog + + 1 2 total + flux + analog + + + 1 2 5 + total nu-fission analog - - 1 5 + + 1 2 5 total - nu-fission + scatter analog + + 1 2 + total + flux + tracklength + + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 + total + flux + tracklength + 1 2 total - prompt-nu-fission - analog + scatter + tracklength - 1 5 + 1 2 5 30 total - prompt-nu-fission + scatter analog - 1 2 + 1 2 5 total - flux - tracklength + nu-scatter + analog 1 2 total - inverse-velocity - tracklength + nu-fission + analog - 1 2 + 1 5 total - flux - tracklength + nu-fission + analog 1 2 total prompt-nu-fission - tracklength - - - 1 2 - total - flux analog - - 1 2 5 + + 1 5 total prompt-nu-fission analog - - 66 2 + + 1 2 total - current - analog + flux + tracklength + + + 1 2 + total + inverse-velocity + tracklength 1 2 @@ -380,7 +380,7 @@ 1 2 total - total + prompt-nu-fission tracklength @@ -390,91 +390,121 @@ analog - 1 5 6 + 1 2 5 total - scatter + prompt-nu-fission analog - 1 2 + 68 2 total - flux - tracklength + current + analog 1 2 total - total + flux tracklength + 1 2 + total + total + tracklength + + 1 2 total flux analog - + + 1 5 6 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + total + tracklength + + + 1 2 + total + flux + analog + + 1 5 6 total nu-scatter analog - + 1 2 total flux tracklength - - 1 77 2 - total - delayed-nu-fission - tracklength - - - 1 77 2 - total - delayed-nu-fission - analog - - - 1 77 5 - total - delayed-nu-fission - analog - - - 1 2 - total - nu-fission - tracklength - - - 1 77 2 - total - delayed-nu-fission - tracklength - - 1 77 + 1 79 2 total delayed-nu-fission tracklength - 1 77 + 1 79 2 + total + delayed-nu-fission + analog + + + 1 79 5 + total + delayed-nu-fission + analog + + + 1 2 + total + nu-fission + tracklength + + + 1 79 2 + total + delayed-nu-fission + tracklength + + + 1 79 + total + delayed-nu-fission + tracklength + + + 1 79 total decay-rate tracklength - + 1 2 total flux analog - - 1 77 2 5 + + 1 79 2 5 total delayed-nu-fission analog diff --git a/tests/regression_tests/mgxs_library_mesh/results_true.dat b/tests/regression_tests/mgxs_library_mesh/results_true.dat index 7a6b4abe7..390b5c453 100644 --- a/tests/regression_tests/mgxs_library_mesh/results_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/results_true.dat @@ -24,6 +24,12 @@ 3 2 2 1 1 total 0.013286 0.000621 mesh 1 group in nuclide mean std. dev. x y z +0 1 1 1 1 total 0.012881 0.000835 +2 1 2 1 1 total 0.013282 0.000552 +1 2 1 1 1 total 0.013896 0.000714 +3 2 2 1 1 total 0.013180 0.000621 + mesh 1 group in nuclide mean std. dev. + x y z 0 1 1 1 1 total 0.001231 0.000979 2 1 2 1 1 total 0.001332 0.000691 1 2 1 1 1 total 0.001346 0.000770 diff --git a/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat index 0d9dbf821..59cbddec4 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat @@ -62,19 +62,19 @@ 1 - + 3 - + 0.0 20000000.0 - + 1 2 3 4 5 6 - + 2 - + 3 @@ -164,19 +164,19 @@ 1 2 total - fission + (n,2n) tracklength 1 2 total - flux + (n,3n) tracklength 1 2 total - fission + (n,4n) tracklength @@ -188,1673 +188,1763 @@ 1 2 total - nu-fission + absorption tracklength 1 2 total - flux + fission tracklength 1 2 total - kappa-fission + flux tracklength 1 2 total - flux + fission tracklength 1 2 total - scatter + flux tracklength 1 2 total - flux - analog + nu-fission + tracklength 1 2 total - nu-scatter - analog + flux + tracklength 1 2 total - flux - analog + kappa-fission + tracklength - 1 2 5 28 + 1 2 total - scatter - analog + flux + tracklength + 1 2 + total + scatter + tracklength + + 1 2 total flux analog - - 1 2 5 28 - total - nu-scatter - analog - - 1 2 5 + 1 2 total nu-scatter analog - 1 2 5 + 1 2 total - scatter + flux analog - 1 2 + 1 2 5 30 total - flux + scatter analog - 1 2 5 + 1 2 total - nu-fission + flux analog - 1 2 5 + 1 2 5 30 total - scatter + nu-scatter analog - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - - 1 2 - total - flux - tracklength - - - 1 2 - total - scatter - tracklength - - - 1 2 5 28 - total - scatter - analog - - 1 2 5 total nu-scatter analog - - 1 52 + + 1 2 5 + total + scatter + analog + + + 1 2 + total + flux + analog + + + 1 2 5 total nu-fission analog + + 1 2 5 + total + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + 1 2 + total + flux + tracklength + + + 1 2 + total + scatter + tracklength + + + 1 2 5 30 + total + scatter + analog + + + 1 2 5 + total + nu-scatter + analog + + + 1 54 + total + nu-fission + analog + + 1 5 total nu-fission analog - - 1 52 + + 1 54 total prompt-nu-fission analog - + 1 5 total prompt-nu-fission analog - - 1 2 - total - flux - tracklength - - - 1 2 - total - inverse-velocity - tracklength - - - 1 2 - total - flux - tracklength - - - 1 2 - total - prompt-nu-fission - tracklength - - - 1 2 - total - flux - analog - - 1 2 5 + 1 2 total - prompt-nu-fission - analog + flux + tracklength 1 2 total - flux + inverse-velocity tracklength 1 2 total - total + flux tracklength 1 2 total - flux - analog + prompt-nu-fission + tracklength - 1 5 6 - total - scatter - analog - - 1 2 total flux - tracklength + analog + + + 1 2 5 + total + prompt-nu-fission + analog 1 2 total - total + flux tracklength 1 2 total - flux - analog + total + tracklength - 1 5 6 - total - nu-scatter - analog - - 1 2 total flux - tracklength + analog + + + 1 5 6 + total + scatter + analog 1 2 total - (n,elastic) + flux tracklength 1 2 total - flux + total tracklength 1 2 total - (n,level) - tracklength + flux + analog - 1 2 + 1 5 6 total - flux - tracklength + nu-scatter + analog 1 2 total - (n,2n) + flux tracklength 1 2 total - flux + (n,elastic) tracklength 1 2 total - (n,na) + flux tracklength 1 2 total - flux + (n,level) tracklength 1 2 total - (n,nc) + flux tracklength 1 2 total - flux + (n,2n) tracklength 1 2 total - (n,gamma) + flux tracklength 1 2 total - flux + (n,na) tracklength 1 2 total - (n,a) + flux tracklength 1 2 total - flux + (n,nc) tracklength 1 2 total - (n,Xa) + flux tracklength 1 2 total - flux + (n,gamma) tracklength 1 2 total - heating + flux tracklength 1 2 total - flux + (n,a) tracklength 1 2 total - damage-energy + flux tracklength 1 2 total - flux + (n,Xa) tracklength 1 2 total - (n,n1) + flux tracklength 1 2 total - flux + heating tracklength 1 2 total - (n,a0) + flux tracklength + 1 2 + total + damage-energy + tracklength + + + 1 2 + total + flux + tracklength + + + 1 2 + total + (n,n1) + tracklength + + + 1 2 + total + flux + tracklength + + + 1 2 + total + (n,a0) + tracklength + + 1 2 total flux analog - + 1 2 5 total (n,nc) analog - + 1 2 total flux analog - + 1 2 5 total (n,n1) analog - + 1 2 total flux analog - + 1 2 5 total (n,2n) analog - + 1 2 total flux tracklength - - 1 106 2 - total - delayed-nu-fission - tracklength - - - 1 106 52 - total - delayed-nu-fission - analog - - - 1 106 5 - total - delayed-nu-fission - analog - - - 1 2 - total - nu-fission - tracklength - - - 1 106 2 - total - delayed-nu-fission - tracklength - - 1 106 + 1 108 2 total delayed-nu-fission tracklength - 1 106 + 1 108 54 + total + delayed-nu-fission + analog + + + 1 108 5 + total + delayed-nu-fission + analog + + + 1 2 + total + nu-fission + tracklength + + + 1 108 2 + total + delayed-nu-fission + tracklength + + + 1 108 + total + delayed-nu-fission + tracklength + + + 1 108 total decay-rate tracklength - + 1 2 total flux analog - - 1 106 2 5 + + 1 108 2 5 total delayed-nu-fission analog - - 120 2 + + 122 2 total flux tracklength - - 120 2 + + 122 2 total total tracklength - - 120 2 + + 122 2 total flux tracklength - - 120 2 + + 122 2 total total tracklength - - 120 2 + + 122 2 total flux analog - - 120 5 6 + + 122 5 6 total scatter analog - - 120 2 + + 122 2 total flux tracklength - - 120 2 + + 122 2 total total tracklength - - 120 2 + + 122 2 total flux analog - - 120 5 6 + + 122 5 6 total nu-scatter analog - - 120 2 - total - flux - tracklength - - - 120 2 - total - absorption - tracklength - - - 120 2 - total - flux - tracklength - - - 120 2 - total - absorption - tracklength - - - 120 2 - total - fission - tracklength - - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - fission + absorption tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - nu-fission + absorption tracklength - 120 2 + 122 2 + total + (n,2n) + tracklength + + + 122 2 + total + (n,3n) + tracklength + + + 122 2 + total + (n,4n) + tracklength + + + 122 2 total flux tracklength - - 120 2 + + 122 2 + total + absorption + tracklength + + + 122 2 + total + fission + tracklength + + + 122 2 + total + flux + tracklength + + + 122 2 + total + fission + tracklength + + + 122 2 + total + flux + tracklength + + + 122 2 + total + nu-fission + tracklength + + + 122 2 + total + flux + tracklength + + + 122 2 total kappa-fission tracklength - - 120 2 - total - flux - tracklength - - - 120 2 - total - scatter - tracklength - - - 120 2 - total - flux - analog - - - 120 2 - total - nu-scatter - analog - - - 120 2 - total - flux - analog - - - 120 2 5 28 - total - scatter - analog - - - 120 2 - total - flux - analog - - - 120 2 5 28 - total - nu-scatter - analog - - - 120 2 5 - total - nu-scatter - analog - - - 120 2 5 - total - scatter - analog - - 120 2 + 122 2 total flux - analog + tracklength - 120 2 5 - total - nu-fission - analog - - - 120 2 5 + 122 2 total scatter + tracklength + + + 122 2 + total + flux analog - 120 2 + 122 2 total - flux - tracklength + nu-scatter + analog - 120 2 + 122 2 total - scatter - tracklength + flux + analog - 120 2 5 28 + 122 2 5 30 total scatter analog - 120 2 + 122 2 total flux - tracklength - - - 120 2 - total - scatter - tracklength - - - 120 2 5 28 - total - scatter analog - - 120 2 5 + + 122 2 5 30 total nu-scatter analog - - 120 52 + + 122 2 5 total - nu-fission + nu-scatter + analog + + + 122 2 5 + total + scatter + analog + + + 122 2 + total + flux analog - 120 5 + 122 2 5 total nu-fission analog - 120 52 + 122 2 5 total - prompt-nu-fission + scatter analog - 120 5 - total - prompt-nu-fission - analog - - - 120 2 + 122 2 total flux tracklength - - 120 2 + + 122 2 total - inverse-velocity + scatter tracklength + + 122 2 5 30 + total + scatter + analog + - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - prompt-nu-fission + scatter tracklength - 120 2 - total - flux - analog - - - 120 2 5 - total - prompt-nu-fission - analog - - - 120 2 - total - flux - tracklength - - - 120 2 - total - total - tracklength - - - 120 2 - total - flux - analog - - - 120 5 6 + 122 2 5 30 total scatter analog + + 122 2 5 + total + nu-scatter + analog + + + 122 54 + total + nu-fission + analog + + + 122 5 + total + nu-fission + analog + + + 122 54 + total + prompt-nu-fission + analog + + + 122 5 + total + prompt-nu-fission + analog + - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - total + inverse-velocity tracklength - 120 2 + 122 2 total flux - analog + tracklength - 120 5 6 + 122 2 total - nu-scatter - analog + prompt-nu-fission + tracklength - 120 2 + 122 2 total flux - tracklength + analog - 120 2 + 122 2 5 total - (n,elastic) - tracklength + prompt-nu-fission + analog - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,level) + total tracklength - 120 2 + 122 2 total flux - tracklength + analog - 120 2 + 122 5 6 total - (n,2n) - tracklength + scatter + analog - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,na) + total tracklength - 120 2 + 122 2 total flux - tracklength + analog - 120 2 + 122 5 6 total - (n,nc) - tracklength + nu-scatter + analog - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,gamma) + (n,elastic) tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,a) + (n,level) tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,Xa) + (n,2n) tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - heating + (n,na) tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - damage-energy + (n,nc) tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,n1) + (n,gamma) tracklength - 120 2 + 122 2 total flux tracklength - 120 2 + 122 2 total - (n,a0) + (n,a) tracklength - 120 2 + 122 2 total flux - analog + tracklength - 120 2 5 + 122 2 total - (n,nc) - analog + (n,Xa) + tracklength - 120 2 + 122 2 total flux - analog + tracklength - 120 2 5 + 122 2 total - (n,n1) - analog + heating + tracklength - 120 2 + 122 2 total flux - analog + tracklength - 120 2 5 + 122 2 total - (n,2n) - analog + damage-energy + tracklength - 120 2 + 122 2 total flux tracklength - 120 106 2 + 122 2 total - delayed-nu-fission + (n,n1) tracklength - 120 106 52 + 122 2 total - delayed-nu-fission - analog + flux + tracklength - 120 106 5 + 122 2 total - delayed-nu-fission - analog + (n,a0) + tracklength - 120 2 + 122 2 total - nu-fission - tracklength + flux + analog - 120 106 2 + 122 2 5 total - delayed-nu-fission - tracklength + (n,nc) + analog - 120 106 + 122 2 total - delayed-nu-fission - tracklength + flux + analog - 120 106 + 122 2 5 total - decay-rate - tracklength + (n,n1) + analog - 120 2 + 122 2 total flux analog - 120 106 2 5 + 122 2 5 total - delayed-nu-fission + (n,2n) analog - 239 2 + 122 2 total flux tracklength - 239 2 + 122 108 2 total - total + delayed-nu-fission tracklength - 239 2 + 122 108 54 total - flux - tracklength + delayed-nu-fission + analog - 239 2 + 122 108 5 total - total - tracklength + delayed-nu-fission + analog - 239 2 + 122 2 total - flux - analog + nu-fission + tracklength - 239 5 6 + 122 108 2 total - scatter - analog + delayed-nu-fission + tracklength - 239 2 + 122 108 total - flux + delayed-nu-fission tracklength - 239 2 + 122 108 total - total + decay-rate tracklength - 239 2 + 122 2 total flux analog - 239 5 6 + 122 108 2 5 total - nu-scatter + delayed-nu-fission analog - 239 2 + 243 2 total flux tracklength - 239 2 + 243 2 total - absorption + total tracklength - 239 2 + 243 2 total flux tracklength - 239 2 + 243 2 + total + total + tracklength + + + 243 2 + total + flux + analog + + + 243 5 6 + total + scatter + analog + + + 243 2 + total + flux + tracklength + + + 243 2 + total + total + tracklength + + + 243 2 + total + flux + analog + + + 243 5 6 + total + nu-scatter + analog + + + 243 2 + total + flux + tracklength + + + 243 2 total absorption tracklength - - 239 2 - total - fission - tracklength - - - 239 2 - total - flux - tracklength - - - 239 2 - total - fission - tracklength - - - 239 2 - total - flux - tracklength - - - 239 2 - total - nu-fission - tracklength - - - 239 2 - total - flux - tracklength - - - 239 2 - total - kappa-fission - tracklength - - - 239 2 - total - flux - tracklength - - 239 2 + 243 2 total - scatter + flux tracklength - 239 2 + 243 2 total - flux - analog + absorption + tracklength - 239 2 + 243 2 total - nu-scatter - analog + (n,2n) + tracklength - 239 2 + 243 2 total - flux - analog + (n,3n) + tracklength - 239 2 5 28 + 243 2 total - scatter - analog + (n,4n) + tracklength - 239 2 + 243 2 total flux - analog + tracklength - 239 2 5 28 + 243 2 total - nu-scatter - analog + absorption + tracklength - 239 2 5 + 243 2 total - nu-scatter - analog + fission + tracklength - 239 2 5 - total - scatter - analog - - - 239 2 + 243 2 total flux - analog + tracklength + + + 243 2 + total + fission + tracklength - 239 2 5 + 243 2 total - nu-fission - analog + flux + tracklength - 239 2 5 + 243 2 total - scatter - analog + nu-fission + tracklength - 239 2 + 243 2 total flux tracklength - 239 2 + 243 2 total - scatter + kappa-fission tracklength - 239 2 5 28 - total - scatter - analog - - - 239 2 + 243 2 total flux tracklength - - 239 2 + + 243 2 total scatter tracklength - - 239 2 5 28 + + 243 2 total - scatter + flux analog - - 239 2 5 + + 243 2 total nu-scatter analog - - 239 52 + + 243 2 total - nu-fission + flux + analog + + + 243 2 5 30 + total + scatter analog - 239 5 + 243 2 + total + flux + analog + + + 243 2 5 30 + total + nu-scatter + analog + + + 243 2 5 + total + nu-scatter + analog + + + 243 2 5 + total + scatter + analog + + + 243 2 + total + flux + analog + + + 243 2 5 total nu-fission analog - - 239 52 - total - prompt-nu-fission - analog - - - 239 5 - total - prompt-nu-fission - analog - - - 239 2 - total - flux - tracklength - - - 239 2 - total - inverse-velocity - tracklength - - - 239 2 - total - flux - tracklength - - 239 2 + 243 2 5 total - prompt-nu-fission - tracklength + scatter + analog - 239 2 - total - flux - analog - - - 239 2 5 - total - prompt-nu-fission - analog - - - 239 2 + 243 2 total flux tracklength - - 239 2 + + 243 2 total - total + scatter + tracklength + + + 243 2 5 30 + total + scatter + analog + + + 243 2 + total + flux tracklength - 239 2 + 243 2 total - flux - analog + scatter + tracklength - 239 5 6 + 243 2 5 30 total scatter analog - 239 2 - total - flux - tracklength - - - 239 2 - total - total - tracklength - - - 239 2 - total - flux - analog - - - 239 5 6 + 243 2 5 total nu-scatter analog + + 243 54 + total + nu-fission + analog + + + 243 5 + total + nu-fission + analog + + + 243 54 + total + prompt-nu-fission + analog + - 239 2 + 243 5 + total + prompt-nu-fission + analog + + + 243 2 total flux tracklength - - 239 2 + + 243 2 + total + inverse-velocity + tracklength + + + 243 2 + total + flux + tracklength + + + 243 2 + total + prompt-nu-fission + tracklength + + + 243 2 + total + flux + analog + + + 243 2 5 + total + prompt-nu-fission + analog + + + 243 2 + total + flux + tracklength + + + 243 2 + total + total + tracklength + + + 243 2 + total + flux + analog + + + 243 5 6 + total + scatter + analog + + + 243 2 + total + flux + tracklength + + + 243 2 + total + total + tracklength + + + 243 2 + total + flux + analog + + + 243 5 6 + total + nu-scatter + analog + + + 243 2 + total + flux + tracklength + + + 243 2 total (n,elastic) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,level) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,2n) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,na) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,nc) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,gamma) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,a) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,Xa) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total heating tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total damage-energy tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,n1) tracklength - - 239 2 + + 243 2 total flux tracklength - - 239 2 + + 243 2 total (n,a0) tracklength - - 239 2 + + 243 2 total flux analog - - 239 2 5 + + 243 2 5 total (n,nc) analog - - 239 2 + + 243 2 total flux analog - - 239 2 5 + + 243 2 5 total (n,n1) analog - - 239 2 + + 243 2 total flux analog - - 239 2 5 + + 243 2 5 total (n,2n) analog - - 239 2 + + 243 2 total flux tracklength - - 239 106 2 + + 243 108 2 total delayed-nu-fission tracklength - - 239 106 52 + + 243 108 54 total delayed-nu-fission analog - - 239 106 5 + + 243 108 5 total delayed-nu-fission analog - - 239 2 + + 243 2 total nu-fission tracklength - - 239 106 2 + + 243 108 2 total delayed-nu-fission tracklength - - 239 106 + + 243 108 total delayed-nu-fission tracklength - - 239 106 + + 243 108 total decay-rate tracklength - - 239 2 + + 243 2 total flux analog - - 239 106 2 5 + + 243 108 2 5 total delayed-nu-fission analog diff --git a/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat index 32ae7f4cf..dfbd183d4 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat @@ -14,6 +14,10 @@ absorption material group in nuclide mean std. dev. 1 1 1 total 0.027335 0.002038 0 1 2 total 0.263007 0.029616 +reduced absorption + material group in nuclide mean std. dev. +1 1 1 total 0.027278 0.002038 +0 1 2 total 0.263007 0.029616 capture material group in nuclide mean std. dev. 1 1 1 total 0.019722 0.001980 @@ -316,6 +320,10 @@ absorption material group in nuclide mean std. dev. 1 2 1 total 0.001395 0.000129 0 2 2 total 0.005202 0.000498 +reduced absorption + material group in nuclide mean std. dev. +1 2 1 total 0.001390 0.000129 +0 2 2 total 0.005202 0.000498 capture material group in nuclide mean std. dev. 1 2 1 total 0.001395 0.000129 @@ -618,6 +626,10 @@ absorption material group in nuclide mean std. dev. 1 3 1 total 0.000715 0.000032 0 3 2 total 0.031290 0.001882 +reduced absorption + material group in nuclide mean std. dev. +1 3 1 total 0.000715 0.000032 +0 3 2 total 0.031290 0.001882 capture material group in nuclide mean std. dev. 1 3 1 total 0.000715 0.000032 diff --git a/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat index f29a41f3d..e45fe389e 100644 --- a/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat @@ -62,16 +62,16 @@ 1 - + 3 - + 0.0 20000000.0 - + 2 - + 3 @@ -161,19 +161,19 @@ 1 2 U234 U235 U238 O16 - fission + (n,2n) tracklength 1 2 - total - flux + U234 U235 U238 O16 + (n,3n) tracklength 1 2 U234 U235 U238 O16 - fission + (n,4n) tracklength @@ -185,1493 +185,1583 @@ 1 2 U234 U235 U238 O16 - nu-fission + absorption tracklength + 1 2 + U234 U235 U238 O16 + fission + tracklength + + 1 2 total flux tracklength - + + 1 2 + U234 U235 U238 O16 + fission + tracklength + + + 1 2 + total + flux + tracklength + + + 1 2 + U234 U235 U238 O16 + nu-fission + tracklength + + + 1 2 + total + flux + tracklength + + 1 2 U234 U235 U238 O16 kappa-fission tracklength - - 1 2 - total - flux - tracklength - - - 1 2 - U234 U235 U238 O16 - scatter - tracklength - - - 1 2 - total - flux - analog - - - 1 2 - U234 U235 U238 O16 - nu-scatter - analog - - - 1 2 - total - flux - analog - - 1 2 5 28 - U234 U235 U238 O16 - scatter - analog + 1 2 + total + flux + tracklength + 1 2 + U234 U235 U238 O16 + scatter + tracklength + + 1 2 total flux analog - - 1 2 5 28 - U234 U235 U238 O16 - nu-scatter - analog - - 1 2 5 + 1 2 U234 U235 U238 O16 nu-scatter analog - 1 2 5 - U234 U235 U238 O16 - scatter + 1 2 + total + flux analog - 1 2 - total - flux + 1 2 5 30 + U234 U235 U238 O16 + scatter analog - 1 2 5 - U234 U235 U238 O16 - nu-fission + 1 2 + total + flux analog - 1 2 5 + 1 2 5 30 U234 U235 U238 O16 - scatter + nu-scatter analog - 1 2 - total - flux - tracklength - - - 1 2 - U234 U235 U238 O16 - scatter - tracklength - - - 1 2 5 28 - U234 U235 U238 O16 - scatter - analog - - - 1 2 - total - flux - tracklength - - - 1 2 - U234 U235 U238 O16 - scatter - tracklength - - - 1 2 5 28 - U234 U235 U238 O16 - scatter - analog - - 1 2 5 U234 U235 U238 O16 nu-scatter analog - - 1 52 + + 1 2 5 + U234 U235 U238 O16 + scatter + analog + + + 1 2 + total + flux + analog + + + 1 2 5 U234 U235 U238 O16 nu-fission analog - - 1 5 + + 1 2 5 U234 U235 U238 O16 - nu-fission + scatter analog - - 1 52 - U234 U235 U238 O16 - prompt-nu-fission - analog - - - 1 5 - U234 U235 U238 O16 - prompt-nu-fission - analog - - + 1 2 total flux tracklength + + 1 2 + U234 U235 U238 O16 + scatter + tracklength + + + 1 2 5 30 + U234 U235 U238 O16 + scatter + analog + + + 1 2 + total + flux + tracklength + + + 1 2 + U234 U235 U238 O16 + scatter + tracklength + + + 1 2 5 30 + U234 U235 U238 O16 + scatter + analog + + + 1 2 5 + U234 U235 U238 O16 + nu-scatter + analog + + 1 54 + U234 U235 U238 O16 + nu-fission + analog + + + 1 5 + U234 U235 U238 O16 + nu-fission + analog + + + 1 54 + U234 U235 U238 O16 + prompt-nu-fission + analog + + + 1 5 + U234 U235 U238 O16 + prompt-nu-fission + analog + + + 1 2 + total + flux + tracklength + + 1 2 U234 U235 U238 O16 inverse-velocity tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 prompt-nu-fission tracklength - + 1 2 total flux analog - + 1 2 5 U234 U235 U238 O16 prompt-nu-fission analog - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 total tracklength - + 1 2 total flux analog - + 1 5 6 U234 U235 U238 O16 scatter analog - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 total tracklength - + 1 2 total flux analog - + 1 5 6 U234 U235 U238 O16 nu-scatter analog - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,elastic) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,level) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,2n) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,na) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,nc) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,gamma) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,a) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,Xa) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 heating tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 damage-energy tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,n1) tracklength - + 1 2 total flux tracklength - + 1 2 U234 U235 U238 O16 (n,a0) tracklength - + 1 2 total flux analog - + 1 2 5 U234 U235 U238 O16 (n,nc) analog - + 1 2 total flux analog - + 1 2 5 U234 U235 U238 O16 (n,n1) analog - + 1 2 total flux analog - + 1 2 5 U234 U235 U238 O16 (n,2n) analog - - 104 2 + + 106 2 total flux tracklength - - 104 2 + + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 total tracklength - - 104 2 + + 106 2 total flux tracklength - - 104 2 + + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 total tracklength - - 104 2 + + 106 2 total flux analog - - 104 5 6 + + 106 5 6 Zr90 Zr91 Zr92 Zr94 Zr96 scatter analog - - 104 2 + + 106 2 total flux tracklength - - 104 2 + + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 total tracklength - - 104 2 + + 106 2 total flux analog - - 104 5 6 + + 106 5 6 Zr90 Zr91 Zr92 Zr94 Zr96 nu-scatter analog - - 104 2 - total - flux - tracklength - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - absorption - tracklength - - - 104 2 - total - flux - tracklength - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - absorption - tracklength - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - fission - tracklength - - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - fission + absorption tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - nu-fission + absorption tracklength - 104 2 + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,2n) + tracklength + + + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,3n) + tracklength + + + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,4n) + tracklength + + + 106 2 total flux tracklength - - 104 2 + + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + absorption + tracklength + + + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + fission + tracklength + + + 106 2 + total + flux + tracklength + + + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + fission + tracklength + + + 106 2 + total + flux + tracklength + + + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + nu-fission + tracklength + + + 106 2 + total + flux + tracklength + + + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 kappa-fission tracklength - - 104 2 - total - flux - tracklength - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - scatter - tracklength - - - 104 2 - total - flux - analog - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - nu-scatter - analog - - - 104 2 - total - flux - analog - - - 104 2 5 28 - Zr90 Zr91 Zr92 Zr94 Zr96 - scatter - analog - - - 104 2 - total - flux - analog - - - 104 2 5 28 - Zr90 Zr91 Zr92 Zr94 Zr96 - nu-scatter - analog - - - 104 2 5 - Zr90 Zr91 Zr92 Zr94 Zr96 - nu-scatter - analog - - - 104 2 5 - Zr90 Zr91 Zr92 Zr94 Zr96 - scatter - analog - - 104 2 + 106 2 total flux - analog + tracklength - 104 2 5 - Zr90 Zr91 Zr92 Zr94 Zr96 - nu-fission - analog - - - 104 2 5 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 scatter + tracklength + + + 106 2 + total + flux analog - 104 2 - total - flux - tracklength + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + nu-scatter + analog - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - scatter - tracklength + 106 2 + total + flux + analog - 104 2 5 28 + 106 2 5 30 Zr90 Zr91 Zr92 Zr94 Zr96 scatter analog - 104 2 + 106 2 total flux - tracklength - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - scatter - tracklength - - - 104 2 5 28 - Zr90 Zr91 Zr92 Zr94 Zr96 - scatter analog - - 104 2 5 + + 106 2 5 30 Zr90 Zr91 Zr92 Zr94 Zr96 nu-scatter analog - - 104 52 + + 106 2 5 Zr90 Zr91 Zr92 Zr94 Zr96 - nu-fission + nu-scatter + analog + + + 106 2 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + scatter + analog + + + 106 2 + total + flux analog - 104 5 + 106 2 5 Zr90 Zr91 Zr92 Zr94 Zr96 nu-fission analog - 104 52 + 106 2 5 Zr90 Zr91 Zr92 Zr94 Zr96 - prompt-nu-fission + scatter analog - 104 5 - Zr90 Zr91 Zr92 Zr94 Zr96 - prompt-nu-fission - analog - - - 104 2 + 106 2 total flux tracklength - - 104 2 + + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - inverse-velocity + scatter tracklength + + 106 2 5 30 + Zr90 Zr91 Zr92 Zr94 Zr96 + scatter + analog + - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - prompt-nu-fission + scatter tracklength - 104 2 - total - flux - analog - - - 104 2 5 - Zr90 Zr91 Zr92 Zr94 Zr96 - prompt-nu-fission - analog - - - 104 2 - total - flux - tracklength - - - 104 2 - Zr90 Zr91 Zr92 Zr94 Zr96 - total - tracklength - - - 104 2 - total - flux - analog - - - 104 5 6 + 106 2 5 30 Zr90 Zr91 Zr92 Zr94 Zr96 scatter analog + + 106 2 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + nu-scatter + analog + + + 106 54 + Zr90 Zr91 Zr92 Zr94 Zr96 + nu-fission + analog + + + 106 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + nu-fission + analog + + + 106 54 + Zr90 Zr91 Zr92 Zr94 Zr96 + prompt-nu-fission + analog + + + 106 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + prompt-nu-fission + analog + - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - total + inverse-velocity tracklength - 104 2 + 106 2 total flux - analog + tracklength - 104 5 6 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - nu-scatter - analog + prompt-nu-fission + tracklength - 104 2 + 106 2 total flux - tracklength + analog - 104 2 + 106 2 5 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,elastic) - tracklength + prompt-nu-fission + analog - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,level) + total tracklength - 104 2 + 106 2 total flux - tracklength + analog - 104 2 + 106 5 6 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,2n) - tracklength + scatter + analog - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,na) + total tracklength - 104 2 + 106 2 total flux - tracklength + analog - 104 2 + 106 5 6 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,nc) - tracklength + nu-scatter + analog - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,gamma) + (n,elastic) tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,a) + (n,level) tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,Xa) + (n,2n) tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - heating + (n,na) tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - damage-energy + (n,nc) tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,n1) + (n,gamma) tracklength - 104 2 + 106 2 total flux tracklength - 104 2 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,a0) + (n,a) tracklength - 104 2 + 106 2 total flux - analog + tracklength - 104 2 5 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,nc) - analog + (n,Xa) + tracklength - 104 2 + 106 2 total flux - analog + tracklength - 104 2 5 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,n1) - analog + heating + tracklength - 104 2 + 106 2 total flux - analog + tracklength - 104 2 5 + 106 2 Zr90 Zr91 Zr92 Zr94 Zr96 - (n,2n) - analog + damage-energy + tracklength - 207 2 + 106 2 total flux tracklength - 207 2 - H1 O16 B10 B11 - total + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,n1) tracklength - 207 2 + 106 2 total flux tracklength - 207 2 - H1 O16 B10 B11 - total + 106 2 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,a0) tracklength - 207 2 + 106 2 total flux analog - 207 5 6 - H1 O16 B10 B11 - scatter + 106 2 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,nc) analog - 207 2 + 106 2 total flux - tracklength + analog - 207 2 - H1 O16 B10 B11 - total - tracklength + 106 2 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,n1) + analog - 207 2 + 106 2 total flux analog - 207 5 6 - H1 O16 B10 B11 - nu-scatter + 106 2 5 + Zr90 Zr91 Zr92 Zr94 Zr96 + (n,2n) analog - 207 2 + 211 2 total flux tracklength - 207 2 + 211 2 H1 O16 B10 B11 - absorption + total tracklength - 207 2 + 211 2 total flux tracklength - 207 2 + 211 2 + H1 O16 B10 B11 + total + tracklength + + + 211 2 + total + flux + analog + + + 211 5 6 + H1 O16 B10 B11 + scatter + analog + + + 211 2 + total + flux + tracklength + + + 211 2 + H1 O16 B10 B11 + total + tracklength + + + 211 2 + total + flux + analog + + + 211 5 6 + H1 O16 B10 B11 + nu-scatter + analog + + + 211 2 + total + flux + tracklength + + + 211 2 H1 O16 B10 B11 absorption tracklength - - 207 2 - H1 O16 B10 B11 - fission - tracklength - - - 207 2 - total - flux - tracklength - - - 207 2 - H1 O16 B10 B11 - fission - tracklength - - - 207 2 - total - flux - tracklength - - - 207 2 - H1 O16 B10 B11 - nu-fission - tracklength - - - 207 2 - total - flux - tracklength - - - 207 2 - H1 O16 B10 B11 - kappa-fission - tracklength - - - 207 2 - total - flux - tracklength - - 207 2 - H1 O16 B10 B11 - scatter + 211 2 + total + flux tracklength - 207 2 - total - flux - analog + 211 2 + H1 O16 B10 B11 + absorption + tracklength - 207 2 + 211 2 H1 O16 B10 B11 - nu-scatter - analog + (n,2n) + tracklength - 207 2 - total - flux - analog + 211 2 + H1 O16 B10 B11 + (n,3n) + tracklength - 207 2 5 28 + 211 2 H1 O16 B10 B11 - scatter - analog + (n,4n) + tracklength - 207 2 + 211 2 total flux - analog + tracklength - 207 2 5 28 + 211 2 H1 O16 B10 B11 - nu-scatter - analog + absorption + tracklength - 207 2 5 + 211 2 H1 O16 B10 B11 - nu-scatter - analog + fission + tracklength - 207 2 5 - H1 O16 B10 B11 - scatter - analog - - - 207 2 + 211 2 total flux - analog + tracklength + + + 211 2 + H1 O16 B10 B11 + fission + tracklength - 207 2 5 - H1 O16 B10 B11 - nu-fission - analog + 211 2 + total + flux + tracklength - 207 2 5 + 211 2 H1 O16 B10 B11 - scatter - analog + nu-fission + tracklength - 207 2 + 211 2 total flux tracklength - 207 2 + 211 2 H1 O16 B10 B11 - scatter + kappa-fission tracklength - 207 2 5 28 - H1 O16 B10 B11 - scatter - analog - - - 207 2 + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 scatter tracklength - - 207 2 5 28 - H1 O16 B10 B11 - scatter + + 211 2 + total + flux analog - - 207 2 5 + + 211 2 H1 O16 B10 B11 nu-scatter analog + + 211 2 + total + flux + analog + - 207 52 + 211 2 5 30 H1 O16 B10 B11 - nu-fission + scatter analog - 207 5 + 211 2 + total + flux + analog + + + 211 2 5 30 + H1 O16 B10 B11 + nu-scatter + analog + + + 211 2 5 + H1 O16 B10 B11 + nu-scatter + analog + + + 211 2 5 + H1 O16 B10 B11 + scatter + analog + + + 211 2 + total + flux + analog + + + 211 2 5 H1 O16 B10 B11 nu-fission analog - - 207 52 - H1 O16 B10 B11 - prompt-nu-fission - analog - - - 207 5 - H1 O16 B10 B11 - prompt-nu-fission - analog - - - 207 2 - total - flux - tracklength - - - 207 2 - H1 O16 B10 B11 - inverse-velocity - tracklength - - - 207 2 - total - flux - tracklength - - 207 2 + 211 2 5 H1 O16 B10 B11 - prompt-nu-fission - tracklength + scatter + analog - 207 2 - total - flux - analog - - - 207 2 5 - H1 O16 B10 B11 - prompt-nu-fission - analog - - - 207 2 + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 - total + scatter + tracklength + + + 211 2 5 30 + H1 O16 B10 B11 + scatter + analog + + + 211 2 + total + flux tracklength - 207 2 - total - flux - analog + 211 2 + H1 O16 B10 B11 + scatter + tracklength - 207 5 6 + 211 2 5 30 H1 O16 B10 B11 scatter analog - 207 2 - total - flux - tracklength - - - 207 2 - H1 O16 B10 B11 - total - tracklength - - - 207 2 - total - flux - analog - - - 207 5 6 + 211 2 5 H1 O16 B10 B11 nu-scatter analog + + 211 54 + H1 O16 B10 B11 + nu-fission + analog + + + 211 5 + H1 O16 B10 B11 + nu-fission + analog + + + 211 54 + H1 O16 B10 B11 + prompt-nu-fission + analog + - 207 2 + 211 5 + H1 O16 B10 B11 + prompt-nu-fission + analog + + + 211 2 total flux tracklength - - 207 2 + + 211 2 + H1 O16 B10 B11 + inverse-velocity + tracklength + + + 211 2 + total + flux + tracklength + + + 211 2 + H1 O16 B10 B11 + prompt-nu-fission + tracklength + + + 211 2 + total + flux + analog + + + 211 2 5 + H1 O16 B10 B11 + prompt-nu-fission + analog + + + 211 2 + total + flux + tracklength + + + 211 2 + H1 O16 B10 B11 + total + tracklength + + + 211 2 + total + flux + analog + + + 211 5 6 + H1 O16 B10 B11 + scatter + analog + + + 211 2 + total + flux + tracklength + + + 211 2 + H1 O16 B10 B11 + total + tracklength + + + 211 2 + total + flux + analog + + + 211 5 6 + H1 O16 B10 B11 + nu-scatter + analog + + + 211 2 + total + flux + tracklength + + + 211 2 H1 O16 B10 B11 (n,elastic) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,level) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,2n) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,na) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,nc) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,gamma) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,a) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,Xa) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 heating tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 damage-energy tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,n1) tracklength - - 207 2 + + 211 2 total flux tracklength - - 207 2 + + 211 2 H1 O16 B10 B11 (n,a0) tracklength - - 207 2 + + 211 2 total flux analog - - 207 2 5 + + 211 2 5 H1 O16 B10 B11 (n,nc) analog - - 207 2 + + 211 2 total flux analog - - 207 2 5 + + 211 2 5 H1 O16 B10 B11 (n,n1) analog - - 207 2 + + 211 2 total flux analog - - 207 2 5 + + 211 2 5 H1 O16 B10 B11 (n,2n) analog diff --git a/tests/regression_tests/mgxs_library_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_nuclides/results_true.dat index c0084547b..7825aba3f 100644 --- a/tests/regression_tests/mgxs_library_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_nuclides/results_true.dat @@ -1 +1 @@ -d36f8abb1131212063470d622dbedeae31602da71006389421bd5dba712c94fe96acbc8ded833cb237687fb1071c1a6c3e0ec67b18ce7cf0f7720451b86d993a \ No newline at end of file +93ad567f1b36461a68d4ead0ff5cfa4a2003b05cf5241a232544545001a94a33fc7b99f21af277ea3a24861d38aac3a9ac36c8b1706c4b3b33caec589df2c90c \ No newline at end of file diff --git a/tests/regression_tests/microxs/__init.py__ b/tests/regression_tests/microxs/__init.py__ new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py new file mode 100644 index 000000000..dbfe3357b --- /dev/null +++ b/tests/regression_tests/microxs/test.py @@ -0,0 +1,52 @@ +"""Test one-group cross section generation""" +from pathlib import Path + +import numpy as np +import pytest +import openmc + +from openmc.deplete import MicroXS + +CHAIN_FILE = Path(__file__).parents[2] / "chain_simple.xml" + +@pytest.fixture(scope="module") +def model(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + + clad = openmc.Material(name="clad") + clad.add_element("Zr", 1) + clad.set_density("g/cc", 6) + + water = openmc.Material(name="water") + water.add_element("O", 1) + water.add_element("H", 2) + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + + radii = [0.42, 0.45] + fuel.volume = np.pi * radii[0] ** 2 + + materials = openmc.Materials([fuel, clad, water]) + + pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] + pin_univ = openmc.model.pin(pin_surfaces, materials) + bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + geometry = openmc.Geometry([root_cell]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + + +def test_from_model(model): + ref_xs = MicroXS.from_csv('test_reference.csv') + test_xs = MicroXS.from_model(model, model.materials[0], CHAIN_FILE) + + np.testing.assert_allclose(test_xs, ref_xs, rtol=1e-11) diff --git a/tests/regression_tests/microxs/test_reference.csv b/tests/regression_tests/microxs/test_reference.csv new file mode 100644 index 000000000..787ce74c3 --- /dev/null +++ b/tests/regression_tests/microxs/test_reference.csv @@ -0,0 +1,13 @@ +nuclide,"(n,gamma)",fission +U234,22.231989822002465,0.49620744663749855 +U235,10.479008971197121,48.41787337164604 +U238,0.8673334105437324,0.10467880588762352 +U236,8.65171044607122,0.31948392400019293 +O16,7.497851000107524e-05,0.0 +O17,0.0004079227797153371,0.0 +I135,6.842395323713927,0.0 +Xe135,227463.86426990604,0.0 +Xe136,0.02317896034753588,0.0 +Cs135,2.1721665580713623,0.0 +Gd157,12786.09939237018,0.0 +Gd156,3.4006085445846983,0.0 diff --git a/tests/regression_tests/torus/inputs_true.dat b/tests/regression_tests/torus/inputs_true.dat index 0045d7e46..0ccbda400 100644 --- a/tests/regression_tests/torus/inputs_true.dat +++ b/tests/regression_tests/torus/inputs_true.dat @@ -3,7 +3,7 @@ - + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true.dat b/tests/regression_tests/unstructured_mesh/inputs_true.dat new file mode 100644 index 000000000..e556782ad --- /dev/null +++ b/tests/regression_tests/unstructured_mesh/inputs_true.dat @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixed source + 1000 + 10 + + + + + 1.0 1.0 + + + 0.0 1.0 + + + + 15000000.0 1.0 + + + + + + + 10 10 10 + -10.0 -10.0 -10.0 + 10.0 10.0 10.0 + + + test_mesh_hexes.e + + + 1 + + + 2 + + + 1 + flux + collision + + + 2 + flux + collision + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true0.dat b/tests/regression_tests/unstructured_mesh/inputs_true0.dat index 1ed60d507..2e2594cde 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true0.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true0.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true1.dat b/tests/regression_tests/unstructured_mesh/inputs_true1.dat index 67dde56a9..84a3b186b 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true1.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true1.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true10.dat b/tests/regression_tests/unstructured_mesh/inputs_true10.dat index 9ca47a356..e9272a333 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true10.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true10.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true11.dat b/tests/regression_tests/unstructured_mesh/inputs_true11.dat index 683e1fade..0b89d280c 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true11.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true11.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true12.dat b/tests/regression_tests/unstructured_mesh/inputs_true12.dat index 8ce9f3cf2..b3673a254 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true12.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true12.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true13.dat b/tests/regression_tests/unstructured_mesh/inputs_true13.dat index bca0bee4c..c466396b6 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true13.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true13.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true14.dat b/tests/regression_tests/unstructured_mesh/inputs_true14.dat index 226331ba8..8c6a5120a 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true14.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true14.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true15.dat b/tests/regression_tests/unstructured_mesh/inputs_true15.dat index a6a084165..1b9abbd83 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true15.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true15.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true2.dat b/tests/regression_tests/unstructured_mesh/inputs_true2.dat index 4b442c757..7136d485a 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true2.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true2.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true3.dat b/tests/regression_tests/unstructured_mesh/inputs_true3.dat index 52e53498e..23900a060 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true3.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true3.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true8.dat b/tests/regression_tests/unstructured_mesh/inputs_true8.dat index e484c95a2..a0cbbcae7 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true8.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true8.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/inputs_true9.dat b/tests/regression_tests/unstructured_mesh/inputs_true9.dat index 5e83d71bd..930bd8579 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true9.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true9.dat @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/tests/regression_tests/unstructured_mesh/test.py b/tests/regression_tests/unstructured_mesh/test.py index daebe7948..9ed36c69c 100644 --- a/tests/regression_tests/unstructured_mesh/test.py +++ b/tests/regression_tests/unstructured_mesh/test.py @@ -1,6 +1,8 @@ +import filecmp import glob from itertools import product import os +import warnings import openmc import openmc.lib @@ -9,18 +11,52 @@ import numpy as np import pytest from tests.testing_harness import PyAPITestHarness -TETS_PER_VOXEL = 12 - class UnstructuredMeshTest(PyAPITestHarness): - def __init__(self, statepoint_name, model, inputs_true, holes): + ELEM_PER_VOXEL = 12 + + def __init__(self, + statepoint_name, + model, + inputs_true='inputs_true.dat', + holes=False, + scale_factor=10.0): super().__init__(statepoint_name, model, inputs_true) self.holes = holes # holes in the test mesh + self.scale_bounding_cell(scale_factor) + + def scale_bounding_cell(self, scale_factor): + geometry = self._model.geometry + for surface in geometry.get_all_surfaces().values(): + if surface.boundary_type != 'vacuum': + continue + for coeff in surface._coefficients: + surface._coefficients[coeff] *= scale_factor def _compare_results(self): with openmc.StatePoint(self._sp_name) as sp: + # check some properties of the unstructured mesh + umesh = None + for m in sp.meshes.values(): + if isinstance(m, openmc.UnstructuredMesh): + umesh = m + assert umesh is not None + + # check that the first element centroid is correct + # this will depend on whether the tet mesh or hex mesh + # file is being used in this test + if umesh.element_types[0] == umesh._LINEAR_TET: + exp_vertex = (-10.0, -10.0, -10.0) + exp_centroid = (-8.75, -9.75, -9.25) + else: + exp_vertex = (-10.0, -10.0, 10.0) + exp_centroid = (-9.0, -9.0, 9.0) + + np.testing.assert_array_equal(umesh.vertices[0], exp_vertex) + np.testing.assert_array_equal(umesh.centroid(0), exp_centroid) + # loop over the tallies and get data for tally in sp.tallies.values(): # find the regular and unstructured meshes @@ -28,32 +64,27 @@ class UnstructuredMeshTest(PyAPITestHarness): flt = tally.find_filter(openmc.MeshFilter) if isinstance(flt.mesh, openmc.RegularMesh): - reg_mesh_data, reg_mesh_std_dev = self.get_mesh_tally_data(tally) + reg_mesh_data = self.get_mesh_tally_data(tally) if self.holes: reg_mesh_data = np.delete(reg_mesh_data, self.holes) - reg_mesh_std_dev = np.delete(reg_mesh_std_dev, self.holes) else: umesh_tally = tally - unstructured_data, unstructured_std_dev = self.get_mesh_tally_data(tally, True) + unstructured_data = self.get_mesh_tally_data(tally, True) - # we expect these results to be the same to within at least ten - # decimal places - decimals = 10 if umesh_tally.estimator == 'collision' else 8 - np.testing.assert_array_almost_equal(unstructured_data, - reg_mesh_data, - decimals) + # we expect these results to be the same to within at least ten + # decimal places + decimals = 10 if umesh_tally.estimator == 'collision' else 8 + np.testing.assert_array_almost_equal(np.sort(unstructured_data), + np.sort(reg_mesh_data), + decimals) - @staticmethod - def get_mesh_tally_data(tally, structured=False): + def get_mesh_tally_data(self, tally, structured=False): data = tally.get_reshaped_data(value='mean') - std_dev = tally.get_reshaped_data(value='std_dev') if structured: - data.shape = (data.size // TETS_PER_VOXEL, TETS_PER_VOXEL) - std_dev.shape = (std_dev.size // TETS_PER_VOXEL, TETS_PER_VOXEL) + data = data.reshape((-1, self.ELEM_PER_VOXEL)) else: data.shape = (data.size, 1) - std_dev.shape = (std_dev.size, 1) - return np.sum(data, axis=1), np.sum(std_dev, axis=1) + return np.sum(data, axis=1) def _cleanup(self): super()._cleanup() @@ -64,35 +95,11 @@ class UnstructuredMeshTest(PyAPITestHarness): os.remove(f) -param_values = (['libmesh', 'moab'], # mesh libraries - ['collision', 'tracklength'], # estimators - [True, False], # geometry outside of the mesh - [(333, 90, 77), None]) # location of holes in the mesh -test_cases = [] -for i, (lib, estimator, ext_geom, holes) in enumerate(product(*param_values)): - test_cases.append({'library' : lib, - 'estimator' : estimator, - 'external_geom' : ext_geom, - 'holes' : holes, - 'inputs_true' : 'inputs_true{}.dat'.format(i)}) - - -@pytest.mark.parametrize("test_opts", test_cases) -def test_unstructured_mesh(test_opts): - +@pytest.fixture +def model(): openmc.reset_auto_ids() - # skip the test if the library is not enabled - if test_opts['library'] == 'moab' and not openmc.lib._dagmc_enabled(): - pytest.skip("DAGMC (and MOAB) mesh not enbaled in this build.") - - if test_opts['library'] == 'libmesh' and not openmc.lib._libmesh_enabled(): - pytest.skip("LibMesh is not enabled in this build.") - - # skip the tracklength test for libmesh - if test_opts['library'] == 'libmesh' and \ - test_opts['estimator'] == 'tracklength': - pytest.skip("Tracklength tallies are not supported using libmesh.") + model = openmc.Model() ### Materials ### materials = openmc.Materials() @@ -113,7 +120,7 @@ def test_unstructured_mesh(test_opts): water_mat.set_density("atom/b-cm", 0.07416) materials.append(water_mat) - materials.export_to_xml() + model.materials = materials ### Geometry ### fuel_min_x = openmc.XPlane(-5.0, name="minimum x") @@ -149,29 +156,26 @@ def test_unstructured_mesh(test_opts): +clad_min_z & -clad_max_z) clad_cell.fill = zirc_mat - if test_opts['external_geom']: - bounds = (15, 15, 15) - else: - bounds = (10, 10, 10) - - water_min_x = openmc.XPlane(x0=-bounds[0], + # set bounding cell dimension to one + # this will be updated later according to the test case parameters + water_min_x = openmc.XPlane(x0=-1.0, name="minimum x", boundary_type='vacuum') - water_max_x = openmc.XPlane(x0=bounds[0], + water_max_x = openmc.XPlane(x0=1.0, name="maximum x", boundary_type='vacuum') - water_min_y = openmc.YPlane(y0=-bounds[1], + water_min_y = openmc.YPlane(y0=-1.0, name="minimum y", boundary_type='vacuum') - water_max_y = openmc.YPlane(y0=bounds[1], + water_max_y = openmc.YPlane(y0=1.0, name="maximum y", boundary_type='vacuum') - water_min_z = openmc.ZPlane(z0=-bounds[2], + water_min_z = openmc.ZPlane(z0=-1.0, name="minimum z", boundary_type='vacuum') - water_max_z = openmc.ZPlane(z0=bounds[2], + water_max_z = openmc.ZPlane(z0=1.0, name="maximum z", boundary_type='vacuum') @@ -185,9 +189,9 @@ def test_unstructured_mesh(test_opts): water_cell.fill = water_mat # create a containing universe - geometry = openmc.Geometry([fuel_cell, clad_cell, water_cell]) + model.geometry = openmc.Geometry([fuel_cell, clad_cell, water_cell]) - ### Tallies ### + ### Reference Tally ### # create meshes and mesh filters regular_mesh = openmc.RegularMesh() @@ -196,29 +200,11 @@ def test_unstructured_mesh(test_opts): regular_mesh.upper_right = (10.0, 10.0, 10.0) regular_mesh_filter = openmc.MeshFilter(mesh=regular_mesh) - - if test_opts['holes']: - mesh_filename = "test_mesh_tets_w_holes.e" - else: - mesh_filename = "test_mesh_tets.e" - - uscd_mesh = openmc.UnstructuredMesh(mesh_filename, test_opts['library']) - uscd_filter = openmc.MeshFilter(mesh=uscd_mesh) - - # create tallies - tallies = openmc.Tallies() - regular_mesh_tally = openmc.Tally(name="regular mesh tally") regular_mesh_tally.filters = [regular_mesh_filter] regular_mesh_tally.scores = ['flux'] - regular_mesh_tally.estimator = test_opts['estimator'] - tallies.append(regular_mesh_tally) - uscd_tally = openmc.Tally(name="unstructured mesh tally") - uscd_tally.filters = [uscd_filter] - uscd_tally.scores = ['flux'] - uscd_tally.estimator = test_opts['estimator'] - tallies.append(uscd_tally) + model.tallies = openmc.Tallies([regular_mesh_tally]) ### Settings ### settings = openmc.Settings() @@ -236,13 +222,92 @@ def test_unstructured_mesh(test_opts): source = openmc.Source(space=space, energy=energy) settings.source = source - model = openmc.model.Model(geometry=geometry, - materials=materials, - tallies=tallies, - settings=settings) + model.settings = settings + + return model + + +param_values = (['libmesh', 'moab'], # mesh libraries + ['collision', 'tracklength'], # estimators + [True, False], # geometry outside of the mesh + [(333, 90, 77), None]) # location of holes in the mesh +test_cases = [] +for i, (lib, estimator, ext_geom, holes) in enumerate(product(*param_values)): + test_cases.append({'library' : lib, + 'estimator' : estimator, + 'external_geom' : ext_geom, + 'holes' : holes, + 'inputs_true' : 'inputs_true{}.dat'.format(i)}) + + +@pytest.mark.parametrize("test_opts", test_cases) +def test_unstructured_mesh_tets(model, test_opts): + # skip the test if the library is not enabled + if test_opts['library'] == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip("DAGMC (and MOAB) mesh not enbaled in this build.") + + if test_opts['library'] == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip("LibMesh is not enabled in this build.") + + # skip the tracklength test for libmesh + if test_opts['library'] == 'libmesh' and \ + test_opts['estimator'] == 'tracklength': + pytest.skip("Tracklength tallies are not supported using libmesh.") + + if test_opts['holes']: + mesh_filename = "test_mesh_tets_w_holes.e" + else: + mesh_filename = "test_mesh_tets.e" + + # add reference mesh tally + regular_mesh_tally = model.tallies[0] + regular_mesh_tally.estimator = test_opts['estimator'] + + # add analagous unstructured mesh tally + uscd_mesh = openmc.UnstructuredMesh(mesh_filename, test_opts['library']) + uscd_filter = openmc.MeshFilter(mesh=uscd_mesh) + + # create tallies + uscd_tally = openmc.Tally(name="unstructured mesh tally") + uscd_tally.filters = [uscd_filter] + uscd_tally.scores = ['flux'] + uscd_tally.estimator = test_opts['estimator'] + model.tallies.append(uscd_tally) + + # modify model geometry according to test opts + if test_opts['external_geom']: + scale_factor = 15.0 + else: + scale_factor = 10.0 harness = UnstructuredMeshTest('statepoint.10.h5', model, test_opts['inputs_true'], - test_opts['holes']) + test_opts['holes'], + scale_factor) harness.main() + + +@pytest.mark.skipif(not openmc.lib._libmesh_enabled(), + reason='LibMesh is not enabled in this build.') +def test_unstructured_mesh_hexes(model): + regular_mesh_tally = model.tallies[0] + regular_mesh_tally.estimator = 'collision' + + # add analagous unstructured mesh tally + uscd_mesh = openmc.UnstructuredMesh('test_mesh_hexes.e', 'libmesh') + uscd_filter = openmc.MeshFilter(mesh=uscd_mesh) + + # create tallies + uscd_tally = openmc.Tally(name="unstructured mesh tally") + uscd_tally.filters = [uscd_filter] + uscd_tally.scores = ['flux'] + uscd_tally.estimator = 'collision' + model.tallies.append(uscd_tally) + + harness = UnstructuredMeshTest('statepoint.10.h5', + model) + harness.ELEM_PER_VOXEL = 1 + + harness.main() + diff --git a/tests/regression_tests/unstructured_mesh/test_mesh_hexes.e b/tests/regression_tests/unstructured_mesh/test_mesh_hexes.e new file mode 120000 index 000000000..421bf6a89 --- /dev/null +++ b/tests/regression_tests/unstructured_mesh/test_mesh_hexes.e @@ -0,0 +1 @@ +test_mesh_hexes.exo \ No newline at end of file diff --git a/tests/regression_tests/unstructured_mesh/test_mesh_hexes.exo b/tests/regression_tests/unstructured_mesh/test_mesh_hexes.exo new file mode 100644 index 000000000..03682c275 Binary files /dev/null and b/tests/regression_tests/unstructured_mesh/test_mesh_hexes.exo differ diff --git a/tests/unit_tests/dagmc/test_bounds.py b/tests/unit_tests/dagmc/test_bounds.py new file mode 100644 index 000000000..dbca3ca25 --- /dev/null +++ b/tests/unit_tests/dagmc/test_bounds.py @@ -0,0 +1,73 @@ +import openmc +import pytest +from pathlib import Path + + +def test_bounding_box(request): + """Checks that the DAGMCUniverse.bounding_box returns the correct values""" + + u = openmc.DAGMCUniverse(Path(request.fspath).parent / "dagmc.h5m") + + ll, ur = u.bounding_box + assert ll == pytest.approx((-25.0, -25.0, -25)) + assert ur == pytest.approx((25.0, 25.0, 25)) + + +def test_bounding_region(request): + """Checks that the DAGMCUniverse.bounding_region() returns a region with + correct surfaces and boundary types""" + + u = openmc.DAGMCUniverse(Path(request.fspath).parent / "dagmc.h5m") + + region = u.bounding_region() # should default to bounded_type='box' + assert isinstance(region, openmc.Region) + assert len(region) == 6 + assert region[0].surface.type == "x-plane" + assert region[1].surface.type == "x-plane" + assert region[2].surface.type == "y-plane" + assert region[3].surface.type == "y-plane" + assert region[4].surface.type == "z-plane" + assert region[5].surface.type == "z-plane" + assert region[0].surface.boundary_type == "vacuum" + assert region[1].surface.boundary_type == "vacuum" + assert region[2].surface.boundary_type == "vacuum" + assert region[3].surface.boundary_type == "vacuum" + assert region[4].surface.boundary_type == "vacuum" + assert region[5].surface.boundary_type == "vacuum" + + region = u.bounding_region(bounded_type="sphere", boundary_type="reflective") + assert isinstance(region, openmc.Region) + assert isinstance(region, openmc.Halfspace) + assert region.surface.type == "sphere" + assert region.surface.boundary_type == "reflective" + + +def test_bounded_universe(request): + """Checks that the DAGMCUniverse.bounded_universe() returns a + openmc.Universe with correct surface ids and cell ids""" + + u = openmc.DAGMCUniverse(Path(request.fspath).parent / "dagmc.h5m") + + # bounded with defaults + bu = u.bounded_universe() + + cells = list(bu.get_all_cells().items()) + assert isinstance(bu, openmc.Universe) + assert len(cells) == 1 + assert cells[0][0] == 10000 # default bounding_cell_id is 10000 + assert cells[0][1].id == 10000 # default bounding_cell_id is 10000 + surfaces = list(cells[0][1].region.get_surfaces().items()) + assert len(surfaces) == 6 + assert surfaces[0][1].id == 10000 + + # bounded with non defaults + bu = u.bounded_universe(bounding_cell_id=42, bounded_type="sphere", starting_id=43) + + cells = list(bu.get_all_cells().items()) + assert isinstance(bu, openmc.Universe) + assert len(cells) == 1 + assert cells[0][0] == 42 # default bounding_cell_id is 10000 + assert cells[0][1].id == 42 # default bounding_cell_id is 10000 + surfaces = list(cells[0][1].region.get_surfaces().items()) + assert surfaces[0][1].type == "sphere" + assert surfaces[0][1].id == 43 diff --git a/tests/unit_tests/mesh_to_vtk/__init__.py b/tests/unit_tests/mesh_to_vtk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/mesh_to_vtk/hexes.exo b/tests/unit_tests/mesh_to_vtk/hexes.exo new file mode 100644 index 000000000..c16138e90 Binary files /dev/null and b/tests/unit_tests/mesh_to_vtk/hexes.exo differ diff --git a/tests/unit_tests/mesh_to_vtk/libmesh_hexes_ref.vtk b/tests/unit_tests/mesh_to_vtk/libmesh_hexes_ref.vtk new file mode 100644 index 000000000..041d7065d --- /dev/null +++ b/tests/unit_tests/mesh_to_vtk/libmesh_hexes_ref.vtk @@ -0,0 +1,76 @@ +# vtk DataFile Version 5.1 +vtk output +ASCII +DATASET UNSTRUCTURED_GRID +POINTS 54 double +-1 -1 2.5 -1 -1 1.5 -1 0 1.5 +-1 0 2.5 0 -1 2.5 0 -1 1.5 +0 0 1.5 0 0 2.5 -1 -1 0.5 +-1 0 0.5 0 -1 0.5 0 0 0.5 +-1 -1 -0.5 -1 0 -0.5 0 -1 -0.5 +0 0 -0.5 -1 -1 -1.5 -1 0 -1.5 +0 -1 -1.5 0 0 -1.5 -1 -1 -2.5 +-1 0 -2.5 0 -1 -2.5 0 0 -2.5 +-1 1 1.5 -1 1 2.5 0 1 1.5 +0 1 2.5 -1 1 0.5 0 1 0.5 +-1 1 -0.5 0 1 -0.5 -1 1 -1.5 +0 1 -1.5 -1 1 -2.5 0 1 -2.5 +1 -1 2.5 1 -1 1.5 1 0 1.5 +1 0 2.5 1 -1 0.5 1 0 0.5 +1 -1 -0.5 1 0 -0.5 1 -1 -1.5 +1 0 -1.5 1 -1 -2.5 1 0 -2.5 +1 1 1.5 1 1 2.5 1 1 0.5 +1 1 -0.5 1 1 -1.5 1 1 -2.5 + +CELLS 21 160 +OFFSETS vtktypeint64 +0 8 16 24 32 40 48 56 64 +72 80 88 96 104 112 120 128 136 +144 152 160 +CONNECTIVITY vtktypeint64 +0 1 2 3 4 5 6 7 1 +8 9 2 5 10 11 6 8 12 +13 9 10 14 15 11 12 16 17 +13 14 18 19 15 16 20 21 17 +18 22 23 19 3 2 24 25 7 +6 26 27 2 9 28 24 6 11 +29 26 9 13 30 28 11 15 31 +29 13 17 32 30 15 19 33 31 +17 21 34 32 19 23 35 33 4 +5 6 7 36 37 38 39 5 10 +11 6 37 40 41 38 10 14 15 +11 40 42 43 41 14 18 19 15 +42 44 45 43 18 22 23 19 44 +46 47 45 7 6 26 27 39 38 +48 49 6 11 29 26 38 41 50 +48 11 15 31 29 41 43 51 50 +15 19 33 31 43 45 52 51 19 +23 35 33 45 47 53 52 +CELL_TYPES 20 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 +12 + +CELL_DATA 20 +FIELD FieldData 1 +ids 1 20 double +0 1 2 3 4 5 6 7 8 +9 10 11 12 13 14 15 16 17 +18 19 diff --git a/tests/unit_tests/mesh_to_vtk/libmesh_tets_ref.vtk b/tests/unit_tests/mesh_to_vtk/libmesh_tets_ref.vtk new file mode 100644 index 000000000..90f226121 --- /dev/null +++ b/tests/unit_tests/mesh_to_vtk/libmesh_tets_ref.vtk @@ -0,0 +1,292 @@ +# vtk DataFile Version 5.1 +vtk output +ASCII +DATASET UNSTRUCTURED_GRID +POINTS 58 double +-0.02593964576 -1 -1.1195739536 -0.40239958217 -0.40962166746 -1.9256035383 -1 0.02593964576 -1.1195739536 +-0.2248833639 0.23189144433 -1.2841109811 0.02593964576 1 -1.1195739536 1 -0.02593964576 -1.1195739536 +-0.042928712675 0.066425810853 -0.54270728017 -1 1 -1.5 -1 1 -0.5 +-0.49265312381 0.49332495053 -1.9656359509 -0.02608137991 -1 1.121521147 0.30281888252 -0.30293622513 1.8067963894 +1 -0.02608137991 1.121521147 -0.23236342389 0.26314805583 1.2867556783 0.074337770194 0.074337770194 2.5 +-0.40005770996 -0.36909660956 1.8768871762 -0.4896743493 0.49451064621 1.952829049 -1 1 1.5 +0.02608137991 1 1.121521147 -1 1 0.5 -0.038567136385 0.14628887118 0.55111724874 +-1 0.02608137991 1.121521147 0 -1 -2.5 0.37543954218 -0.36967061971 -1.8691105401 +1 -1 -2.5 0.074337770194 -0.074337770194 -2.5 1 0 -2.5 +0.4135797166 0.38351746262 -1.9274856063 1 -1 -1.5 -1 0 -2.5 +-1 -1 -1.5 -1 -1 -2.5 -1 0 2.5 +-1 1 2.5 0 1 2.5 0.46519182209 0.49356920409 1.95286052 +0 -1 2.5 -1 -1 1.5 -1 -1 2.5 +1 -1 2.5 1 -1 1.5 1 0 2.5 +1 1 2.5 1 1 1.5 0 1 -2.5 +-1 1 -2.5 1 -1 -0.5 1 1 -0.5 +0.038669824604 1 0.0046021677516 -1 -1 0.5 -0.038669824604 -1 0.0046021677516 +1 1 0.5 -1 0.038669824604 0.0046021677516 1 -0.038669824604 0.0046021677516 +1 1 -2.5 1 1 -1.5 1 -1 0.5 +-1 -1 -0.5 +CELLS 155 616 +OFFSETS vtktypeint64 +0 4 8 12 16 20 24 28 32 +36 40 44 48 52 56 60 64 68 +72 76 80 84 88 92 96 100 104 +108 112 116 120 124 128 132 136 140 +144 148 152 156 160 164 168 172 176 +180 184 188 192 196 200 204 208 212 +216 220 224 228 232 236 240 244 248 +252 256 260 264 268 272 276 280 284 +288 292 296 300 304 308 312 316 320 +324 328 332 336 340 344 348 352 356 +360 364 368 372 376 380 384 388 392 +396 400 404 408 412 416 420 424 428 +432 436 440 444 448 452 456 460 464 +468 472 476 480 484 488 492 496 500 +504 508 512 516 520 524 528 532 536 +540 544 548 552 556 560 564 568 572 +576 580 584 588 592 596 600 604 608 +612 616 +CONNECTIVITY vtktypeint64 +0 1 2 3 4 3 5 6 7 +4 8 3 7 3 2 9 8 3 +4 6 10 11 12 13 14 13 15 +16 17 13 18 16 19 18 13 20 +10 13 21 15 22 1 0 23 22 +24 25 23 25 1 3 9 26 23 +5 27 24 28 26 23 0 1 3 +23 29 30 31 1 22 31 30 1 +2 3 1 9 29 2 1 9 22 +0 28 23 32 15 21 16 14 13 +11 15 33 17 34 16 33 34 32 +16 34 16 18 35 10 11 13 15 +18 13 12 35 36 11 10 15 36 +37 38 15 32 38 37 15 32 21 +17 16 36 10 37 15 36 39 40 +11 41 40 39 11 41 12 11 35 +17 21 13 16 10 13 12 20 12 +13 11 35 14 13 16 35 34 17 +18 16 42 34 43 35 41 12 40 +11 14 41 11 35 42 41 14 35 +0 3 5 23 0 5 3 6 44 +4 9 27 29 2 30 1 44 45 +29 9 29 45 7 9 0 46 5 +6 47 48 4 6 0 3 2 6 +4 5 3 27 49 10 50 20 8 +2 3 6 51 18 48 20 49 50 +52 20 19 13 21 20 51 53 12 +20 19 48 18 20 25 3 1 23 +7 4 3 9 25 3 23 27 44 +4 7 9 25 23 26 27 54 55 +44 27 54 26 55 27 18 12 13 +20 14 16 34 35 19 21 52 20 +30 2 0 1 22 25 29 1 5 +23 3 27 25 26 44 27 22 29 +31 1 54 44 26 27 25 44 9 +27 22 28 24 23 26 5 55 27 +25 24 26 23 22 25 1 23 25 +44 29 9 25 29 1 9 48 6 +53 20 8 4 48 6 10 40 56 +12 10 40 12 11 17 19 18 13 +30 57 0 2 25 9 3 27 7 +8 2 3 26 28 5 23 29 7 +2 9 28 0 5 23 37 10 49 +21 49 50 57 52 56 12 53 20 +10 12 56 20 43 18 12 35 32 +37 21 15 14 11 13 35 19 52 +8 48 19 52 48 20 41 39 36 +11 50 6 52 20 46 53 5 6 +4 3 9 27 44 7 45 9 55 +47 4 5 28 0 46 5 8 52 +2 6 57 2 52 6 50 56 53 +20 10 56 50 20 57 50 0 6 +37 10 21 15 33 32 17 16 34 +18 43 35 57 52 50 6 51 48 +47 53 18 16 13 35 50 56 46 +53 14 36 32 15 14 11 36 15 +17 21 19 13 10 21 13 20 51 +12 18 20 50 53 46 6 14 15 +32 16 42 43 41 35 14 41 36 +11 49 52 21 20 36 40 10 11 +47 53 48 6 49 21 10 20 21 +15 13 16 51 48 53 20 55 5 +4 27 44 55 4 27 47 4 5 +6 8 48 52 6 47 5 53 6 +50 53 6 20 57 0 2 6 50 +46 0 6 52 6 48 20 42 14 +34 35 43 18 51 12 41 43 12 +35 22 30 0 1 14 32 34 16 +36 38 32 15 +CELL_TYPES 154 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 + +CELL_DATA 154 +FIELD FieldData 1 +ids 1 154 double +0 1 2 3 4 5 6 7 8 +9 10 11 12 13 14 15 16 17 +18 19 20 21 22 23 24 25 26 +27 28 29 30 31 32 33 34 35 +36 37 38 39 40 41 42 43 44 +45 46 47 48 49 50 51 52 53 +54 55 56 57 58 59 60 61 62 +63 64 65 66 67 68 69 70 71 +72 73 74 75 76 77 78 79 80 +81 82 83 84 85 86 87 88 89 +90 91 92 93 94 95 96 97 98 +99 100 101 102 103 104 105 106 107 +108 109 110 111 112 113 114 115 116 +117 118 119 120 121 122 123 124 125 +126 127 128 129 130 131 132 133 134 +135 136 137 138 139 140 141 142 143 +144 145 146 147 148 149 150 151 152 +153 diff --git a/tests/unit_tests/mesh_to_vtk/moab_tets_ref.vtk b/tests/unit_tests/mesh_to_vtk/moab_tets_ref.vtk new file mode 120000 index 000000000..91e356480 --- /dev/null +++ b/tests/unit_tests/mesh_to_vtk/moab_tets_ref.vtk @@ -0,0 +1 @@ +libmesh_tets_ref.vtk \ No newline at end of file diff --git a/tests/unit_tests/mesh_to_vtk/test.py b/tests/unit_tests/mesh_to_vtk/test.py new file mode 100644 index 000000000..5f6cd6a41 --- /dev/null +++ b/tests/unit_tests/mesh_to_vtk/test.py @@ -0,0 +1,69 @@ +import filecmp +from itertools import product +from pathlib import Path + +import numpy as np + +import openmc +import openmc.lib +import pytest + +pytest.importorskip('vtk') + + +def ids_func(param): + return f"{param['library']}_{param['elem_type']}" + +test_params = (['libmesh', 'moab'], + ['tets', 'hexes']) + +test_cases = [ + {'library' : library, 'elem_type' : elem_type} + for library, elem_type in product(*test_params) +] + +@pytest.mark.parametrize("test_opts", test_cases, ids=ids_func) +def test_unstructured_mesh_to_vtk(run_in_tmpdir, request, test_opts): + + if test_opts['library'] == 'moab' and test_opts['elem_type'] == 'hexes': + pytest.skip('Hexes are not supported with MOAB') + + if test_opts['library'] == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip('LibMesh is not enabled in this build.') + + if test_opts['library'] == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip('DAGMC (and MOAB) mesh not enabled in this build.') + + # pull in a simple model -- just need to create the statepoint file + openmc.reset_auto_ids() + model = openmc.examples.pwr_pin_cell() + + if test_opts['elem_type'] == 'tets': + filename = Path('tets.exo') + else: + filename = Path('hexes.exo') + + # create a basic tally using the unstructured mesh + umesh = openmc.UnstructuredMesh(request.node.path.parent / filename, + test_opts['library']) + umesh.output = False + mesh_filter = openmc.MeshFilter(umesh) + tally = openmc.Tally() + tally.filters = [mesh_filter] + tally.scores = ['flux'] + tally.estimator = 'collision' + model.tallies = openmc.Tallies([tally]) + sp_file = model.run() + + # check VTK output after reading mesh from statepoint file + with openmc.StatePoint(sp_file) as sp: + umesh = sp.meshes[umesh.id] + + test_data = {'ids': np.arange(umesh.n_elements)} + umesh.write_data_to_vtk('umesh.vtk', + datasets=test_data, + volume_normalization=False) + + # compare file content with reference file + ref_file = Path(f"{test_opts['library']}_{test_opts['elem_type']}_ref.vtk") + assert filecmp.cmp('umesh.vtk', request.node.path.parent / ref_file) diff --git a/tests/unit_tests/mesh_to_vtk/tets.exo b/tests/unit_tests/mesh_to_vtk/tets.exo new file mode 100644 index 000000000..add4f7cfa Binary files /dev/null and b/tests/unit_tests/mesh_to_vtk/tets.exo differ diff --git a/tests/unit_tests/mesh_to_vtk/umesh.vtk b/tests/unit_tests/mesh_to_vtk/umesh.vtk new file mode 100644 index 000000000..90f226121 --- /dev/null +++ b/tests/unit_tests/mesh_to_vtk/umesh.vtk @@ -0,0 +1,292 @@ +# vtk DataFile Version 5.1 +vtk output +ASCII +DATASET UNSTRUCTURED_GRID +POINTS 58 double +-0.02593964576 -1 -1.1195739536 -0.40239958217 -0.40962166746 -1.9256035383 -1 0.02593964576 -1.1195739536 +-0.2248833639 0.23189144433 -1.2841109811 0.02593964576 1 -1.1195739536 1 -0.02593964576 -1.1195739536 +-0.042928712675 0.066425810853 -0.54270728017 -1 1 -1.5 -1 1 -0.5 +-0.49265312381 0.49332495053 -1.9656359509 -0.02608137991 -1 1.121521147 0.30281888252 -0.30293622513 1.8067963894 +1 -0.02608137991 1.121521147 -0.23236342389 0.26314805583 1.2867556783 0.074337770194 0.074337770194 2.5 +-0.40005770996 -0.36909660956 1.8768871762 -0.4896743493 0.49451064621 1.952829049 -1 1 1.5 +0.02608137991 1 1.121521147 -1 1 0.5 -0.038567136385 0.14628887118 0.55111724874 +-1 0.02608137991 1.121521147 0 -1 -2.5 0.37543954218 -0.36967061971 -1.8691105401 +1 -1 -2.5 0.074337770194 -0.074337770194 -2.5 1 0 -2.5 +0.4135797166 0.38351746262 -1.9274856063 1 -1 -1.5 -1 0 -2.5 +-1 -1 -1.5 -1 -1 -2.5 -1 0 2.5 +-1 1 2.5 0 1 2.5 0.46519182209 0.49356920409 1.95286052 +0 -1 2.5 -1 -1 1.5 -1 -1 2.5 +1 -1 2.5 1 -1 1.5 1 0 2.5 +1 1 2.5 1 1 1.5 0 1 -2.5 +-1 1 -2.5 1 -1 -0.5 1 1 -0.5 +0.038669824604 1 0.0046021677516 -1 -1 0.5 -0.038669824604 -1 0.0046021677516 +1 1 0.5 -1 0.038669824604 0.0046021677516 1 -0.038669824604 0.0046021677516 +1 1 -2.5 1 1 -1.5 1 -1 0.5 +-1 -1 -0.5 +CELLS 155 616 +OFFSETS vtktypeint64 +0 4 8 12 16 20 24 28 32 +36 40 44 48 52 56 60 64 68 +72 76 80 84 88 92 96 100 104 +108 112 116 120 124 128 132 136 140 +144 148 152 156 160 164 168 172 176 +180 184 188 192 196 200 204 208 212 +216 220 224 228 232 236 240 244 248 +252 256 260 264 268 272 276 280 284 +288 292 296 300 304 308 312 316 320 +324 328 332 336 340 344 348 352 356 +360 364 368 372 376 380 384 388 392 +396 400 404 408 412 416 420 424 428 +432 436 440 444 448 452 456 460 464 +468 472 476 480 484 488 492 496 500 +504 508 512 516 520 524 528 532 536 +540 544 548 552 556 560 564 568 572 +576 580 584 588 592 596 600 604 608 +612 616 +CONNECTIVITY vtktypeint64 +0 1 2 3 4 3 5 6 7 +4 8 3 7 3 2 9 8 3 +4 6 10 11 12 13 14 13 15 +16 17 13 18 16 19 18 13 20 +10 13 21 15 22 1 0 23 22 +24 25 23 25 1 3 9 26 23 +5 27 24 28 26 23 0 1 3 +23 29 30 31 1 22 31 30 1 +2 3 1 9 29 2 1 9 22 +0 28 23 32 15 21 16 14 13 +11 15 33 17 34 16 33 34 32 +16 34 16 18 35 10 11 13 15 +18 13 12 35 36 11 10 15 36 +37 38 15 32 38 37 15 32 21 +17 16 36 10 37 15 36 39 40 +11 41 40 39 11 41 12 11 35 +17 21 13 16 10 13 12 20 12 +13 11 35 14 13 16 35 34 17 +18 16 42 34 43 35 41 12 40 +11 14 41 11 35 42 41 14 35 +0 3 5 23 0 5 3 6 44 +4 9 27 29 2 30 1 44 45 +29 9 29 45 7 9 0 46 5 +6 47 48 4 6 0 3 2 6 +4 5 3 27 49 10 50 20 8 +2 3 6 51 18 48 20 49 50 +52 20 19 13 21 20 51 53 12 +20 19 48 18 20 25 3 1 23 +7 4 3 9 25 3 23 27 44 +4 7 9 25 23 26 27 54 55 +44 27 54 26 55 27 18 12 13 +20 14 16 34 35 19 21 52 20 +30 2 0 1 22 25 29 1 5 +23 3 27 25 26 44 27 22 29 +31 1 54 44 26 27 25 44 9 +27 22 28 24 23 26 5 55 27 +25 24 26 23 22 25 1 23 25 +44 29 9 25 29 1 9 48 6 +53 20 8 4 48 6 10 40 56 +12 10 40 12 11 17 19 18 13 +30 57 0 2 25 9 3 27 7 +8 2 3 26 28 5 23 29 7 +2 9 28 0 5 23 37 10 49 +21 49 50 57 52 56 12 53 20 +10 12 56 20 43 18 12 35 32 +37 21 15 14 11 13 35 19 52 +8 48 19 52 48 20 41 39 36 +11 50 6 52 20 46 53 5 6 +4 3 9 27 44 7 45 9 55 +47 4 5 28 0 46 5 8 52 +2 6 57 2 52 6 50 56 53 +20 10 56 50 20 57 50 0 6 +37 10 21 15 33 32 17 16 34 +18 43 35 57 52 50 6 51 48 +47 53 18 16 13 35 50 56 46 +53 14 36 32 15 14 11 36 15 +17 21 19 13 10 21 13 20 51 +12 18 20 50 53 46 6 14 15 +32 16 42 43 41 35 14 41 36 +11 49 52 21 20 36 40 10 11 +47 53 48 6 49 21 10 20 21 +15 13 16 51 48 53 20 55 5 +4 27 44 55 4 27 47 4 5 +6 8 48 52 6 47 5 53 6 +50 53 6 20 57 0 2 6 50 +46 0 6 52 6 48 20 42 14 +34 35 43 18 51 12 41 43 12 +35 22 30 0 1 14 32 34 16 +36 38 32 15 +CELL_TYPES 154 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 + +CELL_DATA 154 +FIELD FieldData 1 +ids 1 154 double +0 1 2 3 4 5 6 7 8 +9 10 11 12 13 14 15 16 17 +18 19 20 21 22 23 24 25 26 +27 28 29 30 31 32 33 34 35 +36 37 38 39 40 41 42 43 44 +45 46 47 48 49 50 51 52 53 +54 55 56 57 58 59 60 61 62 +63 64 65 66 67 68 69 70 71 +72 73 74 75 76 77 78 79 80 +81 82 83 84 85 86 87 88 89 +90 91 92 93 94 95 96 97 98 +99 100 101 102 103 104 105 106 107 +108 109 110 111 112 113 114 115 116 +117 118 119 120 121 122 123 124 125 +126 127 128 129 130 131 132 133 134 +135 136 137 138 139 140 141 142 143 +144 145 146 147 148 149 150 151 152 +153 diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index 06b8e6bed..f0bda1bd9 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -23,6 +23,14 @@ def nb90(): return openmc.data.Decay.from_endf(filename) +@pytest.fixture(scope='module') +def ba137m(): + """Ba137_m1 decay data.""" + endf_data = os.environ['OPENMC_ENDF_DATA'] + filename = os.path.join(endf_data, 'decay', 'dec-056_Ba_137m1.endf') + return openmc.data.Decay.from_endf(filename) + + @pytest.fixture(scope='module') def u235_yields(): """U235 fission product yield data.""" @@ -48,6 +56,7 @@ def test_nb90_halflife(nb90): ufloat_close(nb90.decay_constant, log(2.)/nb90.half_life) ufloat_close(nb90.decay_energy, ufloat(2265527.5, 25159.400474401213)) + def test_nb90_nuclide(nb90): assert nb90.nuclide['atomic_number'] == 41 assert nb90.nuclide['mass_number'] == 90 @@ -91,3 +100,29 @@ def test_fpy(u235_yields): assert len(u235_yields.independent) == 3 thermal = u235_yields.independent[0] ufloat_close(thermal['I135'], ufloat(0.0292737, 0.000819663)) + + +def test_sources(ba137m, nb90): + # Running .sources twice should give same objects + sources = ba137m.sources + sources2 = ba137m.sources + for key in sources: + assert sources[key] is sources2[key] + + # Each source should be a univariate distribution + for dist in sources.values(): + assert isinstance(dist, openmc.stats.Univariate) + + # Check for presence of 662 keV gamma ray in decay of Ba137m + gamma_source = ba137m.sources['photon'] + assert isinstance(gamma_source, openmc.stats.Discrete) + b = np.isclose(gamma_source.x, 661657.) + assert np.count_nonzero(b) == 1 + + # Check value of decay/s/atom + idx = np.flatnonzero(b)[0] + assert gamma_source.p[idx] == pytest.approx(0.004069614) + + # Nb90 decays by β+ and should emit positrons, electrons, and photons + sources = nb90.sources + assert len(set(sources.keys()) ^ {'positron', 'electron', 'photon'}) == 0 diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py new file mode 100644 index 000000000..3b837af7d --- /dev/null +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -0,0 +1,176 @@ +"""Test of the Kalbach-Mann slope calculation when data are +retrieved from ENDF files.""" + +import os +from pathlib import Path +import pytest + +import numpy as np + +from openmc.data import IncidentNeutron +from openmc.data.kalbach_mann import _separation_energy, _AtomicRepresentation +from openmc.data import kalbach_slope +from openmc.data import KalbachMann + +from . import needs_njoy + + +@pytest.fixture(scope='module') +def neutron(): + """Neutron AtomicRepresentation.""" + return _AtomicRepresentation(z=0, a=1) + + +@pytest.fixture(scope='module') +def triton(): + """Triton AtomicRepresentation.""" + return _AtomicRepresentation(z=1, a=3) + + +@pytest.fixture(scope='module') +def b10(): + """B10 AtomicRepresentation.""" + return _AtomicRepresentation(z=5, a=10) + + +@pytest.fixture(scope='module') +def c12(): + """C12 AtomicRepresentation.""" + return _AtomicRepresentation(z=6, a=12) + + +@pytest.fixture(scope='module') +def c13(): + """C13 AtomicRepresentation.""" + return _AtomicRepresentation(z=6, a=13) + + +@pytest.fixture(scope='module') +def na23(): + """Na23 AtomicRepresentation.""" + return _AtomicRepresentation(z=11, a=23) + + +def test_atomic_representation(neutron, triton, b10, c12, c13, na23): + """Test the _AtomicRepresentation class.""" + # Test instantiation from_za + assert b10 == _AtomicRepresentation.from_za(5010) + + # Test addition + assert c13 + b10 == na23 + + # Test substraction + assert c13 - c12 == neutron + assert c13 - b10 == triton + + # Test properties when no information for Kalbach-Mann are given + assert c13.a == 13 + assert c13.z == 6 + assert c13.n == 7 + assert c13.za == 6013 + + # Test properties when information for Kalbach-Mann are given + assert triton.a == 3 + assert triton.z == 1 + assert triton.n == 2 + assert triton.za == 1003 + + # Test instantiation errors + with pytest.raises(ValueError): + _AtomicRepresentation(z=5, a=1) + with pytest.raises(ValueError): + _AtomicRepresentation(z=-1, a=1) + with pytest.raises(ValueError): + _AtomicRepresentation(z=5, a=0) + with pytest.raises(ValueError): + _AtomicRepresentation(z=5, a=-2) + with pytest.raises(ValueError): + neutron - triton + + +def test_separation_energy(triton, b10, c13): + """Comparison to hand-calculations on a simple example.""" + assert _separation_energy( + compound=c13, + nucleus=b10, + particle=triton + ) == pytest.approx(18.6880713) + + +def test_kalbach_slope(): + """Comparison to hand-calculations for n + c12 -> c13 -> triton + b10.""" + energy_projectile = 10.2 # [eV] + energy_emitted = 5.4 # [eV] + + # Check that NotImplementedError is raised if the projectile is not + # a neutron + with pytest.raises(NotImplementedError): + kalbach_slope( + energy_projectile=energy_projectile, + energy_emitted=energy_emitted, + za_projectile=1000, + za_emitted=1, + za_target=6012 + ) + + assert kalbach_slope( + energy_projectile=energy_projectile, + energy_emitted=energy_emitted, + za_projectile=1, + za_emitted=1003, + za_target=6012 + ) == pytest.approx(0.8409921475) + + +@pytest.mark.parametrize( + "hdf5_filename, endf_filename", [ + ('O16.h5', 'n-008_O_016.endf'), + ('Ca46.h5', 'n-020_Ca_046.endf'), + ('Hg204.h5', 'n-080_Hg_204.endf') + ] +) +def test_comparison_slope_hdf5(hdf5_filename, endf_filename): + """Test the calculation of the Kalbach-Mann slope done by OpenMC + by comparing it to HDF5 data. The test is based on the first product + of MT=5 (neutron). The isotopes tested have been selected because the + corresponding products in ENDF/B-VII.1 are described using MF=6, LAW=1, + LANG=2 (i.e., Kalbach-Mann systematics) and the slope is not given + explicitly. + + If an error occurs during the "validity check", this means that + the nuclear data evaluation has evolved and the distribution might + no longer be described using Kalbach-Mann systematics. Another + isotope needs to be identified and tested. + + Warning: This test is valid as long as ENDF files are not directly + used to generate the HDF5 files used in the tests. + + """ + # HDF5 data + hdf5_directory = Path(os.environ['OPENMC_CROSS_SECTIONS']).parent + hdf5_data = IncidentNeutron.from_hdf5(hdf5_directory / hdf5_filename) + hdf5_product = hdf5_data[5].products[0] + hdf5_distribution = hdf5_product.distribution[0] + + # ENDF data + endf_directory = Path(os.environ['OPENMC_ENDF_DATA']) + endf_path = endf_directory / 'neutrons' / endf_filename + endf_data = IncidentNeutron.from_endf(endf_path) + endf_product = endf_data[5].products[0] + endf_distribution = endf_product.distribution[0] + + # Validity check + assert isinstance(endf_distribution, KalbachMann) + assert isinstance(hdf5_distribution, KalbachMann) + assert endf_product.particle == hdf5_product.particle + assert len(endf_distribution.slope) == len(hdf5_distribution.slope) + + # Results check + for i, hdf5_slope in enumerate(hdf5_distribution.slope): + assert endf_distribution._calculated_slope[i] + + np.testing.assert_array_almost_equal( + endf_distribution.slope[i].y, + hdf5_slope.y, + decimal=5 + ) diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index dc4d24628..61ae61b24 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -262,3 +262,108 @@ def test_get_thermal_name(): # Names that don't remotely match anything assert f('boogie_monster') == 'c_boogie_monster' + + +@pytest.fixture +def fake_mixed_elastic(): + fake_tsl = openmc.data.ThermalScattering("c_D_in_7LiD", 1.9968, 4.9, [0.0253]) + fake_tsl.nuclides = ['H2'] + + # Create elastic reaction + bragg_edges = [0.00370672, 0.00494229, 0.00988458, 0.01359131, 0.01482688, + 0.01976918, 0.02347589, 0.02471147, 0.02965376, 0.03336048, + 0.03953834, 0.04324506, 0.04448063, 0.04942292, 0.05312964, + 0.05436522, 0.05930751, 0.06301423, 0.0642498 , 0.06919209, + 0.07289881, 0.07907667, 0.08278339, 0.08401896, 0.08896126, + 0.09266798, 0.09390355, 0.09884584, 0.1025526 , 0.1037882 , + 0.1087305 , 0.1124372 , 0.1186151 , 0.1223218 , 0.1235574 , + 0.1284997 , 0.1322064 , 0.133442 , 0.142091 , 0.1433266 , + 0.1482688 , 0.1519756 , 0.1581534 , 0.1618601 , 0.1630957 , + 0.168038 , 0.1717447 , 0.1729803 , 0.1779226 , 0.1816293 , + 0.1828649 , 0.1878072 , 0.1915139 , 0.1976918 , 0.2026341 , + 0.2075763 , 0.2125186 , 0.2174609 , 0.2224032 , 0.2273455 , + 0.2421724 , 0.2471147 , 0.252057 , 0.2569993 , 0.2619415 , + 0.2668838 , 0.2767684 , 0.2817107 , 0.2915953 , 0.3064222 , + 0.3261913 , 0.366965] + factors = [0.00375735, 0.01386287, 0.02595574, 0.02992438, 0.03549502, + 0.03855745, 0.04058831, 0.04986305, 0.05703106, 0.05855471, + 0.06078031, 0.06212291, 0.06656602, 0.06930339, 0.0697072 , + 0.07201456, 0.07263853, 0.07313129, 0.07465531, 0.07714482, + 0.07759976, 0.077809 , 0.07790282, 0.07927957, 0.08013058, + 0.08026637, 0.08073475, 0.08112202, 0.08123039, 0.08187171, + 0.08213756, 0.08218236, 0.08236572, 0.08240729, 0.08259795, + 0.08297893, 0.08300455, 0.08314566, 0.08315611, 0.08337715, + 0.08350026, 0.08350663, 0.08352815, 0.08353776, 0.0836098 , + 0.08367017, 0.08367361, 0.0837242 , 0.08375069, 0.08375227, + 0.08377006, 0.08381488, 0.08381644, 0.08382698, 0.08386266, + 0.08387756, 0.08388445, 0.08388974, 0.08390341, 0.08391088, + 0.08391695, 0.08392361, 0.08392684, 0.08392818, 0.08393161, + 0.08393546, 0.08393685, 0.08393801, 0.08393976, 0.08394167, + 0.08394288, 0.08394398] + coherent_xs = openmc.data.CoherentElastic(bragg_edges, factors) + incoherent_xs = openmc.data.Tabulated1D([0.00370672, 0.00370672], [0.00370672, 0.00370672]) + elastic_xs = {'294K': openmc.data.Sum((coherent_xs, incoherent_xs))} + coherent_dist = openmc.data.CoherentElasticAE(coherent_xs) + incoherent_dist = openmc.data.IncoherentElasticAEDiscrete([ + [-0.6, -0.18, 0.18, 0.6], [-0.6, -0.18, 0.18, 0.6] + ]) + elastic_dist = {'294K': openmc.data.MixedElasticAE(coherent_dist, incoherent_dist)} + fake_tsl.elastic = openmc.data.ThermalScatteringReaction(elastic_xs, elastic_dist) + + # Create inelastic reaction + inelastic_xs = {'294K': openmc.data.Tabulated1D([1.0e-5, 4.9], [13.4, 3.35])} + breakpoints = [3] + interpolation = [2] + energy = [1.0e-5, 4.3e-2, 4.9] + energy_out = [ + openmc.data.Tabular([0.0002, 0.067, 0.146, 0.366], [0.25, 0.25, 0.25, 0.25]), + openmc.data.Tabular([0.0001, 0.009, 0.137, 0.277], [0.25, 0.25, 0.25, 0.25]), + openmc.data.Tabular([0.0579, 4.555, 4.803, 4.874], [0.25, 0.25, 0.25, 0.25]), + ] + for eout in energy_out: + eout.normalize() + eout.c = eout.cdf() + discrete = openmc.stats.Discrete([-0.9, -0.6, -0.3, -0.1, 0.1, 0.3, 0.6, 0.9], [1/8]*8) + discrete.c = discrete.cdf()[1:] + mu = [[discrete]*4]*3 + inelastic_dist = {'294K': openmc.data.IncoherentInelasticAE( + breakpoints, interpolation, energy, energy_out, mu)} + inelastic = openmc.data.ThermalScatteringReaction(inelastic_xs, inelastic_dist) + fake_tsl.inelastic = inelastic + + return fake_tsl + + +def test_mixed_elastic(fake_mixed_elastic, run_in_tmpdir): + # Write data to HDF5 and then read back + original = fake_mixed_elastic + original.export_to_hdf5('c_D_in_7LiD.h5') + copy = openmc.data.ThermalScattering.from_hdf5('c_D_in_7LiD.h5') + + # Make sure data did not change as a result of HDF5 writing/reading + assert original == copy + + # Create modified cross_sections.xml file that includes the above data + xs = openmc.data.DataLibrary.from_xml() + xs.register_file('c_D_in_7LiD.h5') + xs.export_to_xml('cross_sections_mixed.xml') + + # Create a minimal model that includes the new data and run it + mat = openmc.Material() + mat.add_nuclide('H2', 1.0) + mat.add_nuclide('Li7', 1.0) + mat.set_density('g/cm3', 1.0) + mat.add_s_alpha_beta('c_D_in_7LiD') + sph = openmc.Sphere(r=10.0, boundary_type="vacuum") + cell = openmc.Cell(fill=mat, region=-sph) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.materials = openmc.Materials([mat]) + model.materials.cross_sections = "cross_sections_mixed.xml" + model.settings.particles = 1000 + model.settings.batches = 10 + model.settings.run_mode = 'fixed source' + model.settings.source = openmc.Source( + energy=openmc.stats.Discrete([3.0], [1.0]) # 3 eV source + ) + model.run() diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py new file mode 100644 index 000000000..6de0342b0 --- /dev/null +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -0,0 +1,55 @@ +"""Basic unit tests for openmc.deplete.CoupledOperator instantiation + +""" + +from pathlib import Path + +import pytest +from openmc.deplete import CoupledOperator +import openmc +import numpy as np + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" + +@pytest.fixture(scope="module") +def model(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + + clad = openmc.Material(name="clad") + clad.add_element("Zr", 1) + clad.set_density("g/cc", 6) + + water = openmc.Material(name="water") + water.add_element("O", 1) + water.add_element("H", 2) + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + + radii = [0.42, 0.45] + fuel.volume = np.pi * radii[0] ** 2 + clad.volume = np.pi * (radii[1]**2 - radii[0]**2) + water.volume = 1.24**2 - (np.pi * radii[1]**2) + + materials = openmc.Materials([fuel, clad, water]) + + pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] + pin_univ = openmc.model.pin(pin_surfaces, materials) + bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + geometry = openmc.Geometry([root_cell]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + +def test_operator_init(model): + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + + CoupledOperator(model, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py new file mode 100644 index 000000000..f6cc7e200 --- /dev/null +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -0,0 +1,36 @@ +"""Basic unit tests for openmc.deplete.IndependentOperator instantiation + +""" + +from pathlib import Path + +import pytest +from openmc.deplete import IndependentOperator, MicroXS +from openmc import Material, Materials +import numpy as np + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + +def test_operator_init(): + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + volume = 1 + nuclides = {'U234': 8.922411359424315e+18, + 'U235': 9.98240191860822e+20, + 'U238': 2.2192386373095893e+22, + 'U236': 4.5724195495061115e+18, + 'O16': 4.639065406771322e+22, + 'O17': 1.7588724018066158e+19} + micro_xs = MicroXS.from_csv(ONE_GROUP_XS) + IndependentOperator.from_nuclides( + volume, nuclides, micro_xs, CHAIN_PATH, nuc_units='atom/cm3') + + fuel = Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + fuel.depletable=True + fuel.volume = 1 + materials = Materials([fuel]) + IndependentOperator(materials, micro_xs, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py new file mode 100644 index 000000000..557082eda --- /dev/null +++ b/tests/unit_tests/test_deplete_microxs.py @@ -0,0 +1,59 @@ +"""Basic unit tests for openmc.deplete.IndependentOperator instantiation + +Modifies and resets environment variable OPENMC_CROSS_SECTIONS +to a custom file with new depletion_chain node +""" + +from os import remove +from pathlib import Path + +import pytest +from openmc.deplete import MicroXS +import numpy as np + +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + + +def test_from_array(): + nuclides = [ + 'U234', + 'U235', + 'U238', + 'U236', + 'O16', + 'O17', + 'I135', + 'Xe135', + 'Xe136', + 'Cs135', + 'Gd157', + 'Gd156'] + reactions = ['fission', '(n,gamma)'] + # These values are placeholders and are not at all + # physically meaningful. + data = np.array([[0.1, 0.], + [0.1, 0.], + [0.9, 0.], + [0.4, 0.], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.9], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.1]]) + + MicroXS.from_array(nuclides, reactions, data) + with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' + r'reactions array of length \d* do not ' + r'match dimensions of data array of shape \(\d*\,d*\)'): + MicroXS.from_array(nuclides, reactions, data[:, 0]) + +def test_csv(): + ref_xs = MicroXS.from_csv(ONE_GROUP_XS) + ref_xs.to_csv('temp_xs.csv') + temp_xs = MicroXS.from_csv('temp_xs.csv') + assert np.all(ref_xs == temp_xs) + remove('temp_xs.csv') + diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 6ce15601f..63073cf57 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -11,7 +11,7 @@ import openmc.deplete @pytest.fixture def res(): """Load the reference results""" - filename = (Path(__file__).parents[1] / 'regression_tests' / 'deplete' + filename = (Path(__file__).parents[1] / 'regression_tests' / 'deplete_with_transport' / 'test_reference.h5') return openmc.deplete.Results(filename) @@ -70,14 +70,16 @@ def test_get_keff(res): np.testing.assert_allclose(k[:, 1], u_ref) -@pytest.mark.parametrize("unit", ("s", "d", "min", "h")) +@pytest.mark.parametrize("unit", ("s", "d", "min", "h", "a")) def test_get_steps(unit): # Make a Results full of near-empty Result instances # Just fill out a time schedule results = openmc.deplete.Results() # Time in units of unit times = np.linspace(0, 100, num=5) - if unit == "d": + if unit == "a": + conversion_to_seconds = 60 * 60 * 24 * 365.25 + elif unit == "d": conversion_to_seconds = 60 * 60 * 24 elif unit == "h": conversion_to_seconds = 60 * 60 diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 6b344b827..ec051d9fd 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -1,3 +1,4 @@ +import math import numpy as np import openmc from pytest import fixture, approx @@ -246,3 +247,11 @@ def test_energy(): f = openmc.EnergyFilter.from_group_structure('CCFE-709') assert f.bins.shape == (709, 2) assert len(f.values) == 710 + + +def test_lethargy_bin_width(): + f = openmc.EnergyFilter.from_group_structure('VITAMIN-J-175') + assert len(f.lethargy_bin_width) == 175 + energy_bins = openmc.mgxs.GROUP_STRUCTURES['VITAMIN-J-175'] + assert f.lethargy_bin_width[0] == math.log10(energy_bins[1]/energy_bins[0]) + assert f.lethargy_bin_width[-1] == math.log10(energy_bins[-1]/energy_bins[-2]) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index fd0e4f624..451c5308a 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -25,6 +25,52 @@ def test_add_nuclide(): with pytest.raises(ValueError): m.add_nuclide('H1', 1.0, 'oa') +def test_add_components(): + """Test adding multipe elements or nuclides at once""" + m = openmc.Material() + components = {'H1': 2.0, + 'O16': 1.0, + 'Zr': 1.0, + 'O': 1.0, + 'U': {'percent': 1.0, + 'enrichment': 4.5}, + 'Li': {'percent': 1.0, + 'enrichment': 60.0, + 'enrichment_target': 'Li7'}, + 'H': {'percent': 1.0, + 'enrichment': 50.0, + 'enrichment_target': 'H2', + 'enrichment_type': 'wo'}} + m.add_components(components) + with pytest.raises(ValueError): + m.add_components({'U': {'percent': 1.0, + 'enrichment': 100.0}}) + with pytest.raises(ValueError): + m.add_components({'Pu': {'percent': 1.0, + 'enrichment': 3.0}}) + with pytest.raises(ValueError): + m.add_components({'U': {'percent': 1.0, + 'enrichment': 70.0, + 'enrichment_target':'U235'}}) + with pytest.raises(ValueError): + m.add_components({'He': {'percent': 1.0, + 'enrichment': 17.0, + 'enrichment_target': 'He6'}}) + with pytest.raises(ValueError): + m.add_components({'li': 1.0}) # should fail as 1st char is lowercase + with pytest.raises(ValueError): + m.add_components({'LI': 1.0}) # should fail as 2nd char is uppercase + with pytest.raises(ValueError): + m.add_components({'Xx': 1.0}) # should fail as Xx is not an element + with pytest.raises(ValueError): + m.add_components({'n': 1.0}) # check to avoid n for neutron being accepted + with pytest.raises(TypeError): + m.add_components({'H1': '1.0'}) + with pytest.raises(TypeError): + m.add_components({1.0: 'H1'}, percent_type = 'wo') + with pytest.raises(ValueError): + m.add_components({'H1': 1.0}, percent_type = 'oa') + def test_remove_nuclide(): """Test removing nuclides.""" @@ -49,7 +95,7 @@ def test_remove_elements(): assert m.nuclides[0].percent == 1.0 -def test_elements(): +def test_add_element(): """Test adding elements.""" m = openmc.Material() m.add_element('Zr', 1.0) @@ -74,7 +120,6 @@ def test_elements(): with pytest.raises(ValueError): m.add_element('n', 1.0) # check to avoid n for neutron being accepted - def test_elements_by_name(): """Test adding elements by name""" m = openmc.Material() @@ -426,28 +471,46 @@ def test_mix_materials(): assert m5.density == pytest.approx(dens5) -def test_activity_of_stable(): - """Creates a material with stable isotopes to checks the activity is 0""" +def test_get_activity(): + """Tests the activity of stable, metastable and active materials""" + + # Creates a material with stable isotopes to check the activity is 0 m1 = openmc.Material() - m1.add_element("Fe", 1) - m1.set_density('g/cm3', 1) + m1.add_element("Fe", 0.7) + m1.add_element("Li", 0.3) + m1.set_density('g/cm3', 1.5) + # activity in Bq/cc and Bq/g should not require volume setting + assert m1.get_activity(units='Bq/cm3') == 0 + assert m1.get_activity(units='Bq/g') == 0 m1.volume = 1 - assert m1.activity == 0 + assert m1.get_activity(units='Bq') == 0 + # Checks that 1g of tritium has the correct activity scaling + m2 = openmc.Material() + m2.add_nuclide("H3", 1) + m2.set_density('g/cm3', 1) + m2.volume = 1 + assert pytest.approx(m2.get_activity(units='Bq')) == 3.559778e14 + m2.set_density('g/cm3', 2) + assert pytest.approx(m2.get_activity(units='Bq')) == 3.559778e14*2 + m2.volume = 3 + assert pytest.approx(m2.get_activity(units='Bq')) == 3.559778e14*2*3 -def test_activity_of_tritium(): - """Checks that 1g of tritium has the correct activity""" - m1 = openmc.Material() - m1.add_nuclide("H3", 1) - m1.set_density('g/cm3', 1) - m1.volume = 1 - assert pytest.approx(m1.activity) == 3.559778e14 + # Checks that 1 mol of a metastable nuclides has the correct activity + m3 = openmc.Material() + m3.add_nuclide("Tc99_m1", 1) + m3.set_density('g/cm3', 1) + m3.volume = 98.9 + assert pytest.approx(m3.get_activity(units='Bq'), rel=0.001) == 1.93e19 - -def test_activity_of_metastable(): - """Checks that 1 mol of a Tc99_m1 nuclides has the correct activity""" - m1 = openmc.Material() - m1.add_nuclide("Tc99_m1", 1) - m1.set_density('g/cm3', 1) - m1.volume = 98.9 - assert pytest.approx(m1.activity, rel=0.001) == 1.93e19 + # Checks that specific and volumetric activity of tritium are correct + m4 = openmc.Material() + m4.add_nuclide("H3", 1) + m4.set_density('g/cm3', 1.5) + assert pytest.approx(m4.get_activity(units='Bq/g')) == 355978108155965.94 # [Bq/g] + assert pytest.approx(m4.get_activity(units='Bq/g', by_nuclide=True)["H3"]) == 355978108155965.94 # [Bq/g] + assert pytest.approx(m4.get_activity(units='Bq/cm3')) == 355978108155965.94*3/2 # [Bq/cc] + assert pytest.approx(m4.get_activity(units='Bq/cm3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2 # [Bq/cc] + # volume is required to calculate total activity + m4.volume = 10. + assert pytest.approx(m4.get_activity(units='Bq')) == 355978108155965.94*3/2*10 # [Bq] diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index 8537e3b80..086ce6401 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -208,3 +208,17 @@ def test_from_expression(reset): # Opening parenthesis immediately after halfspace r = openmc.Region.from_expression('1(2|-3)', surfs) assert str(r) == '(1 (2 | -3))' + + +def test_translate_inplace(): + sph = openmc.Sphere() + x = openmc.XPlane() + region = -sph & +x + + # Translating a region should produce new surfaces + region2 = region.translate((0.5, -6.7, 3.9), inplace=False) + assert str(region) != str(region2) + + # Translating a region in-place should *not* produce new surfaces + region3 = region.translate((0.5, -6.7, 3.9), inplace=True) + assert str(region) == str(region3) diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index b21c036fa..7678711a4 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -3,9 +3,7 @@ import openmc.stats def test_export_to_xml(run_in_tmpdir): - s = openmc.Settings() - s.run_mode = 'fixed source' - s.batches = 1000 + s = openmc.Settings(run_mode='fixed source', batches=1000, seed=17) s.generations_per_batch = 10 s.inactive = 100 s.particles = 1000000 @@ -25,7 +23,6 @@ def test_export_to_xml(run_in_tmpdir): s.surf_source_write = {'surface_ids': [2], 'max_particles': 200} s.confidence_intervals = True s.ptables = True - s.seed = 17 s.survival_biasing = True s.cutoff = {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index b57f9578a..dbf06d0b5 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -6,6 +6,12 @@ import openmc import openmc.stats +def assert_sample_mean(samples, expected_mean): + std_dev = samples.std() / np.sqrt(samples.size) + assert np.abs(expected_mean - samples.mean()) < 3*std_dev + + + def test_discrete(): x = [0.0, 1.0, 10.0] p = [0.3, 0.2, 0.5] @@ -13,8 +19,8 @@ def test_discrete(): elem = d.to_xml_element('distribution') d = openmc.stats.Discrete.from_xml_element(elem) - assert d.x == x - assert d.p == p + np.testing.assert_array_equal(d.x, x) + np.testing.assert_array_equal(d.p, p) assert len(d) == len(x) d = openmc.stats.Univariate.from_xml_element(elem) @@ -33,13 +39,11 @@ def test_discrete(): d3 = openmc.stats.Discrete(vals, probs) - # sample discrete distribution + # sample discrete distribution and check that the mean of the samples is + # within 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d3.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_merge_discrete(): @@ -57,12 +61,14 @@ def test_merge_discrete(): assert merged.x == pytest.approx([0.0, 0.5, 1.0, 5.0, 10.0]) assert merged.p == pytest.approx( [0.6*0.3, 0.4*0.4, 0.6*0.2 + 0.4*0.5, 0.4*0.1, 0.6*0.5]) + assert merged.integral() == pytest.approx(1.0) # Probabilities add up but are not normalized d1 = openmc.stats.Discrete([3.0], [1.0]) triple = openmc.stats.Discrete.merge([d1, d1, d1], [1.0, 2.0, 3.0]) assert triple.x == pytest.approx([3.0]) assert triple.p == pytest.approx([6.0]) + assert triple.integral() == pytest.approx(6.0) def test_uniform(): @@ -76,17 +82,16 @@ def test_uniform(): assert len(d) == 2 t = d.to_tabular() - assert t.x == [a, b] - assert t.p == [1/(b-a), 1/(b-a)] + np.testing.assert_array_equal(t.x, [a, b]) + np.testing.assert_array_equal(t.p, [1/(b-a), 1/(b-a)]) assert t.interpolation == 'histogram' + # Sample distribution and check that the mean of the samples is within 3 + # std. dev. of the expected mean exp_mean = 0.5 * (a + b) n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_powerlaw(): @@ -102,13 +107,11 @@ def test_powerlaw(): exp_mean = 100.0 * (n+1) / (n+2) - # sample power law distribution + # sample power law distribution and check that the mean of the samples is + # within 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_maxwell(): @@ -122,21 +125,15 @@ def test_maxwell(): exp_mean = 3/2 * theta - # sample maxwell distribution + # sample maxwell distribution and check that the mean of the samples is + # within 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) # A second sample with a different seed samples_2 = d.sample(n_samples, seed=200) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples_2.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples_2.mean()) < 3*std_dev - + assert_sample_mean(samples_2, exp_mean) assert samples_2.mean() != samples.mean() @@ -156,13 +153,11 @@ def test_watt(): # https://doi.org/10.1016/j.physletb.2003.09.048 exp_mean = 3/2 * a + a**2 * b / 4 - # sample Watt distribution + # sample Watt distribution and check that the mean of the samples is within + # 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_tabular(): @@ -193,6 +188,7 @@ def test_tabular(): # test histogram sampling d = openmc.stats.Tabular(x, p, interpolation='histogram') d.normalize() + assert d.integral() == pytest.approx(1.0) samples = d.sample(n_samples, seed=100) diff = np.abs(samples - d.mean()) @@ -228,6 +224,11 @@ def test_mixture(): assert mix.distribution == [d1, d2] assert len(mix) == 4 + # Sample and make sure sample mean is close to expected mean + n_samples = 1_000_000 + samples = mix.sample(n_samples) + assert_sample_mean(samples, (2.5 + 5.0)/2) + elem = mix.to_xml_element('distribution') d = openmc.stats.Mixture.from_xml_element(elem) @@ -396,3 +397,48 @@ def test_muir(): assert within_2_sigma / n_samples >= 0.95 within_3_sigma = np.count_nonzero(samples < 3*d.std_dev) assert within_3_sigma / n_samples >= 0.99 + + +def test_combine_distributions(): + # Combine two discrete (same data as in test_merge_discrete) + x1 = [0.0, 1.0, 10.0] + p1 = [0.3, 0.2, 0.5] + d1 = openmc.stats.Discrete(x1, p1) + x2 = [0.5, 1.0, 5.0] + p2 = [0.4, 0.5, 0.1] + d2 = openmc.stats.Discrete(x2, p2) + + # Merged distribution should have x values sorted and probabilities + # appropriately combined. Duplicate x values should appear once. + merged = openmc.stats.combine_distributions([d1, d2], [0.6, 0.4]) + assert isinstance(merged, openmc.stats.Discrete) + assert merged.x == pytest.approx([0.0, 0.5, 1.0, 5.0, 10.0]) + assert merged.p == pytest.approx( + [0.6*0.3, 0.4*0.4, 0.6*0.2 + 0.4*0.5, 0.4*0.1, 0.6*0.5]) + + # Probabilities add up but are not normalized + d1 = openmc.stats.Discrete([3.0], [1.0]) + triple = openmc.stats.combine_distributions([d1, d1, d1], [1.0, 2.0, 3.0]) + assert triple.x == pytest.approx([3.0]) + assert triple.p == pytest.approx([6.0]) + + # Combine discrete and tabular + t1 = openmc.stats.Tabular(x2, p2) + mixed = openmc.stats.combine_distributions([d1, t1], [0.5, 0.5]) + assert isinstance(mixed, openmc.stats.Mixture) + assert len(mixed.distribution) == 2 + assert len(mixed.probability) == 2 + + # Combine 1 discrete and 2 tabular -- the tabular distributions should + # combine to produce a uniform distribution with mean 0.5. The combined + # distribution should have a mean of 0.25. + t1 = openmc.stats.Tabular([0., 1.], [2.0, 0.0]) + t2 = openmc.stats.Tabular([0., 1.], [0.0, 2.0]) + d1 = openmc.stats.Discrete([0.0], [1.0]) + combined = openmc.stats.combine_distributions([t1, t2, d1], [0.25, 0.25, 0.5]) + assert combined.integral() == pytest.approx(1.0) + + # Sample the combined distribution and make sure the sample mean is within + # uncertainty of the expected value + samples = combined.sample(10_000) + assert_sample_mean(samples, 0.25)