diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8c14279b9..c3da79c048 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: - name: Setup tmate debug session continue-on-error: true - if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} + if: ${{ failure() && contains(env.COMMIT_MESSAGE, '[gha-debug]') }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fe133a22e..d5af016108 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -611,9 +611,7 @@ add_custom_command(TARGET libopenmc POST_BUILD #=============================================================================== # Install executable, scripts, manpage, license #=============================================================================== - -configure_file(cmake/OpenMCConfig.cmake.in "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" @ONLY) -configure_file(cmake/OpenMCConfigVersion.cmake.in "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" @ONLY) +include(CMakePackageConfigHelpers) set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/OpenMC) install(TARGETS openmc libopenmc @@ -627,10 +625,24 @@ install(EXPORT openmc-targets NAMESPACE OpenMC:: DESTINATION ${INSTALL_CONFIGDIR}) +configure_package_config_file( + "cmake/OpenMCConfig.cmake.in" + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" + INSTALL_DESTINATION ${INSTALL_CONFIGDIR} +) + +write_basic_package_version_file( + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" + VERSION ${OPENMC_VERSION} + COMPATIBILITY AnyNewerVersion +) + install(FILES - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" - DESTINATION ${INSTALL_CONFIGDIR}) + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" + DESTINATION "${INSTALL_CONFIGDIR}" +) + install(FILES man/man1/openmc.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES LICENSE DESTINATION "${CMAKE_INSTALL_DOCDIR}" RENAME copyright) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index cc837bba71..0e15060a37 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -1,12 +1,18 @@ -get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) +@PACKAGE_INIT@ -# Compute the install prefix from this file's location -get_filename_component(_OPENMC_PREFIX "${OpenMC_CMAKE_DIR}/../../.." ABSOLUTE) +include("${CMAKE_CURRENT_LIST_DIR}/OpenMCConfigVersion.cmake") +include(CMakeFindDependencyMacro) + +# Explicitly calculate prefix if it was not generated above +if(NOT DEFINED PACKAGE_PREFIX_DIR) + get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) +endif() + +find_dependency(fmt CONFIG REQUIRED HINTS ${PACKAGE_PREFIX_DIR}) +find_dependency(pugixml CONFIG REQUIRED HINTS ${PACKAGE_PREFIX_DIR}) -find_package(fmt CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(pugixml CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) if(@OPENMC_USE_DAGMC@) - find_package(DAGMC REQUIRED HINTS @DAGMC_DIR@) + find_dependency(DAGMC REQUIRED HINTS @DAGMC_DIR@) endif() if(@OPENMC_USE_LIBMESH@) @@ -16,20 +22,24 @@ if(@OPENMC_USE_LIBMESH@) pkg_check_modules(LIBMESH REQUIRED @LIBMESH_PC_FILE@>=1.7.0 IMPORTED_TARGET) endif() -find_package(PNG) - -if(NOT TARGET OpenMC::libopenmc) - include("${OpenMC_CMAKE_DIR}/OpenMCTargets.cmake") +if("@PNG_FOUND@") + find_dependency(PNG) endif() if(@OPENMC_USE_MPI@) - find_package(MPI REQUIRED) + find_dependency(MPI REQUIRED) endif() if(@OPENMC_USE_OPENMP@) - find_package(OpenMP REQUIRED) + find_dependency(OpenMP REQUIRED) endif() if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") endif() + +include("${CMAKE_CURRENT_LIST_DIR}/OpenMCTargets.cmake") + +if(NOT OpenMC_FIND_QUIETLY) + message(STATUS "Found OpenMC: ${PACKAGE_VERSION} (found in ${PACKAGE_PREFIX_DIR})") +endif() diff --git a/cmake/OpenMCConfigVersion.cmake.in b/cmake/OpenMCConfigVersion.cmake.in deleted file mode 100644 index 90d345de44..0000000000 --- a/cmake/OpenMCConfigVersion.cmake.in +++ /dev/null @@ -1,11 +0,0 @@ -set(PACKAGE_VERSION "@OPENMC_VERSION@") - -# Check whether the requested PACKAGE_FIND_VERSION is compatible -if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") - set(PACKAGE_VERSION_COMPATIBLE FALSE) -else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") - set(PACKAGE_VERSION_EXACT TRUE) - endif() -endif() diff --git a/docs/source/io_formats/collision_track.rst b/docs/source/io_formats/collision_track.rst index 8e1e00ffb8..4123fdac6b 100644 --- a/docs/source/io_formats/collision_track.rst +++ b/docs/source/io_formats/collision_track.rst @@ -10,7 +10,7 @@ may also be written after each batch when multiple files are requested (``collision_track.N.h5``) or when the run is performed in parallel. The file contains the information needed to reconstruct each recorded collision. -The current revision of the collision track file format is 1.1. +The current revision of the collision track file format is 1.2. **/** @@ -33,7 +33,7 @@ The current revision of the collision track file format is 1.1. - ``event_mt`` (*int*) -- ENDF MT number identifying the reaction. - ``delayed_group`` (*int*) -- Delayed neutron group index (non-zero for delayed events). - ``cell_id`` (*int*) -- ID of the cell in which the collision occurred. - - ``nuclide_id`` (*int*) -- ZA identifier of the nuclide (ZZZAAAM format). + - ``nuclide_id`` (*int*) -- PDG number of the nuclide (100ZZZAAAM). - ``material_id`` (*int*) -- ID of the material containing the collision site. - ``universe_id`` (*int*) -- ID of the universe containing the collision site. - ``n_collision`` (*int*) -- Collision counter for the particle history. diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index cef3bb79c8..60ff0afaef 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -406,24 +406,55 @@ Each ```` element can have the following attributes or sub-eleme *Default*: None - :material_overrides: - This element contains information on material overrides to be applied to the - DAGMC universe. It has the following attributes and sub-elements: + :cell: + Zero or more ```` sub-elements may appear to override properties of + individual DAGMC volumes. Each ```` element supports the following + attributes and sub-elements: - :cell: - Material override information for a single cell. It contains the following - attributes and sub-elements: + :id: + The integer cell ID in the DAGMC geometry to override. Required. - :id: - The cell ID in the DAGMC geometry for which the material override will - apply. + :name: + An optional string label for the cell. - :materials: - A list of material IDs that will apply to instances of the cell. If the - list contains only one ID, it will replace the original material - assignment of all instances of the DAGMC cell. If the list contains more - than one material, each material ID of the list will be assigned to the - various instances of the DAGMC cell. + *Default*: None + + :material: + The material ID to assign to this cell. Use ``void`` for vacuum. Multiple + space-separated IDs may be given to specify a distribmat (distributed + material) assignment. Required. + + :temperature: + Temperature(s) in [K] to assign to the cell. Must be ≥ 0. Multiple + space-separated values may be given. + + *Default*: None + + :density: + Density in [g/cm³] to assign to the cell. Must be > 0. Requires a non-void + material fill. Multiple space-separated values may be given. + + *Default*: None + + :volume: + Volume of the cell in [cm³]. + + .. note:: DAGMC can compute cell volumes exactly from the triangulated + mesh surfaces. Specifying a manual volume risks inconsistency + with that capability. + + *Default*: None + + The following standard ```` attributes are **not** supported inside + ```` and will raise an error if present: ``region``, + ``fill``, ``universe``, ``translation``, ``rotation``. + + .. deprecated:: + The ```` sub-element (containing ```` + children with ````) is deprecated. A deprecation warning is + emitted and the overrides are converted to the ```` format at parse + time. It is an error to specify both ```` and + ```` sub-elements on the same ````. *Default*: None diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index f8b064a37d..fb02159169 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -98,6 +98,11 @@ sub-elements: A list of strings representing the nuclide, to define specific define specific target nuclide collisions to be banked. + .. note:: + Electron and positron collision-track events are not associated with + a specific nuclide. If a ``nuclides`` entry is specified, these events + are omitted. + *Default*: None :reactions: @@ -606,30 +611,30 @@ found in the :ref:`random ray user guide `. *Default*: None :adjoint_source: - Specifies an adjoint fixed source for adjoint transport simulations, and - follows the format for :ref:`source_element`. The distributions which make - up the adjoint source are subject to the same restrictions as forward + Specifies an adjoint fixed source for adjoint transport simulations, and + follows the format for :ref:`source_element`. The distributions which make + up the adjoint source are subject to the same restrictions as forward fixed sources in Random Ray mode. *Default*: None - + :adjoint: - Specifies whether to perform adjoint transport. The default is 'False', + Specifies whether to perform adjoint transport. The default is 'False', corresponding to forward transport. *Default*: None - + :volume_estimator: - Specifies choice of volume estimator for the random ray solver. Options + Specifies choice of volume estimator for the random ray solver. Options are 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. *Default*: None :volume_normalized_flux_tallies: - Specifies whether to normalize flux tallies by volume (bool). The - default is 'False'. When enabled, flux tallies will be reported in units - of cm/cm^3. When disabled, flux tallies will be reported in units of cm - (i.e., total distance traveled by neutrons in the spatial tally + Specifies whether to normalize flux tallies by volume (bool). The + default is 'False'. When enabled, flux tallies will be reported in units + of cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally region). *Default*: None @@ -741,14 +746,16 @@ pseudo-random number generator. *Default*: 1 --------------------- -```` Element --------------------- +----------------------------------- +```` Element +----------------------------------- -The ``stride`` element is used to specify how many random numbers are allocated -for each source particle history. - - *Default*: 152,917 + The ``shared_secondary_bank`` element indicates whether to use a shared + secondary particle bank. When enabled, secondary particles are collected into + a global bank, sorted for reproducibility, and load-balanced across MPI ranks + between generations. If not specified, the shared secondary bank is enabled + automatically for fixed-source simulations with weight windows active, and + disabled otherwise. .. _source_element: @@ -1156,23 +1163,6 @@ based on constraints. *Default*: 0.05 -------------------------- -```` Element -------------------------- - -The ```` element indicates at what batches a state point file -should be written. A state point file can be used to restart a run or to get -tally results at any batch. The default behavior when using this tag is to -write out the source bank in the state_point file. This behavior can be -customized by using the ```` element. This element has the -following attributes/sub-elements: - - :batches: - A list of integers separated by spaces indicating at what batches a state - point file should be written. - - *Default*: Last batch only - -------------------------- ```` Element -------------------------- @@ -1222,6 +1212,32 @@ attributes/sub-elements: *Default*: false +------------------------- +```` Element +------------------------- + +The ```` element indicates at what batches a state point file +should be written. A state point file can be used to restart a run or to get +tally results at any batch. The default behavior when using this tag is to +write out the source bank in the state_point file. This behavior can be +customized by using the ```` element. This element has the +following attributes/sub-elements: + + :batches: + A list of integers separated by spaces indicating at what batches a state + point file should be written. + + *Default*: Last batch only + +-------------------- +```` Element +-------------------- + +The ``stride`` element is used to specify how many random numbers are allocated +for each source particle history. + + *Default*: 152,917 + ------------------------------ ```` Element ------------------------------ @@ -1746,11 +1762,11 @@ mesh-based weight windows. The ratio of the lower to upper weight window bounds. *Default*: 5.0 - + For FW-CADIS: :targets: - A sequence of IDs corresponding to the tallies which cover phase + A sequence of IDs corresponding to the tallies which cover phase space regions of interest for local variance reduction. *Default*: None diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 261900ce61..9a22adf016 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -449,3 +449,42 @@ to transfer xenon from one material to another, you'd use:: ... integrator.add_transfer_rate(mat1, ['Xe'], 0.1, destination_material=mat2) + +Comparing to Other Codes +======================== + +Comparing depletion results from OpenMC with those from another code, such as +MCNP or Serpent, requires more than constructing equivalent transport models. +At each depletion step, differences in the transport solution, nuclear data, +reaction rate normalization, and numerical integration can all affect the +result. Small differences can also accumulate over successive depletion steps. + +For a meaningful comparison, align as many of the following inputs and methods +as possible: + +- Geometry and material definitions and associated physical properties such as + temperature +- Neutron cross section library (e.g., ENDF/B-VIII.0) +- Treatment of thermal scattering and unresolved resonance probability tables +- Neutron reactions accounted for in the depletion chain +- Decay data in the depletion chain +- Isomeric branching ratios for reactions in the depletion chain +- Fission product yields in the depletion chain +- Fission product yield interpolation method + (``CoupledOperator(fission_yield_mode=...)``) +- Reaction rate normalization, including fission Q values + (``CoupledOperator(fission_q=...)``) +- Depletion integration method (``PredictorIntegrator``, ``CECMIntegrator``, + etc.) and time-step sizes + +When comparing to codes that use ACE format cross sections, it is recommended to +directly convert the ACE files to HDF5 format using functionality from the +:mod:`openmc.data` module (see :ref:`create_xs_library`). Some of the +LANL-distributed ACE libraries used with MCNP have also been converted to HDF5 +format and are available for download at https://openmc.org/data. + +Even after these choices have been aligned, exact agreement should not be +expected. Codes may use different approximations or numerical methods that +cannot be configured identically. When investigating a discrepancy, first +compare transport results and one-group reaction rates at the initial time, then +compare changes over subsequent timesteps. diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index fe6ab0826f..ed3e8564af 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -97,7 +97,15 @@ VTK Mesh File Generation ------------------------ VTK files of OpenMC meshes can be created using the -:meth:`openmc.Mesh.write_data_to_vtk` method. Data can be applied to the +:meth:`openmc.Mesh.write_data_to_vtk` method. This method supports several VTK +formats depending on the mesh type. Structured meshes +(:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, +:class:`~openmc.CylindricalMesh`, and :class:`~openmc.SphericalMesh`) can be +exported to legacy VTK format (``.vtk``). The :class:`~openmc.UnstructuredMesh` +class supports VTK unstructured grid formats (``.vtu``) as well as an HDF5-based +format (``.vtkhdf``) that does not require the ``vtk`` module to write. + +Data can be applied to the elements of the resulting mesh from mesh filter objects. This data can be provided either as a flat array or, in the case of structured meshes (:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index d35aff83b7..06d4e50ed7 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1105,10 +1105,15 @@ external source is present in the problem. Simulation settings (e.g., number of rays, batches, etc.) will be identical for both calculations. At the conclusion of the run, all results (e.g., tallies, plots, etc.) will be derived from the adjoint flux rather than the forward flux but are not labeled -any differently. The initial forward flux solution will not be stored or -available in the final statepoint file. Those wishing to do analysis requiring -both the forward and adjoint solutions will need to run two separate -simulations and load both statepoint files. +any differently. When an initial forward solve is performed (i.e., when no +user-specified adjoint source is present), its output files are also written to +disk with a ``forward`` infix, so they are not overwritten by the subsequent +adjoint solve. This applies to the statepoint, ``tallies.out``, and any voxel +plots, e.g., ``statepoint.forward.N.h5`` and ``tallies.forward.out``; the +adjoint solve keeps the usual file names. This allows analyses requiring both +the forward and adjoint solutions to be performed from a single run. When +generating FW-CADIS weight windows, no weight window file is written for the +forward solve, as only the final adjoint-derived weight windows are meaningful. .. note:: Use of the automated diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 8ac07f3c89..e8514b8561 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -792,6 +792,11 @@ collision_track.h5 file at the end of the simulation. The file contains 300 recorded collisions that occurred in materials with IDs 1 or 2, involving fission or (n,2n) reactions on the nuclides U-238 or O-16, within cells with IDs 5 and 12. + +.. note:: + Electron and positron collision-track events are not associated with a + specific nuclide. If a ``nuclides`` entry is specified, these events are omitted. + The file can be read using :func:`openmc.read_collision_track_file`. The example below shows how to extract the data from the collision_track feature and displays the fields stored in the file: diff --git a/include/openmc/bank.h b/include/openmc/bank.h index c4e940bc87..6abcdd7f18 100644 --- a/include/openmc/bank.h +++ b/include/openmc/bank.h @@ -34,18 +34,24 @@ extern vector> ifp_fission_lifetime_bank; extern vector progeny_per_particle; +extern SharedArray shared_secondary_bank_read; +extern SharedArray shared_secondary_bank_write; + } // namespace simulation //============================================================================== // Non-member functions //============================================================================== -void sort_fission_bank(); +void sort_bank(SharedArray& bank, bool is_fission_bank); void free_memory_bank(); void init_fission_bank(int64_t max); +int64_t synchronize_global_secondary_bank( + SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_BANK_H diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 911654d318..6b78145a4c 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -123,8 +123,13 @@ int openmc_new_filter(const char* type, int32_t* index); int openmc_next_batch(int* status); int openmc_nuclide_name(int index, const char** name); int openmc_plot_geometry(); +// Deprecated; use openmc_slice_data. int openmc_id_map(const void* slice, int32_t* data_out); +// Deprecated; use openmc_slice_data. int openmc_property_map(const void* slice, double* data_out); +int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool show_overlaps, int level, + int32_t filter_index, int32_t* geom_data, double* property_data); int openmc_get_plot_index(int32_t id, int32_t* index); int openmc_plot_get_id(int32_t index, int32_t* id); int openmc_plot_set_id(int32_t index, int32_t id); diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 26c34ebd41..1a27ad5160 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -145,6 +145,30 @@ private: bool simple_; //!< Does the region contain only intersections? }; +//============================================================================== +// XML parsing helpers for nodes +//============================================================================== + +//! Parse material IDs from a XML node. +//! \param node XML node containing a "material" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of material IDs (MATERIAL_VOID for "void") +vector parse_cell_material_xml(pugi::xml_node node, int32_t cell_id); + +//! Parse temperatures in [K] from a XML node. +//! Validates that all values are non-negative and the list is non-empty. +//! \param node XML node containing a "temperature" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of temperatures in [K] +vector parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id); + +//! Parse densities in [g/cm³] from a XML node. +//! Validates that all values are positive and the list is non-empty. +//! \param node XML node containing a "density" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of densities in [g/cm³] +vector parse_cell_density_xml(pugi::xml_node node, int32_t cell_id); + //============================================================================== class Cell { diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 0b425a673d..26b4a224c0 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -35,7 +35,7 @@ constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; -constexpr array VERSION_COLLISION_TRACK {1, 1}; +constexpr array VERSION_COLLISION_TRACK {1, 2}; // ============================================================================ // ADJUSTABLE PARAMETERS @@ -368,6 +368,7 @@ enum class SolverType { MONTE_CARLO, RANDOM_RAY }; enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; enum class RandomRaySampleMethod { PRNG, HALTON, S2 }; +enum class RandomRaySolve { FORWARD, FORWARD_FOR_ADJOINT, ADJOINT }; //============================================================================== // Geometry Constants diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 0e27402a15..858636c0bc 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -94,6 +94,10 @@ private: class DAGUniverse : public Universe { public: + using MaterialOverrides = std::unordered_map>; + using TemperatureOverrides = std::unordered_map>; + using DensityOverrides = std::unordered_map>; + explicit DAGUniverse(pugi::xml_node node); //! Create a new DAGMC universe @@ -112,6 +116,9 @@ public: //! Initialize the DAGMC accel. data structures, indices, material //! assignments, etc. void initialize(); + void initialize(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides = {}); //! Reads UWUW materials and returns an ID map void read_uwuw_materials(); @@ -146,7 +153,8 @@ public: //! Assign a material overriding normal assignement to a cell //! \param[in] c The OpenMC cell to which the material is assigned - void override_assign_material(std::unique_ptr& c) const; + void override_assign_material(std::unique_ptr& c, + const MaterialOverrides& material_overrides) const; //! Return the index into the model cells vector for a given DAGMC volume //! handle in the universe @@ -187,7 +195,9 @@ private: void set_id(); //!< Deduce the universe id from model::universes void init_dagmc(); //!< Create and initialise DAGMC pointer void init_metadata(); //!< Create and initialise dagmcMetaData pointer - void init_geometry(); //!< Create cells and surfaces from DAGMC entities + void init_geometry(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides); std::string filename_; //!< Name of the DAGMC file used to create this universe @@ -201,11 +211,6 @@ private: //!< generate new material IDs for the universe bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard" //!< volume - std::unordered_map> - material_overrides_; //!< Map of material overrides - //!< keys correspond to the DAGMCCell id - //!< values are a list of material ids used - //!< for the override }; //============================================================================== diff --git a/include/openmc/error.h b/include/openmc/error.h index d73795aee2..1f6e15f49c 100644 --- a/include/openmc/error.h +++ b/include/openmc/error.h @@ -64,14 +64,14 @@ void write_message( int level, const std::string& message, const Params&... fmt_args) { if (settings::verbosity >= level) { - write_message(fmt::format(message, fmt_args...)); + write_message(fmt::format(fmt::runtime(message), fmt_args...)); } } template void write_message(const std::string& message, const Params&... fmt_args) { - write_message(fmt::format(message, fmt_args...)); + write_message(fmt::format(fmt::runtime(message), fmt_args...)); } #ifdef OPENMC_MPI diff --git a/include/openmc/event.h b/include/openmc/event.h index 2d215a10e4..48a009f2b4 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -112,6 +112,19 @@ void process_collision_events(); //! \param n_particles The number of particles in the particle buffer void process_death_events(int64_t n_particles); +//! Process event queues until all are empty. Each iteration processes the +//! longest queue first to maximize vectorization efficiency. +void process_transport_events(); + +//! Initialize secondary particles from a shared secondary bank for +//! event-based transport +// +//! \param n_particles The number of particles to initialize +//! \param offset The offset index in the shared secondary bank +//! \param shared_secondary_bank The shared secondary bank to read from +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_EVENT_H diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 107cc7d1f3..43cf9bb585 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -3,9 +3,12 @@ #include #include +#include +#include #include "openmc/array.h" #include "openmc/constants.h" +#include "openmc/random_ray/source_region.h" // For hash_combine #include "openmc/vector.h" namespace openmc { @@ -13,6 +16,34 @@ namespace openmc { class BoundaryInfo; class GeometryState; +//============================================================================== +//! OverlapKey to store cell and universe data of a single overlap, along with +//! a functor for hashing an OverlapKey into an unordered_map. +//============================================================================== + +struct OverlapKey { + int universe_id; + int cell1_id; + int cell2_id; + + bool operator==(const OverlapKey& other) const + { + return universe_id == other.universe_id && cell1_id == other.cell1_id && + cell2_id == other.cell2_id; + } +}; + +struct OverlapKeyHash { + std::size_t operator()(const OverlapKey& k) const + { + size_t seed = 0; + hash_combine(seed, k.universe_id); + hash_combine(seed, k.cell1_id); + hash_combine(seed, k.cell2_id); + return seed; + } +}; + //============================================================================== // Global variables //============================================================================== @@ -24,6 +55,10 @@ extern "C" int n_coord_levels; //!< Number of CSG coordinate levels extern vector overlap_check_count; +// Overlap data structures get cleared every slice_data run +extern vector overlap_keys; +extern std::unordered_map overlap_key_index; + } // namespace model //============================================================================== @@ -38,8 +73,7 @@ inline bool coincident(double d1, double d2) //============================================================================== //! Check for overlapping cells at a particle's position. //============================================================================== - -bool check_cell_overlap(GeometryState& p, bool error = true); +int check_cell_overlap(GeometryState& p, bool error = true); //============================================================================== //! Get the cell instance for a particle at the specified universe level diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index dcc0e21fe2..f322ad28ac 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -233,5 +233,17 @@ void get_energy_index( double standard_normal_cdf(double z); +//============================================================================== +//! Return true if two floating-point values are approximately equal within a +//! combined relative and absolute tolerance. +//! +//! \param a first floating point value +//! \param b second floating point value +//! \param rel_tol relative tolerance +//! \param abs_tol absolute tolerance +//! \return true if a and b are approximately equal, false otherwise +//============================================================================== +bool isclose(double a, double b, double rel_tol, double abs_tol); + } // namespace openmc #endif // OPENMC_MATH_FUNCTIONS_H diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 7a8b2acadd..ae39a53ddf 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -84,6 +84,9 @@ public: double collapse_rate(int MT, double temperature, span energy, span flux) const; + //! Return a ParticleType object representing this nuclide + ParticleType particle_type() const { return {Z_, A_, metastable_}; } + //============================================================================ // Data members std::string name_; //!< Name of nuclide, e.g. "U235" diff --git a/include/openmc/particle.h b/include/openmc/particle.h index e75f3785a5..8db9721baa 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -71,7 +71,8 @@ public: void event_advance(); void event_cross_surface(); void event_collide(); - void event_revive_from_secondary(); + void event_revive_from_secondary(const SourceSite& site); + void event_check_limit_and_revive(); void event_death(); //! pulse-height recording diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 5b632bbce5..f72948f6eb 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -50,8 +50,11 @@ struct SourceSite { // Extra attributes that don't show up in source written to file int parent_nuclide {-1}; - int64_t parent_id; - int64_t progeny_id; + int64_t parent_id {0}; + int64_t progeny_id {0}; + double wgt_born {1.0}; + double wgt_ww_born {-1.0}; + int64_t n_split {0}; }; struct CollisionTrackSite { @@ -533,14 +536,14 @@ private: uint64_t seeds_[N_STREAMS]; int stream_; - vector secondary_bank_; + vector local_secondary_bank_; // Keep track of how many secondary particles were created in the collision // and what the starting index is in the secondary bank for this particle int n_secondaries_ {0}; int secondary_bank_index_ {0}; - int64_t current_work_; + int64_t current_work_ {0}; vector flux_derivs_; @@ -563,7 +566,9 @@ private: int n_event_ {0}; - int n_split_ {0}; + int64_t n_tracks_ {0}; //!< number of tracks in this particle history + + int64_t n_split_ {0}; double ww_factor_ {0.0}; int64_t n_progeny_ {0}; @@ -693,12 +698,14 @@ public: int& stream() { return stream_; } // secondary particle bank - SourceSite& secondary_bank(int i) { return secondary_bank_[i]; } - const SourceSite& secondary_bank(int i) const { return secondary_bank_[i]; } - decltype(secondary_bank_)& secondary_bank() { return secondary_bank_; } - decltype(secondary_bank_) const& secondary_bank() const + SourceSite& local_secondary_bank(int i) { return local_secondary_bank_[i]; } + const SourceSite& local_secondary_bank(int i) const { - return secondary_bank_; + return local_secondary_bank_[i]; + } + decltype(local_secondary_bank_)& local_secondary_bank() + { + return local_secondary_bank_; } // Number of secondaries created in a collision @@ -747,13 +754,16 @@ public: int& n_event() { return n_event_; } // Number of times variance reduction has caused a particle split - int n_split() const { return n_split_; } - int& n_split() { return n_split_; } + int64_t n_split() const { return n_split_; } + int64_t& n_split() { return n_split_; } // Particle-specific factor for on-the-fly weight window adjustment double ww_factor() const { return ww_factor_; } double& ww_factor() { return ww_factor_; } + // Number of tracks in this particle history + int64_t& n_tracks() { return n_tracks_; } + // Number of progeny produced by this particle int64_t& n_progeny() { return n_progeny_; } diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 6c00fc2f6b..ba8ac84a1c 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "openmc/tensor.h" #include "pugixml.hpp" @@ -18,6 +19,8 @@ #include "openmc/position.h" #include "openmc/random_lcg.h" #include "openmc/ray.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/filter_match.h" #include "openmc/xml_interface.h" namespace openmc { @@ -148,11 +151,12 @@ public: struct IdData { // Constructor - IdData(size_t h_res, size_t v_res); + IdData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); - void set_overlap(size_t y, size_t x); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members tensor::Tensor data_; //!< 2D array of cell & material ids @@ -160,16 +164,34 @@ struct IdData { struct PropertyData { // Constructor - PropertyData(size_t h_res, size_t v_res); + PropertyData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); - void set_overlap(size_t y, size_t x); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members tensor::Tensor data_; //!< 2D array of temperature & density data }; +struct RasterData { + // Constructor + RasterData(size_t h_res, size_t v_res, bool include_filter = false); + + // Methods + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x, int overlap_idx); + + // Members + tensor::Tensor + id_data_; //!< [v_res, h_res, 3 or 4]: cell, instance, mat, [filter_bin] + tensor::Tensor + property_data_; //!< [v_res, h_res, 2]: temperature, density + bool include_filter_; //!< Whether filter bin index is included +}; + //=============================================================================== // Plot class //=============================================================================== @@ -177,7 +199,7 @@ struct PropertyData { class SlicePlotBase { public: template - T get_map() const; + T get_map(int32_t filter_index = -1) const; enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; @@ -188,70 +210,65 @@ public: // Members public: - Position origin_; //!< Plot origin in geometry - Position width_; //!< Plot width in geometry - PlotBasis basis_; //!< Plot basis (XY/XZ/YZ) - array pixels_; //!< Plot size in pixels - bool slice_color_overlaps_; //!< Show overlapping cells? - int slice_level_ {-1}; //!< Plot universe level + Position origin_; //!< Plot origin in geometry + Direction u_span_; //!< Full-width span vector in geometry + Direction v_span_; //!< Full-height span vector in geometry + array pixels_; //!< Plot size in pixels + bool show_overlaps_; //!< Show overlapping cells? + int slice_level_ {-1}; //!< Plot universe level private: }; template -T SlicePlotBase::get_map() const +T SlicePlotBase::get_map(int32_t filter_index) const { size_t width = pixels_[0]; size_t height = pixels_[1]; - // get pixel size - double in_pixel = (width_[0]) / static_cast(width); - double out_pixel = (width_[1]) / static_cast(height); - - // size data array - T data(width, height); - - // setup basis indices and initial position centered on pixel - int in_i, out_i; - Position xyz = origin_; - switch (basis_) { - case PlotBasis::xy: - in_i = 0; - out_i = 1; - break; - case PlotBasis::xz: - in_i = 0; - out_i = 2; - break; - case PlotBasis::yz: - in_i = 1; - out_i = 2; - break; - default: - UNREACHABLE(); + // Determine if filter is being used + bool include_filter = (filter_index >= 0); + Filter* filter = nullptr; + if (include_filter) { + filter = model::tally_filters[filter_index].get(); } - // set initial position - xyz[in_i] = origin_[in_i] - width_[0] / 2. + in_pixel / 2.; - xyz[out_i] = origin_[out_i] + width_[1] / 2. - out_pixel / 2.; + // size data array + T data(width, height, include_filter); - // arbitrary direction - Direction dir = {1. / std::sqrt(2.), 1. / std::sqrt(2.), 0.0}; + // compute pixel steps and top-left pixel center + Direction u_step = u_span_ / static_cast(width); + Direction v_step = v_span_ / static_cast(height); + + Position start = + origin_ - 0.5 * u_span_ + 0.5 * v_span_ + 0.5 * u_step - 0.5 * v_step; + + // Validate that span vectors define a valid plane + Position cross = u_span_.cross(v_span_); + if (cross.norm() == 0.0) { + fatal_error("Slice span vectors are invalid (zero area)."); + } + + // Use an arbitrary direction that is not aligned with any coordinate axis. + // The direction has no physical meaning for plotting but is used by + // Surface::sense() to break ties when a pixel is coincident with a surface. + Direction dir = {1.0 / std::sqrt(2.0), 1.0 / std::sqrt(2.0), 0.0}; #pragma omp parallel { - GeometryState p; - p.r() = xyz; + Particle p; + p.r() = start; p.u() = dir; p.coord(0).universe() = model::root_universe; int level = slice_level_; int j {}; + FilterMatch match; #pragma omp for for (int y = 0; y < height; y++) { - p.r()[out_i] = xyz[out_i] - out_pixel * y; + Position row = start - v_step * static_cast(y); for (int x = 0; x < width; x++) { - p.r()[in_i] = xyz[in_i] + in_pixel * x; + p.r() = row + u_step * static_cast(x); p.n_coord() = 1; // local variables bool found_cell = exhaustive_find_cell(p); @@ -260,10 +277,13 @@ T SlicePlotBase::get_map() const j = level; } if (found_cell) { - data.set_value(y, x, p, j); + data.set_value(y, x, p, j, filter, &match); } - if (slice_color_overlaps_ && check_cell_overlap(p, false)) { - data.set_overlap(y, x); + if (show_overlaps_) { + int overlap_idx = check_cell_overlap(p, false); + if (overlap_idx >= 0) { + data.set_overlap(y, x, overlap_idx); + } } } // inner for } @@ -297,6 +317,8 @@ public: void print_info() const override; PlotType type_; //!< Plot type (Slice/Voxel) + Position width_; //!< Axis-aligned width from plot.xml + PlotBasis basis_; //!< Basis from plot.xml for slice plots int meshlines_width_; //!< Width of lines added to the plot int index_meshlines_mesh_ {-1}; //!< Index of the mesh to draw on the plot RGBColor meshlines_color_; //!< Color of meshlines on the plot diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 6f51af34de..09414fd446 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -76,7 +76,10 @@ public: //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; - static bool adjoint_; // If the user wants outputs based on the adjoint flux + // If the user wants outputs based on the adjoint flux + static bool adjoint_requested_; + // The solve currently being executed + static RandomRaySolve solve_; static bool fw_cadis_local_; static double diagonal_stabilization_rho_; // Adjusts strength of diagonal stabilization diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index ccd2cbe476..e186c549f9 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -22,7 +22,7 @@ public: void apply_fixed_sources_and_mesh_domains(); void prepare_fw_fixed_sources_adjoint(); void prepare_local_fixed_sources_adjoint(); - void prepare_adjoint_simulation(bool fw_adjoint); + void prepare_adjoint_simulation(bool from_forward); void simulate(); void output_simulation_results() const; void instability_check( diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 65f2e6bc41..1d2bbe1e8d 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -51,10 +51,10 @@ inline void hash_combine(size_t& seed, const size_t v) // every iteration. struct TallyTask { int tally_idx; - int filter_idx; + int64_t filter_idx; int score_idx; int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + TallyTask(int tally_idx, int64_t filter_idx, int score_idx, int score_type) : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), score_type(score_type) {} @@ -690,7 +690,7 @@ private: // Private Methods // Helper function for indexing - inline int index(int64_t sr, int g) const { return sr * negroups_ + g; } + inline int64_t index(int64_t sr, int g) const { return sr * negroups_ + g; } }; } // namespace openmc diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 0914a0958b..3bba040f5d 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -98,7 +98,8 @@ extern bool uniform_source_sampling; //!< sample sources uniformly? extern bool ufs_on; //!< uniform fission site method on? extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? extern bool use_decay_photons; //!< use decay photons for D1S -extern "C" bool weight_windows_on; //!< are weight windows are enabled? +extern bool use_shared_secondary_bank; //!< Use shared bank for secondaries +extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? extern bool weight_window_checkpoint_collision; //!< enable weight window check diff --git a/include/openmc/shared_array.h b/include/openmc/shared_array.h index 7e9ef28c58..b309ca3f1c 100644 --- a/include/openmc/shared_array.h +++ b/include/openmc/shared_array.h @@ -4,6 +4,8 @@ //! \file shared_array.h //! \brief Shared array data structure +#include // for copy_n + #include "openmc/memory.h" namespace openmc { @@ -30,14 +32,12 @@ public: //! Default constructor. SharedArray() = default; - //! Construct a zero size container with space to hold capacity number of - //! elements. + //! Construct a container with `size` elements and capacity equal to `size`. // - //! \param capacity The number of elements for the container to allocate - //! space for - SharedArray(int64_t capacity) : capacity_(capacity) + //! \param size The number of elements to allocate and initialize + SharedArray(int64_t size) : size_(size), capacity_(size) { - data_ = make_unique(capacity); + data_ = make_unique(size); } //========================================================================== @@ -97,8 +97,26 @@ public: capacity_ = 0; } + //! Push back an element to the array, with capacity and reallocation behavior + //! as if this were a vector. This does not perform any thread safety checks. + //! If the size exceeds the capacity, then the capacity will double just as + //! with a vector. Data will be reallocated and moved to a new pointer and + //! copied in before the new item is appended. Old data will be freed. + void thread_unsafe_append(const T& value) + { + if (size_ == capacity_) { + int64_t new_capacity = capacity_ == 0 ? 8 : 2 * capacity_; + unique_ptr new_data = make_unique(new_capacity); + std::copy_n(data_.get(), size_, new_data.get()); + data_ = std::move(new_data); + capacity_ = new_capacity; + } + data_[size_++] = value; + } + //! Return the number of elements in the container int64_t size() { return size_; } + int64_t size() const { return size_; } //! Resize the container to contain a specified number of elements. This is //! useful in cases where the container is written to in a non-thread safe diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 9a6cf1b213..454752cd27 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -49,6 +49,9 @@ extern const RegularMesh* ufs_mesh; extern vector k_generation; extern vector work_index; +extern int64_t + simulation_tracks_completed; //!< Number of tracks completed on this rank + } // namespace simulation //============================================================================== @@ -59,7 +62,7 @@ extern vector work_index; void allocate_banks(); //! Determine number of particles to transport per process -void calculate_work(); +void calculate_work(int64_t n_particles); //! Initialize nuclear data before a simulation void initialize_data(); @@ -70,8 +73,9 @@ void initialize_batch(); //! Initialize a fission generation void initialize_generation(); -//! Full initialization of a particle history -void initialize_history(Particle& p, int64_t index_source); +//! Full initialization of a particle track +void initialize_particle_track( + Particle& p, int64_t index_source, bool is_secondary); //! Finalize a batch //! @@ -92,16 +96,35 @@ void broadcast_results(); void free_memory_simulation(); -//! Simulate a single particle history (and all generated secondary particles, -//! if enabled), from birth to death +//! Compute unique particle ID from a 1-based source index +//! \param index_source 1-based source index within this rank's work +//! \return globally unique particle ID +int64_t compute_particle_id(int64_t index_source); + +//! Compute the transport RNG seed from a particle ID +//! \param particle_id the particle's globally unique ID +//! \return seed value passed to init_particle_seeds() +int64_t compute_transport_seed(int64_t particle_id); + +//! Simulate a single particle history from birth to death, inclusive of any +//! secondary particles. In shared secondary mode, only a single track is +//! transported and secondaries are deposited into a shared bank instead. void transport_history_based_single_particle(Particle& p); //! Simulate all particle histories using history-based parallelism void transport_history_based(); +//! Simulate all particles using history-based parallelism, with a shared +//! secondary bank +void transport_history_based_shared_secondary(); + //! Simulate all particle histories using event-based parallelism void transport_event_based(); +//! Simulate all particles using event-based parallelism, with a shared +//! secondary bank +void transport_event_based_shared_secondary(); + } // namespace openmc #endif // OPENMC_SIMULATION_H diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 66e6ff764f..ae604b732d 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -97,9 +97,9 @@ public: //! Given already-set filters, set the stride lengths void set_strides(); - int32_t strides(int i) const { return strides_[i]; } + int64_t strides(int i) const { return strides_[i]; } - int32_t n_filter_bins() const { return n_filter_bins_; } + int64_t n_filter_bins() const { return n_filter_bins_; } bool multiply_density() const { return multiply_density_; } @@ -184,9 +184,9 @@ private: vector filters_; //!< Filter indices in global filters array //! Index strides assigned to each filter to support 1D indexing. - vector strides_; + vector strides_; - int32_t n_filter_bins_ {0}; + int64_t n_filter_bins_ {0}; //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index d1aed28318..4303ddb7a9 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -41,7 +41,7 @@ public: FilterBinIter& operator++(); - int index_ {1}; + int64_t index_ {1}; double weight_ {1.}; vector& filter_matches_; diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index a5d404133c..d0b385d169 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -52,8 +52,12 @@ struct WeightWindow { double weight_cutoff {DEFAULT_WEIGHT_CUTOFF}; int max_split {10}; - //! Whether the weight window is in a valid state - bool is_valid() const { return lower_weight >= 0.0; } + //! Whether the weight window is in a valid state. A non-positive lower + //! bound indicates that no weight window information exists at this + //! location (generators mark such cells with -1, and a lower bound of zero + //! conventionally turns the weight window game off in a cell, as in MCNP + //! wwinp files), in which case no weight window game is played. + bool is_valid() const { return lower_weight > 0.0; } //! Adjust the weight window by a constant factor void scale(double factor) diff --git a/openmc/dagmc.py b/openmc/dagmc.py index fd4258225f..75ead5e43c 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -36,12 +36,6 @@ class DAGMCUniverse(openmc.UniverseBase): auto_mat_ids : bool Set IDs automatically on initialization (True) or report overlaps in ID space between OpenMC and UWUW materials (False) - material_overrides : dict, optional - A dictionary of material overrides. The keys are material name strings - and the values are Iterables of openmc.Material objects. If a material - name is found in the DAGMC file, the material will be replaced with the - openmc.Material object in the value. - Attributes ---------- id : int @@ -78,15 +72,6 @@ class DAGMCUniverse(openmc.UniverseBase): The number of surfaces in the model. .. versionadded:: 0.13.2 - material_overrides : dict - A dictionary of material overrides. Keys are cell IDs; values are - iterables of :class:`openmc.Material` objects. The material assignment - of each DAGMC cell ID key will be replaced with the - :class:`~openmc.Material` object in the value. If the value contains - multiple :class:`~openmc.Material` objects, each Material in the list - will be assigned to the corresponding instance of the cell. - - .. versionadded:: 0.15.1 """ def __init__(self, @@ -94,16 +79,12 @@ class DAGMCUniverse(openmc.UniverseBase): universe_id=None, name='', auto_geom_ids=False, - auto_mat_ids=False, - material_overrides=None): + auto_mat_ids=False): super().__init__(universe_id, name) # Initialize class attributes self.filename = filename self.auto_geom_ids = auto_geom_ids self.auto_mat_ids = auto_mat_ids - self._material_overrides = {} - if material_overrides is not None: - self.material_overrides = material_overrides def __repr__(self): string = super().__repr__() @@ -130,47 +111,17 @@ class DAGMCUniverse(openmc.UniverseBase): @property def material_overrides(self): - return self._material_overrides + raise AttributeError( + "DAGMCUniverse.material_overrides has been removed. Use " + "DAGMCCell objects added via add_cell() to manage per-cell " + "material assignments.") @material_overrides.setter def material_overrides(self, val): - cv.check_type('material overrides', val, Mapping) - for key, value in val.items(): - self.add_material_override(key, value) - - def replace_material_assignment(self, material_name: str, material: openmc.Material): - """Replace a material assignment within the DAGMC universe. - - Replace the material assignment of all cells filled with a material in - the DAGMC universe. The universe must be synchronized in an initialized - Model (see :meth:`~openmc.DAGMCUniverse.sync_dagmc_cells`) before - calling this method. - - .. versionadded:: 0.15.1 - - Parameters - ---------- - material_name : str - Material name to replace - material : openmc.Material - Material to replace the material_name with - - """ - if material_name not in self.material_names: - raise ValueError( - f"No material with name '{material_name}' found in the DAGMC universe") - - if not self.cells: - raise RuntimeError("This DAGMC universe has not been synchronized " - "on an initialized Model.") - - for cell in self.cells.values(): - if cell.fill is None: - continue - if isinstance(cell.fill, openmc.Iterable): - cell.fill = list(map(lambda x: material if x.name == material_name else x, cell.fill)) - else: - cell.fill = material if cell.fill.name == material_name else cell.fill + raise AttributeError( + "DAGMCUniverse.material_overrides has been removed. Use " + "DAGMCCell objects added via add_cell() to manage per-cell " + "material assignments.") def add_material_override(self, key, overrides=None): """Add a material override to the universe. @@ -201,7 +152,10 @@ class DAGMCUniverse(openmc.UniverseBase): if key not in self.cells: raise ValueError(f"Cell ID '{key}' not found in DAGMC universe") - self._material_overrides[key] = overrides + if len(overrides) == 1: + self.cells[key].fill = overrides[0] + else: + self.cells[key].fill = list(overrides) @property def auto_geom_ids(self): @@ -290,12 +244,6 @@ class DAGMCUniverse(openmc.UniverseBase): memo.add(self) - # Ensure that the material overrides are up-to-date - for cell in self.cells.values(): - if cell.fill is None: - continue - self.add_material_override(cell, cell.fill) - # Set xml element values dagmc_element = ET.Element('dagmc_universe') dagmc_element.set('id', str(self.id)) @@ -307,17 +255,10 @@ class DAGMCUniverse(openmc.UniverseBase): if self.auto_mat_ids: dagmc_element.set('auto_mat_ids', 'true') dagmc_element.set('filename', str(self.filename)) - if self._material_overrides: - mat_element = ET.Element('material_overrides') - for key in self._material_overrides: - cell_overrides = ET.Element('cell_override') - cell_overrides.set("id", str(key)) - material_element = ET.Element('material_ids') - material_element.text = ' '.join( - str(t.id) for t in self._material_overrides[key]) - cell_overrides.append(material_element) - mat_element.append(cell_overrides) - dagmc_element.append(mat_element) + if self.cells: + for cell in self.cells.values(): + cell_element = cell.create_xml_subelement(xml_element, memo) + dagmc_element.append(cell_element) xml_element.append(dagmc_element) def bounding_region( @@ -442,7 +383,7 @@ class DAGMCUniverse(openmc.UniverseBase): return out @classmethod - def from_xml_element(cls, elem, mats = None): + def from_xml_element(cls, elem, mats=None): """Generate DAGMC universe from XML element Parameters @@ -471,21 +412,56 @@ class DAGMCUniverse(openmc.UniverseBase): out.auto_geom_ids = bool(get_text(elem, "auto_geom_ids")) out.auto_mat_ids = bool(get_text(elem, "auto_mat_ids")) - el_mat_override = elem.find('material_overrides') - if el_mat_override is not None: - if mats is None: - raise ValueError("Material overrides found in DAGMC universe " - "but no materials were provided to populate " - "the mapping.") - out._material_overrides = {} - for elem in el_mat_override.findall('cell_override'): - cell_id = int(get_text(elem, 'id')) - mat_ids = get_elem_list(elem, "material_ids", str) or [] - mat_objs = [mats[mat_id] for mat_id in mat_ids] - out._material_overrides[cell_id] = mat_objs + has_overrides = elem.find('material_overrides') is not None + has_cells = elem.find('cell') is not None + + if has_overrides and has_cells: + raise ValueError( + "DAGMCUniverse cannot specify both and " + " sub-elements. Use elements only.") + + if has_overrides: + warnings.warn( + "DAGMCUniverse is deprecated and will be " + "removed in a future version. Use nested elements " + "instead.", DeprecationWarning, stacklevel=2) + out._parse_legacy_material_overrides(elem, mats) + elif has_cells: + out._parse_cell_overrides(elem, mats) return out + def _parse_legacy_material_overrides(self, elem, mats): + """Parse the deprecated XML format and populate + the universe with equivalent DAGMCCell objects.""" + if mats is None: + raise ValueError( + "DAGMC material overrides found but no materials were " + "provided to populate the mapping.") + mo_elem = elem.find('material_overrides') + for co_elem in mo_elem.findall('cell_override'): + cell_id = int(get_text(co_elem, 'id')) + mat_ids = co_elem.find('material_ids').text.split() + fill_objs = [mats[mid] for mid in mat_ids] + fill = fill_objs[0] if len(fill_objs) == 1 else fill_objs + if cell_id in self.cells: + raise ValueError( + f"Duplicate DAGMC cell override specified for cell {cell_id}.") + self.add_cell(DAGMCCell(cell_id=cell_id, fill=fill)) + + def _parse_cell_overrides(self, elem, mats): + if mats is None: + raise ValueError("DAGMC cell overrides found in DAGMC universe but " + "no materials were provided to populate the " + "mapping.") + + for cell_elem in elem.findall('cell'): + cell_id = int(get_text(cell_elem, 'id')) + if cell_id in self.cells: + raise ValueError( + f"Duplicate DAGMC cell override specified for cell {cell_id}.") + DAGMCCell.from_xml_element(cell_elem, mats, self) + def _partial_deepcopy(self): """Clone all of the openmc.DAGMCUniverse object's attributes except for its cells, as they are copied within the clone function. This should @@ -565,7 +541,13 @@ class DAGMCUniverse(openmc.UniverseBase): fill = [mats_per_id[mat.id] for mat in dag_cell.fill if mat] else: fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None - self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill)) + name = dag_cell.name + if dag_cell_id in self._cells: + self._cells[dag_cell_id].name = name + self._cells[dag_cell_id].fill = fill + else: + self.add_cell( + openmc.DAGMCCell(cell_id=dag_cell_id, name=name, fill=fill)) @add_plot_params def plot(self, *args, **kwargs): @@ -594,6 +576,14 @@ class DAGMCCell(openmc.Cell): DAG_parent_universe : int The parent universe of the cell. + Notes + ----- + DAGMC geometries are composed of triangulated surfaces, which means cell + volumes can in principle be computed exactly (e.g. via mesh-based + integration). Manually specifying :attr:`volume` overrides any such + calculation and may introduce inconsistencies if the value does not + accurately reflect the true geometric volume. + """ def __init__(self, cell_id=None, name='', fill=None): super().__init__(cell_id, name, fill, None) @@ -625,8 +615,62 @@ class DAGMCCell(openmc.Cell): raise TypeError("plot is not available for DAGMC cells.") def create_xml_subelement(self, xml_element, memo=None): - raise TypeError("create_xml_subelement is not available for DAGMC cells.") + if self.fill_type not in ('void', 'material', 'distribmat'): + raise TypeError("DAGMC cell overrides currently only support " + "material fills.") + if self.temperature is not None and self.fill_type not in ( + 'material', 'distribmat' + ): + raise TypeError("DAGMC cell temperature overrides require a " + "material fill.") + if self.density is not None and self.fill_type not in ('material', 'distribmat'): + raise TypeError("DAGMC cell density overrides require a " + "material fill.") + if any(getattr(self, attr) is not None for attr in ('translation', 'rotation')): + raise TypeError("DAGMC cell overrides do not support translation " + "or rotation.") + return super().create_xml_subelement(xml_element, memo) @classmethod - def from_xml_element(cls, elem, surfaces, materials, get_universe): - raise TypeError("from_xml_element is not available for DAGMC cells.") + def from_xml_element(cls, elem, mats, universe): + """Generate a DAGMCCell from an XML override element. + + Parameters + ---------- + elem : lxml.etree._Element + `` element containing a DAGMC cell property override + mats : dict + Dictionary mapping material ID strings to :class:`openmc.Material` + instances + universe : DAGMCUniverse + Universe to add the parsed cell to. + + Returns + ------- + DAGMCCell + DAGMCCell instance + """ + if not isinstance(universe, DAGMCUniverse): + raise TypeError( + f"universe must be a DAGMCUniverse instance, " + f"got {type(universe).__name__}.") + + cell_id = int(get_text(elem, 'id')) + + # Validate attributes that are unsupported for DAGMC cell overrides + for tag in ('region', 'fill', 'universe'): + if get_text(elem, tag) is not None: + raise ValueError( + f"DAGMC cell {cell_id} override cannot specify '{tag}'.") + for tag in ('translation', 'rotation'): + if get_text(elem, tag) is not None: + raise ValueError( + f"DAGMC cell {cell_id} override does not support " + f"'{tag}'.") + if get_elem_list(elem, 'material', str) is None: + raise ValueError( + f"DAGMC cell {cell_id} must specify a material override.") + + return super().from_xml_element( + elem, surfaces={}, materials=mats, + get_universe=lambda _: universe) diff --git a/openmc/data/angle_distribution.py b/openmc/data/angle_distribution.py index e59ffa0c73..778139cc2a 100644 --- a/openmc/data/angle_distribution.py +++ b/openmc/data/angle_distribution.py @@ -10,8 +10,8 @@ from openmc.mixin import EqualityMixin from openmc.stats import Univariate, Tabular, Uniform, Legendre from .function import INTERPOLATION_SCHEME from .data import EV_PER_MEV -from .endf import get_head_record, get_cont_record, get_tab1_record, \ - get_list_record, get_tab2_record +from .endf import as_evaluation, get_head_record, get_cont_record, \ + get_tab1_record, get_list_record, get_tab2_record class AngleDistribution(EqualityMixin): @@ -213,7 +213,7 @@ class AngleDistribution(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation mt : int The MT value of the reaction to get angular distributions for @@ -224,6 +224,7 @@ class AngleDistribution(EqualityMixin): Angular distribution """ + ev = as_evaluation(ev) file_obj = StringIO(ev.section[4, mt]) # Read HEAD record diff --git a/openmc/data/decay.py b/openmc/data/decay.py index ce20a252c3..5f95e12810 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -14,7 +14,8 @@ from openmc.mixin import EqualityMixin from openmc.stats import Discrete, Tabular, Univariate, combine_distributions from .data import gnds_name, zam from .function import INTERPOLATION_SCHEME -from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record +from .endf import ( + as_evaluation, get_head_record, get_list_record, get_tab1_record) # Gives name and (change in A, change in Z) resulting from decay @@ -75,7 +76,7 @@ class FissionProductYields(EqualityMixin): Parameters ---------- - ev_or_filename : str of openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF fission product yield evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -133,11 +134,7 @@ class FissionProductYields(EqualityMixin): return energies, data - # Get evaluation if str is passed - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) # Assign basic nuclide properties self.nuclide = { @@ -164,7 +161,7 @@ class FissionProductYields(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF fission product yield evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -292,7 +289,7 @@ class Decay(EqualityMixin): Parameters ---------- - ev_or_filename : str of openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF radioactive decay data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -323,11 +320,7 @@ class Decay(EqualityMixin): """ def __init__(self, ev_or_filename): - # Get evaluation if str is passed - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) file_obj = StringIO(ev.section[8, 457]) @@ -486,7 +479,7 @@ class Decay(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF radioactive decay data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -649,5 +642,3 @@ def decay_energy(nuclide: str): warn(f"Chain file '{chain_file}' does not have any decay energy.") return _DECAY_ENERGY.get(nuclide, 0.0) - - diff --git a/openmc/data/dose/dose.py b/openmc/data/dose/dose.py index d49043b0a6..87e0cf9aef 100644 --- a/openmc/data/dose/dose.py +++ b/openmc/data/dose/dose.py @@ -4,50 +4,76 @@ import numpy as np import openmc.checkvalue as cv -_FILES = { - ('icrp74', 'neutron'): Path('icrp74') / 'neutrons.txt', - ('icrp74', 'photon'): Path('icrp74') / 'photons.txt', - ('icrp116', 'electron'): Path('icrp116') / 'electrons.txt', - ('icrp116', 'helium'): Path('icrp116') / 'helium_ions.txt', - ('icrp116', 'mu-'): Path('icrp116') / 'negative_muons.txt', - ('icrp116', 'pi-'): Path('icrp116') / 'negative_pions.txt', - ('icrp116', 'neutron'): Path('icrp116') / 'neutrons.txt', - ('icrp116', 'photon'): Path('icrp116') / 'photons.txt', - ('icrp116', 'photon kerma'): Path('icrp116') / 'photons_kerma.txt', - ('icrp116', 'mu+'): Path('icrp116') / 'positive_muons.txt', - ('icrp116', 'pi+'): Path('icrp116') / 'positive_pions.txt', - ('icrp116', 'positron'): Path('icrp116') / 'positrons.txt', - ('icrp116', 'proton'): Path('icrp116') / 'protons.txt', +_FULL_GEOMETRIES = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') +_LIMITED_GEOMETRIES = ('AP', 'PA', 'ISO') + +_TABLES = { + ('icrp74', 'effective', 'neutron'): ( + Path('icrp74') / 'neutrons.txt', _FULL_GEOMETRIES), + ('icrp74', 'effective', 'photon'): ( + Path('icrp74') / 'photons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'electron'): ( + Path('icrp116') / 'electrons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'helium'): ( + Path('icrp116') / 'helium_ions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'mu-'): ( + Path('icrp116') / 'negative_muons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'pi-'): ( + Path('icrp116') / 'negative_pions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'neutron'): ( + Path('icrp116') / 'neutrons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'photon'): ( + Path('icrp116') / 'photons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'photon kerma'): ( + Path('icrp116') / 'photons_kerma.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'mu+'): ( + Path('icrp116') / 'positive_muons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'pi+'): ( + Path('icrp116') / 'positive_pions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'positron'): ( + Path('icrp116') / 'positrons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'proton'): ( + Path('icrp116') / 'protons.txt', _FULL_GEOMETRIES), + ('icrp74', 'ambient', 'neutron'): ( + Path('icrp74') / 'neutrons_H10.txt', None), + ('icrp74', 'ambient', 'photon'): ( + Path('icrp74') / 'photons_H10.txt', None), } _DOSE_TABLES = {} -def _load_dose_icrp(data_source: str, particle: str): - """Load effective dose tables from text files. +def _load_dose_table(data_source: str, dose_quantity: str, particle: str): + """Load dose tables from text files. Parameters ---------- data_source : {'icrp74', 'icrp116'} The dose conversion data source to use + dose_quantity : {'effective', 'ambient'} + Dose quantity to load. 'ambient' corresponds to ambient dose + equivalent H*(10). particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} Incident particle """ - path = Path(__file__).parent / _FILES[data_source, particle] + key = (data_source, dose_quantity, particle) + path = Path(__file__).parent / _TABLES[key][0] data = np.loadtxt(path, skiprows=3, encoding='utf-8') data[:, 0] *= 1e6 # Change energies to eV - _DOSE_TABLES[data_source, particle] = data + _DOSE_TABLES[key] = data -def dose_coefficients(particle, geometry='AP', data_source='icrp116'): - """Return effective dose conversion coefficients. +def dose_coefficients( + particle, geometry='AP', data_source='icrp116', dose_quantity='effective' +): + """Return dose conversion coefficients. - This function provides fluence (and air kerma) to effective or ambient dose - (H*(10)) conversion coefficients for various types of external exposures - based on values in ICRP publications. Corrected values found in a - corrigendum are used rather than the values in the original report. - Available libraries include `ICRP Publication 74 + This function provides fluence (and air kerma) to effective dose or ambient + dose equivalent (H*(10)) conversion coefficients for various types of + external exposures based on values in ICRP publications. Corrected values + found in a corrigendum are used rather than the values in the original + report. Available libraries include `ICRP Publication 74 ` and `ICRP Publication 116 `. @@ -63,45 +89,58 @@ def dose_coefficients(particle, geometry='AP', data_source='icrp116'): particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} Incident particle geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} - Irradiation geometry assumed. Refer to ICRP-116 (Section 3.2) for the - meaning of the options here. + Irradiation geometry assumed for effective dose coefficients. Refer to + ICRP-116 (Section 3.2) for the meaning of the options here. This + argument does not apply when ``dose_quantity`` is 'ambient'. data_source : {'icrp74', 'icrp116'} - The data source for the effective dose conversion coefficients. + The data source for the dose conversion coefficients. + dose_quantity : {'effective', 'ambient'} + Dose quantity to return. 'effective' returns effective dose + coefficients; 'ambient' returns ambient dose equivalent (H*(10)) + coefficients. Returns ------- energy : numpy.ndarray Energies at which dose conversion coefficients are given dose_coeffs : numpy.ndarray - Effective dose coefficients in [pSv cm^2] at provided energies. For - 'photon kerma', the coefficients are given in [Sv/Gy]. + Dose coefficients in [pSv cm^2] at provided energies. For 'photon + kerma', the coefficients are given in [Sv/Gy]. """ - cv.check_value('geometry', geometry, {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'}) + cv.check_value('geometry', geometry, _FULL_GEOMETRIES) cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) + cv.check_value('dose_quantity', dose_quantity, {'effective', 'ambient'}) - if (data_source, particle) not in _FILES: - available_particles = sorted({p for (ds, p) in _FILES if ds == data_source}) + key = (data_source, dose_quantity, particle) + if key not in _TABLES: + available_particles = sorted( + p for ds, dq, p in _TABLES + if ds == data_source and dq == dose_quantity + ) msg = ( - f"'{particle}' has no dose data in data source {data_source}. " - f"Available particles for {data_source} are: {available_particles}" + f"'{particle}' has no {dose_quantity} dose data in data source " + f"{data_source}. Available particles for {data_source} " + f"with dose quantity {dose_quantity} are: {available_particles}" ) raise ValueError(msg) - elif (data_source, particle) not in _DOSE_TABLES: - _load_dose_icrp(data_source, particle) + elif key not in _DOSE_TABLES: + _load_dose_table(data_source, dose_quantity, particle) - # Get all data for selected particle - data = _DOSE_TABLES[data_source, particle] + data = _DOSE_TABLES[key] + columns = _TABLES[key][1] - # Determine index for selected geometry - if particle in ('neutron', 'photon', 'proton', 'photon kerma'): - columns = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') + if columns is None: + if geometry != 'AP': + raise ValueError( + "Irradiation geometry is not defined for ambient dose " + "equivalent coefficients. Use the default geometry='AP'." + ) + index = 0 else: - columns = ('AP', 'PA', 'ISO') - index = columns.index(geometry) + index = columns.index(geometry) - # Pull out energy and dose from table energy = data[:, 0].copy() dose_coeffs = data[:, index + 1].copy() return energy, dose_coeffs diff --git a/openmc/data/dose/icrp74/neutrons_H10.txt b/openmc/data/dose/icrp74/neutrons_H10.txt new file mode 100644 index 0000000000..fe0036cbc9 --- /dev/null +++ b/openmc/data/dose/icrp74/neutrons_H10.txt @@ -0,0 +1,50 @@ +Neutrons: Ambient per fluence, in units of pSv cm², for monoenergetic particles incident. + +Energy (MeV) Dose + 1.00E-09 6.60 + 1.00E-08 9.00 + 2.53E-08 10.6 + 1.00E-07 12.9 + 2.00E-07 13.5 + 5.00E-07 13.6 + 1.00E-06 13.3 + 2.00E-06 12.9 + 5.00E-06 12.0 + 1.00E-05 11.3 + 2.00E-05 10.6 + 5.00E-05 9.90 + 1.00E-04 9.40 + 2.00E-04 8.90 + 5.00E-04 8.30 + 1.00E-03 7.90 + 2.00E-03 7.70 + 5.00E-03 8.00 + 1.00E-02 10.5 + 2.00E-02 16.6 + 3.00E-02 23.7 + 5.00E-02 41.1 + 7.00E-02 60.0 + 1.00E-01 88.0 + 1.50E-01 132 + 2.00E-01 170 + 3.00E-01 233 + 5.00E-01 322 + 7.00E-01 375 + 9.00E-01 400 + 1 416 + 1.2 425 + 2 420 + 3 412 + 4 408 + 5 405 + 6 400 + 7 405 + 8 409 + 9 420 + 10 440 + 12 480 + 14 520 + 15 540 + 16 555 + 18 570 + 20 600 \ No newline at end of file diff --git a/openmc/data/dose/icrp74/photons_H10.txt b/openmc/data/dose/icrp74/photons_H10.txt new file mode 100644 index 0000000000..66031b2f0e --- /dev/null +++ b/openmc/data/dose/icrp74/photons_H10.txt @@ -0,0 +1,28 @@ +Photons: Ambient dose (H*10) per fluence, in units of pSv cm² + +Energy (MeV) Dose +0.010 0.061 +0.015 0.83 +0.020 1.05 +0.030 0.81 +0.040 0.64 +0.050 0.55 +0.060 0.51 +0.080 0.53 +0.100 0.61 +0.150 0.89 +0.200 1.20 +0.300 1.80 +0.400 2.38 +0.500 2.93 +0.600 3.44 +0.800 4.38 +1 5.20 +1.5 6.90 +2 8.60 +3 11.1 +4 13.4 +5 15.5 +6 17.6 +8 21.6 +10 25.6 \ No newline at end of file diff --git a/openmc/data/endf.py b/openmc/data/endf.py index c50431dc7f..edd8afffb3 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -12,7 +12,12 @@ import re from .data import gnds_name from .function import Tabulated1D -from endf.material import _LIBRARY, _SUBLIBRARY, get_materials as get_evaluations +from endf.material import ( + Material, + _LIBRARY, + _SUBLIBRARY, + get_materials as get_evaluations, +) from endf.incident_neutron import SUM_RULES from endf.records import ( float_endf, @@ -44,7 +49,7 @@ class Evaluation: Parameters ---------- - filename_or_obj : str or file-like + filename_or_obj : str, file-like, or endf.Material Path to ENDF file to read or an open file positioned at the start of an ENDF material @@ -64,17 +69,25 @@ class Evaluation: """ def __init__(self, filename_or_obj): + self.section = {} + self.info = {} + self.target = {} + self.projectile = {} + self.reaction_list = [] + + if isinstance(filename_or_obj, Material): + self.section = dict(filename_or_obj.section_text) + self.section_data = filename_or_obj.section_data + self.material = filename_or_obj.MAT + self._read_header() + return + if isinstance(filename_or_obj, (str, PurePath)): fh = open(str(filename_or_obj), 'r') need_to_close = True else: fh = filename_or_obj need_to_close = False - self.section = {} - self.info = {} - self.target = {} - self.projectile = {} - self.reaction_list = [] # Skip TPID record. Evaluators sometimes put in TPID records that are # ill-formated because they lack MF/MT values or put them in the wrong @@ -199,3 +212,10 @@ class Evaluation: self.target['mass_number'], self.target['isomeric_state']) + +def as_evaluation(ev_or_filename): + """Return an object supporting OpenMC's legacy Evaluation interface.""" + if isinstance(ev_or_filename, Evaluation): + return ev_or_filename + else: + return Evaluation(ev_or_filename) diff --git a/openmc/data/fission_energy.py b/openmc/data/fission_energy.py index 3c7998ee21..e4fac087ae 100644 --- a/openmc/data/fission_energy.py +++ b/openmc/data/fission_energy.py @@ -5,7 +5,8 @@ from io import StringIO import openmc.checkvalue as cv from openmc.mixin import EqualityMixin from .data import EV_PER_MEV -from .endf import get_cont_record, get_list_record, get_tab1_record, Evaluation +from .endf import ( + as_evaluation, get_cont_record, get_list_record, get_tab1_record) from .function import Function1D, Tabulated1D, Polynomial, sum_functions @@ -195,7 +196,7 @@ class FissionEnergyRelease(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation incident_neutron : openmc.data.IncidentNeutron Corresponding incident neutron dataset @@ -206,7 +207,7 @@ class FissionEnergyRelease(EqualityMixin): Fission energy release data """ - cv.check_type('evaluation', ev, Evaluation) + ev = as_evaluation(ev) # Check to make sure this ENDF file matches the expected isomer. if ev.target['atomic_number'] != incident_neutron.atomic_number: diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 492cdd7f3b..f17bb3fe42 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -13,7 +13,8 @@ from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Library, Table, get_table, get_metadata from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV, gnds_name from .endf import ( - Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations) + Evaluation, SUM_RULES, as_evaluation, get_head_record, get_tab1_record, + get_evaluations) from .fission_energy import FissionEnergyRelease from .function import Tabulated1D, Sum, ResonancesWithBackground from .njoy import make_ace, make_pendf @@ -652,7 +653,7 @@ class IncidentNeutron(EqualityMixin): Parameters ---------- - ev_or_filename : openmc.data.endf.Evaluation or str + ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -666,10 +667,7 @@ class IncidentNeutron(EqualityMixin): Incident neutron continuous-energy data """ - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) atomic_number = ev.target['atomic_number'] mass_number = ev.target['mass_number'] diff --git a/openmc/data/photon.py b/openmc/data/photon.py index bc21b2e56a..13cd3ec95f 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -15,7 +15,8 @@ from openmc.mixin import EqualityMixin from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Table, get_metadata, get_table from .data import ATOMIC_SYMBOL, EV_PER_MEV -from .endf import Evaluation, get_head_record, get_tab1_record, get_list_record +from .endf import ( + as_evaluation, get_head_record, get_tab1_record, get_list_record) from .function import Tabulated1D @@ -272,7 +273,7 @@ class AtomicRelaxation(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF atomic relaxation evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -282,10 +283,7 @@ class AtomicRelaxation(EqualityMixin): Atomic relaxation data """ - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) # Atomic relaxation data is always MF=28, MT=533 if (28, 533) not in ev.section: @@ -606,10 +604,10 @@ class IncidentPhoton(EqualityMixin): Parameters ---------- - photoatomic : str or openmc.data.endf.Evaluation + photoatomic : str, openmc.data.endf.Evaluation, or endf.Material ENDF photoatomic data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. - relaxation : str or openmc.data.endf.Evaluation, optional + relaxation : str, openmc.data.endf.Evaluation, or endf.Material, optional ENDF atomic relaxation data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -619,10 +617,7 @@ class IncidentPhoton(EqualityMixin): Photon interaction data """ - if isinstance(photoatomic, Evaluation): - ev = photoatomic - else: - ev = Evaluation(photoatomic) + ev = as_evaluation(photoatomic) Z = ev.target['atomic_number'] data = cls(Z) @@ -1071,7 +1066,7 @@ class PhotonReaction(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF photo-atomic interaction data evaluation mt : int The MT value of the reaction to get data for @@ -1082,6 +1077,7 @@ class PhotonReaction(EqualityMixin): Photon reaction data """ + ev = as_evaluation(ev) rx = cls(mt) # Read photon cross section diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index ab1285b721..44ced5511f 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -13,8 +13,8 @@ from .angle_distribution import AngleDistribution from .angle_energy import AngleEnergy from .correlated import CorrelatedAngleEnergy from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV -from .endf import get_head_record, get_tab1_record, get_list_record, \ - get_tab2_record, get_cont_record +from .endf import as_evaluation, get_head_record, get_tab1_record, \ + get_list_record, get_tab2_record, get_cont_record from .energy_distribution import EnergyDistribution, LevelInelastic, \ DiscretePhoton from .function import Tabulated1D, Polynomial @@ -1151,7 +1151,7 @@ class Reaction(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation mt : int The MT value of the reaction to get data for @@ -1162,6 +1162,7 @@ class Reaction(EqualityMixin): Reaction data """ + ev = as_evaluation(ev) rx = Reaction(mt) # Integrated cross section diff --git a/openmc/data/resonance.py b/openmc/data/resonance.py index 31e230df58..52ee7f1a9e 100644 --- a/openmc/data/resonance.py +++ b/openmc/data/resonance.py @@ -7,7 +7,9 @@ import pandas as pd import openmc.checkvalue as cv from .data import NEUTRON_MASS -from .endf import get_head_record, get_cont_record, get_tab1_record, get_list_record +from .endf import ( + as_evaluation, get_head_record, get_cont_record, get_tab1_record, + get_list_record) try: from .reconstruct import wave_number, penetration_shift, reconstruct_mlbw, \ reconstruct_slbw, reconstruct_rm @@ -77,7 +79,7 @@ class Resonances: Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation Returns @@ -86,6 +88,7 @@ class Resonances: Resonance data """ + ev = as_evaluation(ev) file_obj = io.StringIO(ev.section[2, 151]) # Determine whether discrete or continuous representation diff --git a/openmc/data/resonance_covariance.py b/openmc/data/resonance_covariance.py index 7096570449..aa86ae33ce 100644 --- a/openmc/data/resonance_covariance.py +++ b/openmc/data/resonance_covariance.py @@ -74,7 +74,7 @@ class ResonanceCovariances(Resonances): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation resonances : openmc.data.Resonance object openmc.data.Resonanance object generated from the same evaluation @@ -86,6 +86,7 @@ class ResonanceCovariances(Resonances): Resonance covariance data """ + ev = endf.as_evaluation(ev) file_obj = io.StringIO(ev.section[32, 151]) # Determine whether discrete or continuous representation diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 54e3a73305..3b910c535f 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -1042,7 +1042,7 @@ class ThermalScattering(EqualityMixin): Parameters ---------- - ev_or_filename : openmc.data.endf.Evaluation or str + ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. divide_incoherent_elastic : bool @@ -1056,10 +1056,7 @@ class ThermalScattering(EqualityMixin): Thermal scattering data """ - if isinstance(ev_or_filename, endf.Evaluation): - ev = ev_or_filename - else: - ev = endf.Evaluation(ev_or_filename) + ev = endf.as_evaluation(ev_or_filename) # Read incoherent inelastic data assert (7, 4) in ev.section, 'No MF=7, MT=4 found in thermal scattering' diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 42d4ab07ea..a2ff6dea0b 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -319,16 +319,16 @@ class Chain: String arguments in ``decay_files``, ``fpy_files``, and ``neutron_files`` will be treated as file names to be read. - Alternatively, :class:`openmc.data.endf.Evaluation` instances - can be included in these arguments. + Alternatively, :class:`openmc.data.endf.Evaluation` or + ``endf.Material`` instances can be included in these arguments. Parameters ---------- - decay_files : list of str or openmc.data.endf.Evaluation + decay_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF decay sub-library files - fpy_files : list of str or openmc.data.endf.Evaluation + fpy_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF neutron-induced fission product yield sub-library files - neutron_files : list of str or openmc.data.endf.Evaluation + neutron_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF neutron reaction sub-library files reactions : iterable of str, optional Transmutation reactions to include in the depletion chain, e.g., @@ -363,7 +363,7 @@ class Chain: print('Processing neutron sub-library files...') reactions = {} for f in neutron_files: - evaluation = openmc.data.endf.Evaluation(f) + evaluation = openmc.data.endf.as_evaluation(f) name = evaluation.gnds_name reactions[name] = {} for mf, mt, nc, mod in evaluation.reaction_list: diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index bc99fc42db..d85d2e8a79 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -186,8 +186,6 @@ def apply_time_correction( # Apply TCF, broadcasting to the correct dimensions tcf.shape = (1, -1, 1, 1, 1) - new_tally._sum = tally_sum * tcf - new_tally._sum_sq = tally_sum_sq * (tcf*tcf) new_tally._mean = tally_mean * tcf new_tally._std_dev = tally_std_dev * tcf @@ -196,6 +194,8 @@ def apply_time_correction( if sum_nuclides: # Sum over parent nuclides (note that when combining different bins for # parent nuclide, we can't work directly on sum_sq) + new_tally._sum = None + new_tally._sum_sq = None new_tally._mean = new_tally.mean.sum(axis=1).reshape(shape) new_tally._std_dev = np.linalg.norm(new_tally.std_dev, axis=1).reshape(shape) new_tally._derived = True @@ -203,9 +203,10 @@ def apply_time_correction( # Remove ParentNuclideFilter new_tally.filters.pop(i_filter) else: - # Change shape back to (filter combinations, nuclides, scores) - new_tally._sum.shape = shape - new_tally._sum_sq.shape = shape + # Apply TCF and change shape back to (filter combinations, nuclides, + # scores) + new_tally._sum = (tally_sum * tcf).reshape(shape) + new_tally._sum_sq = (tally_sum_sq * (tcf*tcf)).reshape(shape) new_tally._mean.shape = shape new_tally._std_dev.shape = shape diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index c12863956b..bdfc32763b 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -339,8 +339,12 @@ class IndependentOperator(OpenMCOperator): for i_nuc in nuc_index: nuc = self.nuc_ind_map[i_nuc] + if nuc not in xs._index_nuc: + continue for i_rx in react_index: rx = self.rx_ind_map[i_rx] + if rx not in xs._index_rx: + continue # Determine reaction rate by multiplying xs in [b] by flux # in [n-cm/src] to give [(reactions/src)*b-cm/atom] diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 687cf646f2..7049f7d314 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -84,7 +84,12 @@ def get_microxs_and_flux( reactions listed in the depletion chain file are used. energies : iterable of float or str Energy group boundaries in [eV] or the name of the group structure. - If left as None energies will default to [0.0, 100e6] + If left as None, no energy filter is applied to the flux tally. When + `reaction_rate_mode` is "direct", these boundaries define the output + flux and microscopic cross section energy group structure. When + `reaction_rate_mode` is "flux", these boundaries define the multigroup + flux tally used to collapse continuous-energy cross sections; returned + fluxes and microscopic cross sections are one-group. reaction_rate_mode : {"direct", "flux"}, optional The "direct" method tallies reaction rates directly (per energy group). The "flux" method tallies a multigroup flux spectrum and then @@ -110,7 +115,9 @@ def get_microxs_and_flux( reaction_rate_opts : dict, optional When `reaction_rate_mode="flux"`, allows selecting a subset of nuclide/reaction pairs to be computed via direct reaction-rate tallies - (per energy group). Supported keys: "nuclides", "reactions". + over one energy bin spanning the full `energies` range. Supported keys: + "nuclides", "reactions". If "reactions" are specified without + "nuclides", all selected nuclides are used. Returns ------- @@ -139,10 +146,14 @@ def get_microxs_and_flux( nuclides = [nuc.name for nuc in chain.nuclides if nuc.name in nuclides_with_data] - # Set up the reaction rate and flux tallies + # Set up the reaction rate and flux tallies. When energies are omitted, no + # energy filter is needed for the transport calculation. A one-group energy + # range is still needed later if flux collapse is requested. + collapse_energies = energies if energies is None: - energies = [0.0, 100.0e6] - if isinstance(energies, str): + energy_filter = None + collapse_energies = [0.0, 100.0e6] + elif isinstance(energies, str): energy_filter = openmc.EnergyFilter.from_group_structure(energies) else: energy_filter = openmc.EnergyFilter(energies) @@ -172,8 +183,11 @@ def get_microxs_and_flux( rr_reactions = list(reactions) elif reaction_rate_mode == 'flux' and reaction_rate_opts: opts = reaction_rate_opts or {} - rr_nuclides = list(opts.get('nuclides', [])) rr_reactions = list(opts.get('reactions', [])) + if rr_reactions: + rr_nuclides = list(opts.get('nuclides', nuclides)) + else: + rr_nuclides = list(opts.get('nuclides', [])) # Keep only requested pairs within overall sets if rr_nuclides: rr_nuclides = [n for n in rr_nuclides if n in set(nuclides)] @@ -182,7 +196,7 @@ def get_microxs_and_flux( # Use 1-group energy filter for RR in flux mode has_rr = bool(rr_nuclides and rr_reactions) - if has_rr and reaction_rate_mode == 'flux': + if has_rr and reaction_rate_mode == 'flux' and energy_filter is not None: rr_energy_filter = openmc.EnergyFilter( [energy_filter.values[0], energy_filter.values[-1]]) else: @@ -194,14 +208,18 @@ def get_microxs_and_flux( model.tallies = [] for i, domain_filter in enumerate(domain_filters): flux_tally = openmc.Tally(name=f'MicroXS flux {i}') - flux_tally.filters = [domain_filter, energy_filter] + flux_tally.filters = [domain_filter] + if energy_filter is not None: + flux_tally.filters.append(energy_filter) flux_tally.scores = ['flux'] model.tallies.append(flux_tally) flux_tallies.append(flux_tally) if has_rr: rr_tally = openmc.Tally(name=f'MicroXS RR {i}') - rr_tally.filters = [domain_filter, rr_energy_filter] + rr_tally.filters = [domain_filter] + if rr_energy_filter is not None: + rr_tally.filters.append(rr_energy_filter) rr_tally.nuclides = rr_nuclides rr_tally.multiply_density = False rr_tally.scores = rr_reactions @@ -215,7 +233,8 @@ def get_microxs_and_flux( model.export_to_model_xml() comm.barrier() # Reinitialize with tallies - openmc.lib.init(intracomm=comm) + output = run_kwargs.get('output', True) if run_kwargs else True + openmc.lib.init(intracomm=comm, output=output) with TemporaryDirectory() as temp_dir: # Indicate to run in temporary directory unless being executed through @@ -255,8 +274,12 @@ def get_microxs_and_flux( all_flux_arrays = [] for flux_tally in flux_tallies: # Get flux values and make energy groups last dimension - flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) - flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + flux = flux_tally.get_reshaped_data() + if energy_filter is None: + flux = flux[..., np.newaxis] # (domains, 1, 1, groups) + else: + # (domains, groups, 1, 1) -> (domains, 1, 1, groups) + flux = np.moveaxis(flux, 1, -1) all_flux_arrays.append(flux) fluxes.extend(flux.squeeze((1, 2))) @@ -266,8 +289,15 @@ def get_microxs_and_flux( for flux_arr, rr_tally in zip(all_flux_arrays, rr_tallies): flux = flux_arr # Get reaction rates and make energy groups last dimension - reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) - reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) + reaction_rates = rr_tally.get_reshaped_data() + if rr_energy_filter is None: + # (domains, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = reaction_rates[..., np.newaxis] + else: + # (domains, groups, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = np.moveaxis(reaction_rates, 1, -1) # If RR is 1-group, sum flux over groups if reaction_rate_mode == "flux": @@ -279,16 +309,20 @@ def get_microxs_and_flux( direct_micros.extend( MicroXS(xs_i, rr_nuclides, rr_reactions) for xs_i in xs) - # If using flux mode, compute flux-collapsed microscopic XS if reaction_rate_mode == 'flux': + # Compute flux-collapsed microscopic XS flux_micros = [MicroXS.from_multigroup_flux( - energies=energies, + energies=collapse_energies, multigroup_flux=flux_i, chain_file=chain_file, nuclides=nuclides, reactions=reactions ) for flux_i in fluxes] + # We need to return one-group fluxes to match the microscopic cross + # sections, which are always one-group by virtue of the collapse + fluxes = [flux.sum(keepdims=True) for flux in fluxes] + # Decide which micros to use and merge if needed if reaction_rate_mode == 'flux' and rr_tallies: micros = [m1.merge(m2) for m1, m2 in zip(flux_micros, direct_micros)] diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 6dbb3adb2c..d41e230eed 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -347,7 +347,7 @@ class R2SManager: # Run neutron transport and get fluxes and micros. Run via openmc.lib to # maintain a consistent parallelism strategy with the activation step. - with TemporarySession(): + with TemporarySession(output=False): self.results['fluxes'], self.results['micros'] = get_microxs_and_flux( self.neutron_model, domains, **micro_kwargs) diff --git a/openmc/examples.py b/openmc/examples.py index 90a0bffe7b..6895bd94b5 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -150,7 +150,7 @@ def pwr_core() -> openmc.Model: rpv_steel.add_nuclide('Ni60', 0.0026776, 'wo') rpv_steel.add_nuclide('Mn55', 0.01, 'wo') rpv_steel.add_nuclide('Cr52', 0.002092475, 'wo') - rpv_steel.add_nuclide('C0', 0.0025, 'wo') + rpv_steel.add_element('C', 0.0025, 'wo') rpv_steel.add_nuclide('Cu63', 0.0013696, 'wo') lower_rad_ref = openmc.Material(6, name='Lower radial reflector') @@ -1618,4 +1618,4 @@ def random_ray_three_region_cube_with_detectors() -> openmc.Model: model.settings = settings model.tallies = tallies - return model \ No newline at end of file + return model diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 00876db997..0ee8c3ff06 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -31,7 +31,10 @@ class _SourceSite(Structure): ('particle', c_int32), ('parent_nuclide', c_int), ('parent_id', c_int64), - ('progeny_id', c_int64)] + ('progeny_id', c_int64), + ('wgt_born', c_double), + ('wgt_ww_born', c_double), + ('n_split', c_int64)] # Define input type for numpy arrays that will be passed into C++ functions # Must be an int or double array, with single dimension that is contiguous diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 90af80d5b7..0f2e6c19ab 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -9,6 +9,7 @@ from .core import _FortranObjectWithID from .error import _error_handler import numpy as np +import warnings class _Position(Structure): @@ -51,218 +52,237 @@ class _Position(Structure): return f"({self.x}, {self.y}, {self.z})" -class _PlotBase(Structure): - """A structure defining a 2-D geometry slice with underlying c-types +def _extract_slice_data_args(plot): + """Convert a legacy plot-like object into slice_data keyword arguments.""" + try: + kwargs = { + 'origin': tuple(plot.origin), + 'width': (plot.width, plot.height), + 'basis': plot.basis, + 'pixels': (plot.h_res, plot.v_res), + 'show_overlaps': getattr(plot, 'color_overlaps', False), + 'level': getattr(plot, 'level', -1), + } + except AttributeError as exc: + raise TypeError( + "plot must be a legacy plot-like object with origin, width, " + "height, basis, h_res, and v_res attributes." + ) from exc + return kwargs - C-Type Attributes - ----------------- - origin_ : openmc.lib.plot._Position - A position defining the origin of the plot. - width_ : openmc.lib.plot._Position - The width of the plot along the x, y, and z axes, respectively - basis_ : c_int - The axes basis of the plot view. - pixels_ : c_size_t[3] - The resolution of the plot in the horizontal and vertical dimensions - color_overlaps_ : c_bool - Whether to assign unique IDs (-3) to overlapping regions. - level_ : c_int - The universe level for the plot view - Attributes +_dll.openmc_slice_data.argtypes = [ + POINTER(c_double * 3), # origin + POINTER(c_double * 3), # u_span + POINTER(c_double * 3), # v_span + POINTER(c_size_t * 2), # pixels + c_bool, # show_overlaps + c_int, # level + c_int32, # filter_index + POINTER(c_int32), # geom_data + POINTER(c_double), # property_data (can be None) +] +_dll.openmc_slice_data.restype = c_int +_dll.openmc_slice_data.errcheck = _error_handler + + +def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, + pixels=None, show_overlaps=False, level=-1, filter=None, + include_properties=True): + """Generate a 2D raster of geometry and property data for plotting. + + Parameters ---------- - origin : tuple or list of ndarray - Origin (center) of the plot - width : float - The horizontal dimension of the plot in geometry units (cm) - height : float - The vertical dimension of the plot in geometry units (cm) - basis : string - One of {'xy', 'xz', 'yz'} indicating the horizontal and vertical - axes of the plot. - h_res : int - The horizontal resolution of the plot in pixels - v_res : int - The vertical resolution of the plot in pixels - level : int - The universe level for the plot (default: -1 -> all universes shown) + origin : sequence of float + Center position of the plot [x, y, z] + width : sequence of float + Width of the plot [horizontal, vertical]. Mutually exclusive with + u_span/v_span. + basis : {'xy', 'xz', 'yz'} or int + Plot basis. Ignored if u_span/v_span are provided. + u_span : sequence of float, optional + Full-width span vector for the horizontal axis (3 values). Mutually + exclusive with width. + v_span : sequence of float, optional + Full-height span vector for the vertical axis (3 values). Mutually + exclusive with width. + pixels : sequence of int + Number of pixels [horizontal, vertical] + show_overlaps : bool, optional + Whether to detect overlapping cells + level : int, optional + Universe level (-1 for deepest) + filter : openmc.lib.Filter, optional + Filter for bin index lookup + include_properties : bool, optional + Whether to compute temperature/density + + Returns + ------- + geom_data : numpy.ndarray + Array of shape (v_res, h_res, 3) or (v_res, h_res, 4) with int32 dtype. + Contains [cell_id, cell_instance, material_id] when no filter is provided, + or [cell_id, cell_instance, material_id, filter_bin] when a filter is provided. + property_data : numpy.ndarray or None + Array of shape (v_res, h_res, 2) with float64 dtype containing + [temperature, density], or None if include_properties=False """ - _fields_ = [('origin_', _Position), - ('width_', _Position), - ('basis_', c_int), - ('pixels_', 3*c_size_t), - ('color_overlaps_', c_bool), - ('level_', c_int)] + if pixels is None: + raise ValueError("pixels must be specified.") + if len(pixels) != 2: + raise ValueError("pixels must be a length-2 sequence.") - def __init__(self): - self.level_ = -1 - self.basis_ = 1 - self.color_overlaps_ = False + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") - @property - def origin(self): - return self.origin_ - - @origin.setter - def origin(self, origin): - self.origin_.x = origin[0] - self.origin_.y = origin[1] - self.origin_.z = origin[2] - - @property - def width(self): - return self.width_.x - - @width.setter - def width(self, width): - self.width_.x = width - - @property - def height(self): - return self.width_.y - - @height.setter - def height(self, height): - self.width_.y = height - - @property - def basis(self): - if self.basis_ == 1: - return 'xy' - elif self.basis_ == 2: - return 'xz' - elif self.basis_ == 3: - return 'yz' - - raise ValueError(f"Plot basis {self.basis_} is invalid") - - @basis.setter - def basis(self, basis): + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + u_span = np.asarray(u_span, dtype=float) + v_span = np.asarray(v_span, dtype=float) + if u_span.shape != (3,) or v_span.shape != (3,): + raise ValueError("u_span and v_span must be length-3 sequences.") + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + if u_norm == 0.0 or v_norm == 0.0: + raise ValueError("u_span and v_span must be non-zero vectors.") + dot = float(np.dot(u_span, v_span)) + ortho_tol = 1.0e-10 * u_norm * v_norm + if abs(dot) > ortho_tol: + raise ValueError("u_span and v_span must be orthogonal.") + else: + if width is None: + raise ValueError("width must be provided when u_span/v_span are not set.") + if len(width) != 2: + raise ValueError("width must be a length-2 sequence.") + basis_map = {'xy': 1, 'xz': 2, 'yz': 3} if isinstance(basis, str): - valid_bases = ('xy', 'xz', 'yz') basis = basis.lower() - if basis not in valid_bases: + if basis not in basis_map: raise ValueError(f"{basis} is not a valid plot basis.") - - if basis == 'xy': - self.basis_ = 1 - elif basis == 'xz': - self.basis_ = 2 - elif basis == 'yz': - self.basis_ = 3 - return - - if isinstance(basis, int): - valid_bases = (1, 2, 3) - if basis not in valid_bases: + basis = basis_map[basis] + elif isinstance(basis, int): + if basis not in basis_map.values(): raise ValueError(f"{basis} is not a valid plot basis.") - self.basis_ = basis - return + else: + raise ValueError(f"{basis} is not a valid plot basis.") - raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") + if basis == 1: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, width[1], 0.0], dtype=float) + elif basis == 2: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) + else: + u_span = np.array([0.0, width[0], 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) - @property - def h_res(self): - return self.pixels_[0] + origin = np.asarray(origin, dtype=float) + if origin.shape != (3,): + raise ValueError("origin must be a length-3 sequence.") - @h_res.setter - def h_res(self, h_res): - self.pixels_[0] = h_res + # Prepare ctypes arrays + origin_arr = (c_double * 3)(*origin) + u_span_arr = (c_double * 3)(*u_span) + v_span_arr = (c_double * 3)(*v_span) + pixels_arr = (c_size_t * 2)(*pixels) - @property - def v_res(self): - return self.pixels_[1] + # Get internal filter index from filter ID if filter is provided + if filter is not None: + filter_index = c_int32() + _dll.openmc_get_filter_index(filter.id, filter_index) + filter_index = filter_index.value + else: + filter_index = -1 - @v_res.setter - def v_res(self, v_res): - self.pixels_[1] = v_res + # Allocate output arrays with dynamic size based on filter + n_geom_fields = 4 if filter is not None else 3 + geom_data = np.zeros((pixels[1], pixels[0], n_geom_fields), dtype=np.int32) + if include_properties: + property_data = np.zeros((pixels[1], pixels[0], 2), dtype=np.float64) + prop_ptr = property_data.ctypes.data_as(POINTER(c_double)) + else: + property_data = None + prop_ptr = None - @property - def level(self): - return int(self.level_) + _dll.openmc_slice_data( + origin_arr, + u_span_arr, + v_span_arr, + pixels_arr, + show_overlaps, + level, + filter_index, + geom_data.ctypes.data_as(POINTER(c_int32)), + prop_ptr + ) - @level.setter - def level(self, level): - self.level_ = level - - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, color_overlaps): - self.color_overlaps_ = color_overlaps - - def __repr__(self): - out_str = ["-----", - "Plot:", - "-----", - f"Origin: {self.origin}", - f"Width: {self.width}", - f"Height: {self.height}", - f"Basis: {self.basis}", - f"HRes: {self.h_res}", - f"VRes: {self.v_res}", - f"Color Overlaps: {self.color_overlaps}", - f"Level: {self.level}"] - return '\n'.join(out_str) - - -_dll.openmc_id_map.argtypes = [POINTER(_PlotBase), POINTER(c_int32)] -_dll.openmc_id_map.restype = c_int -_dll.openmc_id_map.errcheck = _error_handler + return geom_data, property_data def id_map(plot): + """Deprecated compatibility wrapper for geometry ID maps. + + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=False)` instead. """ - Generate a 2-D map of cell and material IDs. Used for in-memory image - generation. + warnings.warn( + "openmc.lib.id_map is deprecated and will be removed in a future " + "release; use openmc.lib.slice_data(..., include_properties=False).", + FutureWarning, + ) - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - id_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 3) of - OpenMC property ids with dtype int32. The last dimension of the array - contains, in order, cell IDs, cell instances, and material IDs. - - """ - img_data = np.zeros((plot.v_res, plot.h_res, 3), - dtype=np.dtype('int32')) - _dll.openmc_id_map(plot, img_data.ctypes.data_as(POINTER(c_int32))) - return img_data - - -_dll.openmc_property_map.argtypes = [POINTER(_PlotBase), POINTER(c_double)] -_dll.openmc_property_map.restype = c_int -_dll.openmc_property_map.errcheck = _error_handler + kwargs = _extract_slice_data_args(plot) + geom_data, _ = slice_data(include_properties=False, **kwargs) + return geom_data[:, :, :3] def property_map(plot): + """Deprecated compatibility wrapper for temperature/density maps. + + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=True)` instead. """ - Generate a 2-D map of cell temperatures and material densities. Used for - in-memory image generation. + warnings.warn( + "openmc.lib.property_map is deprecated and will be removed in a " + "future release; use openmc.lib.slice_data(..., " + "include_properties=True).", + FutureWarning, + ) - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - property_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 2) of - OpenMC property ids with dtype float - - """ - prop_data = np.zeros((plot.v_res, plot.h_res, 2)) - _dll.openmc_property_map(plot, prop_data.ctypes.data_as(POINTER(c_double))) + kwargs = _extract_slice_data_args(plot) + _, prop_data = slice_data(include_properties=True, **kwargs) return prop_data + +_dll.openmc_slice_data_overlap_count.argtypes = [POINTER(c_size_t)] +_dll.openmc_slice_data_overlap_count.restype = c_int +_dll.openmc_slice_data_overlap_count.errcheck = _error_handler + +_dll.openmc_slice_data_overlap_info.argtypes = [c_size_t, POINTER(c_int32)] +_dll.openmc_slice_data_overlap_info.restype = c_int +_dll.openmc_slice_data_overlap_info.errcheck = _error_handler + + +# Python wrappings for overlap functions +def slice_data_overlap_count(): + count = c_size_t() + _dll.openmc_slice_data_overlap_count(count) + return count.value + + +def slice_data_overlap_info(): + n = slice_data_overlap_count() + overlap_info = np.empty(n * 3, dtype=np.int32) + + if n > 0: + _dll.openmc_slice_data_overlap_info( + n, + overlap_info.ctypes.data_as(POINTER(c_int32)), + ) + return overlap_info, n + + _dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_plot_index.restype = c_int _dll.openmc_get_plot_index.errcheck = _error_handler diff --git a/openmc/material.py b/openmc/material.py index 86cc3d7939..eded9c3faa 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -296,6 +296,8 @@ class Material(IDManagerMixin): mass += nuc.percent # Compute and return the molar mass + if moles == 0.0: + raise ValueError("Material has no nuclides; cannot compute molar mass") return mass / moles @property @@ -2287,7 +2289,7 @@ class Materials(cv.CheckedList): multigroup_fluxes: Sequence[Sequence[float]] Energy-dependent multigroup flux values, where each sublist corresponds to a specific material. Will be normalized so that it sums to 1. - energy_group_structures': Sequence[Sequence[float] | str] + energy_group_structures: Sequence[Sequence[float] | str] Energy group boundaries in [eV] or the name of the group structure. timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are @@ -2322,6 +2324,11 @@ class Materials(cv.CheckedList): for mat in self: mat.depletable = True + if len(multigroup_fluxes) != len(self): + raise ValueError("multigroup_fluxes length must match number of materials") + if len(energy_group_structures) != len(self): + raise ValueError("energy_group_structures length must match number of materials") + chain = _get_chain(chain_file) # Create MicroXS objects for all materials @@ -2332,6 +2339,10 @@ class Materials(cv.CheckedList): for material, flux, energy in zip( self, multigroup_fluxes, energy_group_structures ): + if material.volume is None: + raise ValueError( + f"Material {material.id} has no volume; cannot deplete" + ) temperature = material.temperature or 293.6 micro_xs = openmc.deplete.MicroXS.from_multigroup_flux( energies=energy, diff --git a/openmc/mesh.py b/openmc/mesh.py index 4129913d85..ff8e145772 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -3,7 +3,7 @@ import warnings from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence, Mapping from functools import wraps -from math import pi, sqrt, atan2 +import math from numbers import Integral, Real from pathlib import Path from typing import Protocol @@ -11,12 +11,10 @@ from typing import Protocol import h5py import lxml.etree as ET import numpy as np -from pathlib import Path import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from openmc.utility_funcs import change_directory from .bounding_box import BoundingBox from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin @@ -291,7 +289,7 @@ class MeshBase(IDManagerMixin, ABC): """ mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode() mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - mesh_name = '' if not 'name' in group else group['name'][()].decode() + mesh_name = '' if 'name' not in group else group['name'][()].decode() if mesh_type == 'regular': return RegularMesh.from_hdf5(group, mesh_id, mesh_name) @@ -1903,7 +1901,7 @@ class CylindricalMesh(StructuredMesh): self, r_grid: Sequence[float], z_grid: Sequence[float], - phi_grid: Sequence[float] = (0, 2*pi), + phi_grid: Sequence[float] = (0, 2*math.pi), origin: Sequence[float] = (0., 0., 0.), mesh_id: int | None = None, name: str = '', @@ -1960,7 +1958,7 @@ class CylindricalMesh(StructuredMesh): cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > 2*pi)): + if np.any((grid < 0.0) | (grid > 2*math.pi)): raise ValueError("phi_grid values must be in [0, 2π].") self._phi_grid = grid @@ -2028,9 +2026,9 @@ class CylindricalMesh(StructuredMesh): return string def get_indices_at_coords( - self, - coords: Sequence[float] - ) -> tuple[int, int, int]: + self, + coords: Sequence[float] + ) -> tuple[int, int, int]: """Finds the index of the mesh element at the specified coordinates. .. versionadded:: 0.15.0 @@ -2046,7 +2044,7 @@ class CylindricalMesh(StructuredMesh): The r, phi, z indices """ - r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2) + r_value_from_origin = math.hypot(coords[0]-self.origin[0], coords[1]-self.origin[1]) if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]: raise ValueError( @@ -2070,13 +2068,13 @@ class CylindricalMesh(StructuredMesh): delta_x = coords[0] - self.origin[0] delta_y = coords[1] - self.origin[1] # atan2 returns values in -pi to +pi range - phi_value = atan2(delta_y, delta_x) + phi_value = math.atan2(delta_y, delta_x) if delta_x < 0 and delta_y < 0: # returned phi_value anticlockwise and negative - phi_value += 2 * pi + phi_value += 2 * math.pi if delta_x > 0 and delta_y < 0: # returned phi_value anticlockwise and negative - phi_value += 2 * pi + phi_value += 2 * math.pi phi_grid_values = np.array(self.phi_grid) @@ -2111,7 +2109,7 @@ class CylindricalMesh(StructuredMesh): dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, name: str = '', - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), + phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi), enclose_domain: bool = False, ) -> CylindricalMesh: """Create CylindricalMesh from a bounding box. @@ -2339,8 +2337,8 @@ class SphericalMesh(StructuredMesh): def __init__( self, r_grid: Sequence[float], - phi_grid: Sequence[float] = (0, 2*pi), - theta_grid: Sequence[float] = (0, pi), + phi_grid: Sequence[float] = (0, 2*math.pi), + theta_grid: Sequence[float] = (0, math.pi), origin: Sequence[float] = (0., 0., 0.), mesh_id: int | None = None, name: str = '', @@ -2397,7 +2395,7 @@ class SphericalMesh(StructuredMesh): cv.check_length('mesh theta_grid', grid, 2) cv.check_increasing('mesh theta_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > pi)): + if np.any((grid < 0.0) | (grid > math.pi)): raise ValueError("theta_grid values must be in [0, π].") self._theta_grid = grid @@ -2411,7 +2409,7 @@ class SphericalMesh(StructuredMesh): cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > 2*pi)): + if np.any((grid < 0.0) | (grid > 2*math.pi)): raise ValueError("phi_grid values must be in [0, 2π].") self._phi_grid = grid @@ -2483,8 +2481,8 @@ class SphericalMesh(StructuredMesh): dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, name: str = '', - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), - theta_grid_bounds: Sequence[float] = (0.0, pi), + phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi), + theta_grid_bounds: Sequence[float] = (0.0, math.pi), enclose_domain: bool = False, ) -> SphericalMesh: """Create SphericalMesh from a bounding box. @@ -2645,10 +2643,82 @@ class SphericalMesh(StructuredMesh): arr[..., 2] = z + origin[2] return arr - def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: - raise NotImplementedError( - "get_indices_at_coords is not yet implemented for SphericalMesh" - ) + def get_indices_at_coords( + self, + coords: Sequence[float] + ) -> tuple[int, int, int]: + """Find the mesh cell indices containing the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point as (x, y, z). + + Returns + ------- + tuple[int, int, int] + The r, theta, phi indices. + + Raises + ------ + ValueError + If the coordinates fall outside the mesh grid boundaries. + + """ + dx = coords[0] - self.origin[0] + dy = coords[1] - self.origin[1] + dz = coords[2] - self.origin[2] + + r_value = math.hypot(dx, dy, dz) + + if r_value < self.r_grid[0] or r_value > self.r_grid[-1]: + raise ValueError( + f'The r value {r_value} computed from the specified ' + f'coordinates is outside the r grid range ' + f'[{self.r_grid[0]}, {self.r_grid[-1]}].' + ) + + r_index = int(min( + np.searchsorted(self.r_grid, r_value, side='right') - 1, + len(self.r_grid) - 2 + )) + + if r_value == 0.0: + theta_value = 0.0 + phi_value = 0.0 + else: + theta_value = math.acos(dz / r_value) + phi_value = math.atan2(dy, dx) + if phi_value < 0: + phi_value += 2 * math.pi + + if theta_value < self.theta_grid[0] or theta_value > self.theta_grid[-1]: + raise ValueError( + f'The theta value {theta_value} computed from the specified ' + f'coordinates is outside the theta grid range ' + f'[{self.theta_grid[0]}, {self.theta_grid[-1]}].' + ) + + theta_index = int(min( + np.searchsorted(self.theta_grid, theta_value, side='right') - 1, + len(self.theta_grid) - 2 + )) + + if phi_value < self.phi_grid[0] or phi_value > self.phi_grid[-1]: + raise ValueError( + f'The phi value {phi_value} computed from the specified ' + f'coordinates is outside the phi grid range ' + f'[{self.phi_grid[0]}, {self.phi_grid[-1]}].' + ) + + phi_index = int(min( + np.searchsorted(self.phi_grid, phi_value, side='right') - 1, + len(self.phi_grid) - 2 + )) + + return (r_index, theta_index, phi_index) def require_statepoint_data(func): @@ -2738,7 +2808,8 @@ class UnstructuredMesh(MeshBase): _UNSUPPORTED_ELEM = -1 _LINEAR_TET = 0 _LINEAR_HEX = 1 - _VTK_TETRA = 10 + _VTK_TET = 10 + _VTK_HEX = 12 def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None, name: str = '', length_multiplier: float = 1.0, @@ -3049,7 +3120,9 @@ class UnstructuredMesh(MeshBase): n_skipped += 1 continue else: - raise RuntimeError(f"Invalid element type {elem_type} found") + raise RuntimeError( + f"Invalid element type {elem_type} found in mesh {self.id}" + ) for i, c in enumerate(conn): if c == -1: @@ -3106,35 +3179,42 @@ class UnstructuredMesh(MeshBase): datasets: dict | None = None, volume_normalization: bool = True, ): - def append_dataset(dset, array): - """Convenience function to append data to an HDF5 dataset""" - origLen = dset.shape[0] - dset.resize(origLen + array.shape[0], axis=0) - dset[origLen:] = array + # This writer supports linear tetrahedra and linear hexahedra elements + conn_list = [] # flattened connectivity ids + cell_sizes = [] # number of points per cell + vtk_types = [] # VTK cell types per cell (uint8) + n_skipped = 0 - if self.library != "moab": - raise NotImplementedError("VTKHDF output is only supported for MOAB meshes") - - # the self.connectivity contains arrays of length 8 to support hex - # elements as well, in the case of tetrahedra mesh elements, the - # last 4 values are -1 and are removed - trimmed_connectivity = [] - for cell in self.connectivity: - # Find the index of the first -1 value, if any - first_negative_index = np.where(cell == -1)[0] - if first_negative_index.size > 0: - # Slice the array up to the first -1 value - trimmed_connectivity.append(cell[: first_negative_index[0]]) + for conn, etype in zip(self.connectivity, self.element_types): + if etype == self._LINEAR_TET: + ids = conn[:4] + vtk_types.append(self._VTK_TET) + elif etype == self._LINEAR_HEX: + ids = conn[:8] + vtk_types.append(self._VTK_HEX) + elif etype == self._UNSUPPORTED_ELEM: + n_skipped += 1 + continue else: - # No -1 values, append the whole cell - trimmed_connectivity.append(cell) - trimmed_connectivity = np.array(trimmed_connectivity, dtype="int32").flatten() + raise RuntimeError( + f"Invalid element type {etype} found in mesh {self.id}" + ) + conn_list.extend(ids.tolist()) + cell_sizes.append(len(ids)) - # MOAB meshes supports tet elements only so we know it has 4 points per cell - points_per_cell = 4 + if n_skipped > 0: + warnings.warn( + f"{n_skipped} elements were not written because " + "they are not of type linear tet/hex" + ) - # offsets are the indices of the first point of each cell in the array of points - offsets = np.arange(0, self.n_elements * points_per_cell + 1, points_per_cell) + connectivity = np.asarray(conn_list, dtype=np.int64) + + # Offsets must be length (numCells + 1) with a leading 0 and + # cumulative end-indices thereafter, per VTK's layout + cell_sizes_arr = np.asarray(cell_sizes, dtype=np.int64) + offsets = np.zeros(cell_sizes_arr.size + 1, dtype=np.int64) + np.cumsum(cell_sizes_arr, out=offsets[1:]) for name, data in datasets.items(): if data.shape != self.dimension: @@ -3156,42 +3236,27 @@ class UnstructuredMesh(MeshBase): dtype=h5py.string_dtype("ascii", len(ascii_type)), ) - # create hdf5 file structure - root.create_dataset("NumberOfPoints", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Types", (0,), maxshape=(None,), dtype="uint8") - root.create_dataset("Points", (0, 3), maxshape=(None, 3), dtype="f") - root.create_dataset( - "NumberOfConnectivityIds", (0,), maxshape=(None,), dtype="i8" - ) - root.create_dataset("NumberOfCells", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Offsets", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Connectivity", (0,), maxshape=(None,), dtype="i8") + # Create HDF5 file structure compliant with VTKHDF UnstructuredGrid + n_points = int(len(self.vertices)) + n_cells = int(len(cell_sizes)) + n_conn_ids = int(len(connectivity)) - append_dataset(root["NumberOfPoints"], np.array([len(self.vertices)])) - append_dataset(root["Points"], self.vertices) - append_dataset( - root["NumberOfConnectivityIds"], - np.array([len(trimmed_connectivity)]), - ) - append_dataset(root["Connectivity"], trimmed_connectivity) - append_dataset(root["NumberOfCells"], np.array([self.n_elements])) - append_dataset(root["Offsets"], offsets) - - append_dataset( - root["Types"], np.full(self.n_elements, self._VTK_TETRA, dtype="uint8") - ) + root.create_dataset("NumberOfPoints", data=(n_points,), dtype="i8") + root.create_dataset("NumberOfCells", data=(n_cells,), dtype="i8") + root.create_dataset("NumberOfConnectivityIds", data=(n_conn_ids,), dtype="i8") + root.create_dataset("Points", data=self.vertices.astype(np.float64, copy=False), dtype="f8") + root.create_dataset("Types", data=np.asarray(vtk_types, dtype=np.uint8), dtype="uint8") + root.create_dataset("Offsets", data=offsets.astype("i8"), dtype="i8") + root.create_dataset("Connectivity", data=connectivity.astype("i8"), dtype="i8") cell_data_group = root.create_group("CellData") for name, data in datasets.items(): - - cell_data_group.create_dataset( - name, (0,), maxshape=(None,), dtype="float64", chunks=True - ) - if volume_normalization: data /= self.volumes - append_dataset(cell_data_group[name], data) + cell_data_group.create_dataset( + name, data=data, dtype="float64", chunks=True + ) @classmethod def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index 78ab3ca198..faa83c0481 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -1290,6 +1290,14 @@ class Library: 'are ignored since multiplicity or nu-scatter matrices '\ 'were not tallied for ' + xsdata_name warn(msg, RuntimeWarning) + + if 'scatter matrix' in self.mgxs_types: + scatt_mgxs = self.get_mgxs(domain, 'scatter matrix') + elif 'consistent scatter matrix' in self.mgxs_types: + scatt_mgxs = self.get_mgxs(domain, 'consistent scatter matrix') + else: + raise ValueError(f'No scatter matrix found for {xsdata_name}.') + xsdata.set_scatter_matrix_mgxs(scatt_mgxs, temperature=temperature, xs_type=xs_type, nuclide=[nuclide], diff --git a/openmc/model/model.py b/openmc/model/model.py index c9a24b8b38..3284942885 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -265,22 +265,22 @@ class Model: denom_tally = openmc.Tally(name='IFP denominator') denom_tally.scores = ['ifp-denominator'] self.tallies.append(denom_tally) - - # TODO: This should also be incorporated into lower-level calls in + + # TODO: This should also be incorporated into lower-level calls in # settings.py, but it requires information about the tallies currently # on the active Model def _assign_fw_cadis_tally_IDs(self): - # Verify that all tallies assigned as targets on WeightWindowGenerators - # exist within model.tallies. If this is the case, convert the .targets + # Verify that all tallies assigned as targets on WeightWindowGenerators + # exist within model.tallies. If this is the case, convert the .targets # attribute of each WeightWindowGenerator to a sequence of tally IDs. if len(self.settings.weight_window_generators) == 0: return - + # List of valid tally IDs reference_tally_ids = np.asarray([tal.id for tal in self.tallies]) - + for wwg in self.settings.weight_window_generators: - # Only proceeds if the "targets" attribute is an openmc.Tallies, + # Only proceeds if the "targets" attribute is an openmc.Tallies, # which means it hasn't been checked against model.tallies. if isinstance(wwg.targets, openmc.Tallies): id_vec = [] @@ -291,8 +291,8 @@ class Model: if tal == reference_tal: id_next = reference_tal.id break - - if id_next == None: + + if id_next is None: raise RuntimeError( f'Local FW-CADIS target tally {tal.id} not found on model.tallies!') else: @@ -467,6 +467,8 @@ class Model: This method iterates over all DAGMC universes in the geometry and synchronizes their cells with the current material assignments. Requires that the model has been initialized via :meth:`Model.init_lib`. + Synchronized DAGMC cells can then be edited and exported as nested + `` overrides inside each `` element. .. versionadded:: 0.15.1 @@ -1125,28 +1127,148 @@ class Model: array contains cell IDs, cell instances, and material IDs (in that order). """ + ids, _ = self.slice_data( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + show_overlaps=color_overlaps, + level=-1, + include_properties=False, + **init_kwargs, + ) + return ids + + def slice_data( + self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + u_span: Sequence[float] | None = None, + v_span: Sequence[float] | None = None, + show_overlaps: bool = False, + level: int = -1, + filter: openmc.Filter | None = None, + include_properties: bool = True, + **init_kwargs + ) -> tuple[np.ndarray, np.ndarray | None]: + """Generate geometry and property data for a 2D plot slice. + + This method combines the functionality of :meth:`id_map` and property + mapping into a single call, avoiding duplicate geometry lookups. It also + supports filter bin index lookup for tally visualization. + + .. versionadded:: 0.16.0 + + Parameters + ---------- + origin : Sequence[float], optional + Origin of the plot. If unspecified, this argument defaults to the + center of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (0.0, 0.0, 0.0). + width : Sequence[float], optional + Width of the plot. If unspecified, this argument defaults to the + width of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (10.0, 10.0). + pixels : int | Sequence[int], optional + If an iterable of ints is provided then this directly sets the + number of pixels to use in each basis direction. If a single int is + provided then this sets the total number of pixels in the plot and + the number of pixels in each basis direction is calculated from this + total and the image aspect ratio based on the width argument. + basis : {'xy', 'yz', 'xz'}, optional + Basis of the plot. + u_span : Sequence[float], optional + Full-width span vector for an oriented slice (3 values). Mutually + exclusive with width. + v_span : Sequence[float], optional + Full-height span vector for an oriented slice (3 values). Mutually + exclusive with width. + show_overlaps : bool, optional + Whether to identify and assign unique IDs (-3) to overlapping + regions. If False, overlapping regions will be assigned the ID of + the lowest-numbered cell that occupies that region. Defaults to + False. + level : int, optional + Universe level to plot (-1 for deepest). Defaults to -1. + filter : openmc.Filter, optional + If provided, the information for each pixel also includes an index + in the filter corresponding to the pixel position. + include_properties : bool, optional + Whether to include temperature/density data. Defaults to True. + **init_kwargs + Keyword arguments passed to :meth:`Model.init_lib`. + + Returns + ------- + geom_data : numpy.ndarray + Shape (v_res, h_res, 3) or (v_res, h_res, 4) int32 array. Contains + [cell_id, cell_instance, material_id] when no filter, or [cell_id, + cell_instance, material_id, filter_bin] with filter. + property_data : numpy.ndarray or None + Shape (v_res, h_res, 2) float64 array with [temperature, density], + or None if include_properties=False. + """ import openmc.lib - origin, width, pixels = self._set_plot_defaults( - origin, width, pixels, basis) + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") - # initialize the openmc.lib.plot._PlotBase object - plot_obj = openmc.lib.plot._PlotBase() - plot_obj.origin = origin - plot_obj.width = width[0] - plot_obj.height = width[1] - plot_obj.h_res = pixels[0] - plot_obj.v_res = pixels[1] - plot_obj.basis = basis - plot_obj.color_overlaps = color_overlaps + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + if origin is None: + origin = (0.0, 0.0, 0.0) + if isinstance(pixels, int): + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + aspect_ratio = u_norm / v_norm + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + else: + origin, width, pixels = self._set_plot_defaults( + origin, width, pixels, basis) # Silence output by default. Also set arguments to start in volume # calculation mode to avoid loading cross sections init_kwargs.setdefault('output', False) init_kwargs.setdefault('args', ['-c']) + # If filter does not already appear in the model, temporarily add a + # tally with the filter + original_length = len(self.tallies) + if filter is not None: + filter_ids = {f.id for t in self.tallies for f in t.filters} + if filter.id not in filter_ids: + # Create temporary tally while preserving ID assignment + next_id = openmc.Tally.next_id + temp_tally = openmc.Tally() + temp_tally.filters = [filter] + temp_tally.scores = ['flux'] + self.tallies.append(temp_tally) + openmc.Tally.used_ids.remove(temp_tally.id) + openmc.Tally.next_id = next_id + with openmc.lib.TemporarySession(self, **init_kwargs): - return openmc.lib.id_map(plot_obj) + geom_data, property_data = openmc.lib.slice_data( + origin=origin, + width=width, + basis=basis, + u_span=u_span, + v_span=v_span, + pixels=pixels, + show_overlaps=show_overlaps, + level=level, + filter=filter, + include_properties=include_properties, + ) + + # If filter was temporarily added, remove it + if len(self.tallies) > original_length: + self.tallies.pop() + + return geom_data, property_data @add_plot_params def plot( @@ -1214,13 +1336,14 @@ class Model: "openmc.config before plotting.") break - # Get ID map from the C API - id_map = self.id_map( + # Get plot IDs from the C API + id_map, _ = self.slice_data( origin=origin, width=width, pixels=pixels, basis=basis, - color_overlaps=show_overlaps + show_overlaps=show_overlaps, + include_properties=False, ) # Generate colors if not provided @@ -1746,10 +1869,10 @@ class Model: @staticmethod def _auto_generate_mgxs_lib( - model: openmc.model.model, + model: openmc.model.Model, groups: openmc.mgxs.EnergyGroups, - correction: str | none, - directory: pathlike, + correction: str | None, + directory: PathLike, ) -> openmc.mgxs.Library: """ Automatically generate a multi-group cross section libray from a model @@ -1956,7 +2079,7 @@ class Model: # Set materials on the model model.materials = [material] - if temperature != None: + if temperature is not None: model.materials[-1].temperature = temperature # Settings @@ -1983,7 +2106,7 @@ class Model: mgxs_lib = Model._auto_generate_mgxs_lib( model, groups, correction, directory) - if temperature != None: + if temperature is not None: return mgxs_lib.get_xsdata(domain=material, xsdata_name=name, temperature=temperature) else: @@ -2056,12 +2179,12 @@ class Model: ) temp_settings = {} - if temperature_settings == None: + if temperature_settings is None: temp_settings = self.settings.temperature else: temp_settings = temperature_settings - if temperatures == None: + if temperatures is None: mgxs_sets = [] for material in self.materials: xs_data = Model._isothermal_infinite_media_mgxs( @@ -2234,7 +2357,7 @@ class Model: model = openmc.Model() model.geometry = stoch_geom - if temperature != None: + if temperature is not None: for material in model.geometry.get_all_materials().values(): material.temperature = temperature @@ -2258,7 +2381,7 @@ class Model: model, groups, correction, directory) # Fetch all of the isothermal results. - if temperature != None: + if temperature is not None: return { mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, temperature=temperature) @@ -2344,12 +2467,12 @@ class Model: ) temp_settings = {} - if temperature_settings == None: + if temperature_settings is None: temp_settings = self.settings.temperature else: temp_settings = temperature_settings - if temperatures == None: + if temperatures is None: mgxs_sets = Model._isothermal_stochastic_slab_mgxs( geo, groups, @@ -2442,7 +2565,7 @@ class Model: model = copy.deepcopy(input_model) model.tallies = openmc.Tallies() - if temperature != None: + if temperature is not None: for material in model.geometry.get_all_materials().values(): material.temperature = temperature @@ -2458,7 +2581,7 @@ class Model: model, groups, correction, directory) # Fetch all of the isothermal results. - if temperature != None: + if temperature is not None: return { mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, temperature=temperature) @@ -2513,12 +2636,12 @@ class Model: entries in openmc.Settings.temperature_settings. """ temp_settings = {} - if temperature_settings == None: + if temperature_settings is None: temp_settings = self.settings.temperature else: temp_settings = temperature_settings - if temperatures == None: + if temperatures is None: mgxs_sets = Model._isothermal_materialwise_mgxs( self, groups, @@ -2649,12 +2772,15 @@ class Model: self.settings.run_mode = original_run_mode break - # Make sure all materials have a name, and that the name is a valid HDF5 - # dataset name + # Temporarily replace each material's name with a unique, valid HDF5 + # dataset name (its name plus ID) for use as its MGXS library entry + # and macroscopic. The ID keeps the name unique even when materials + # share a name; the original names are restored at the end. + original_names = [material.name for material in self.materials] for material in self.materials: - if not material.name or not material.name.strip(): - material.name = f"material {material.id}" - material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + base = material.name if material.name and material.name.strip() \ + else "material" + material.name = re.sub(r'[^a-zA-Z0-9]', '_', base) + f"_{material.id}" # If needed, generate the needed MGXS data library file if not Path(mgxs_path).is_file() or overwrite_mgxs_library: @@ -2686,6 +2812,10 @@ class Model: self.settings.energy_mode = 'multi-group' + # Restore the user's original material names. + for material, name in zip(self.materials, original_names): + material.name = name + def convert_to_random_ray(self): """Convert a multigroup model to use random ray. diff --git a/openmc/settings.py b/openmc/settings.py index 1090babda4..8120eb073e 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -67,6 +67,10 @@ class Settings: (ex: ["(n,fission)", 2, "(n,2n)"] ). (list of str or int) :deposited_E_threshold: Number to define the minimum deposited energy during per collision to trigger banking. (float) + create_delayed_neutrons : bool + Whether delayed neutrons are created in fission. + + .. versionadded:: 0.13.3 create_fission_neutrons : bool Indicate whether fission neutrons should be created or not. cutoff : dict @@ -256,8 +260,15 @@ class Settings: The type of calculation to perform (default is 'eigenvalue') seed : int Seed for the linear congruential pseudorandom number generator - stride : int - Number of random numbers allocated for each source particle history + shared_secondary_bank : bool + Whether to use a shared secondary particle bank. When enabled, + secondary particles are collected into a global bank, sorted for + reproducibility, and load-balanced across MPI ranks between + generations. If not specified, the shared secondary bank is + enabled automatically for fixed-source simulations with weight + windows active, and disabled otherwise. + + .. versionadded:: 0.15.4 source : Iterable of openmc.SourceBase Distribution of source sites in space, angle, and energy source_rejection_fraction : float @@ -277,6 +288,8 @@ class Settings: Options for writing state points. Acceptable keys are: :batches: list of batches at which to write statepoint files + stride : int + Number of random numbers allocated for each source particle history surf_source_read : dict Options for reading surface source points. Acceptable keys are: @@ -373,10 +386,6 @@ class Settings: .. versionadded:: 0.14.0 - create_delayed_neutrons : bool - Whether delayed neutrons are created in fission. - - .. versionadded:: 0.13.3 weight_windows_on : bool Whether weight windows are enabled @@ -482,6 +491,7 @@ class Settings: self._weight_window_generators = cv.CheckedList( WeightWindowGenerator, 'weight window generators') self._weight_windows_on = None + self._shared_secondary_bank = None self._weight_windows_file = None self._weight_window_checkpoints = {} self._max_history_splits = None @@ -1306,6 +1316,15 @@ class Settings: cv.check_type('weight windows on', value, bool) self._weight_windows_on = value + @property + def shared_secondary_bank(self) -> bool: + return self._shared_secondary_bank + + @shared_secondary_bank.setter + def shared_secondary_bank(self, value: bool): + cv.check_type('shared secondary bank', value, bool) + self._shared_secondary_bank = value + @property def weight_window_checkpoints(self) -> dict: return self._weight_window_checkpoints @@ -1924,6 +1943,11 @@ class Settings: elem = ET.SubElement(root, "weight_windows_on") elem.text = str(self._weight_windows_on).lower() + def _create_shared_secondary_bank_subelement(self, root): + if self._shared_secondary_bank is not None: + elem = ET.SubElement(root, "shared_secondary_bank") + elem.text = str(self._shared_secondary_bank).lower() + def _create_weight_window_generators_subelement(self, root, mesh_memo=None): if not self.weight_window_generators: return @@ -2430,6 +2454,11 @@ class Settings: if text is not None: self.weight_windows_on = text in ('true', '1') + def _shared_secondary_bank_from_xml_element(self, root): + text = get_text(root, 'shared_secondary_bank') + if text is not None: + self.shared_secondary_bank = text in ('true', '1') + def _weight_windows_file_from_xml_element(self, root): text = get_text(root, 'weight_windows_file') if text is not None: @@ -2597,6 +2626,7 @@ class Settings: self._create_write_initial_source_subelement(element) self._create_weight_windows_subelement(element, mesh_memo) self._create_weight_windows_on_subelement(element) + self._create_shared_secondary_bank_subelement(element) self._create_weight_window_generators_subelement(element, mesh_memo) self._create_weight_windows_file_element(element) self._create_weight_window_checkpoints_subelement(element) @@ -2714,6 +2744,7 @@ class Settings: settings._write_initial_source_from_xml_element(elem) settings._weight_windows_from_xml_element(elem, meshes) settings._weight_windows_on_from_xml_element(elem) + settings._shared_secondary_bank_from_xml_element(elem) settings._weight_windows_file_from_xml_element(elem) settings._weight_window_generators_from_xml_element(elem, meshes) settings._weight_window_checkpoints_from_xml_element(elem) diff --git a/src/bank.cpp b/src/bank.cpp index 5afdc2ecab..5b12b48fd0 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -7,6 +7,7 @@ #include "openmc/vector.h" #include +#include #include namespace openmc { @@ -43,6 +44,13 @@ vector> ifp_fission_lifetime_bank; // used to efficiently sort the fission bank after each iteration. vector progeny_per_particle; +// When shared secondary bank mode is enabled, secondaries produced during +// transport are collected in the write bank. When a secondary generation is +// complete, write is moved to read for transport, and a new empty write bank +// is created. This repeats until no secondaries remain. +SharedArray shared_secondary_bank_read; +SharedArray shared_secondary_bank_write; + } // namespace simulation //============================================================================== @@ -60,6 +68,8 @@ void free_memory_bank() simulation::ifp_source_lifetime_bank.clear(); simulation::ifp_fission_delayed_group_bank.clear(); simulation::ifp_fission_lifetime_bank.clear(); + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); } void init_fission_bank(int64_t max) @@ -68,13 +78,13 @@ void init_fission_bank(int64_t max) simulation::progeny_per_particle.resize(simulation::work_per_rank); } -// Performs an O(n) sort on the fission bank, by leveraging +// Performs an O(n) sort on a fission or secondary bank, by leveraging // the parent_id and progeny_id fields of banked particles. See the following // paper for more details: // "Reproducibility and Monte Carlo Eigenvalue Calculations," F.B. Brown and // T.M. Sutton, 1992 ANS Annual Meeting, Transactions of the American Nuclear // Society, Volume 65, Page 235. -void sort_fission_bank() +void sort_bank(SharedArray& bank, bool is_fission_bank) { // Ensure we don't read off the end of the array if we ran with 0 particles if (simulation::progeny_per_particle.size() == 0) { @@ -96,44 +106,200 @@ void sort_fission_bank() vector> sorted_ifp_lifetime_bank; // If there is not enough space, allocate a temporary vector and point to it - if (simulation::fission_bank.size() > - simulation::fission_bank.capacity() / 2) { - sorted_bank_holder.resize(simulation::fission_bank.size()); + if (bank.size() > bank.capacity() / 2) { + sorted_bank_holder.resize(bank.size()); sorted_bank = sorted_bank_holder.data(); } else { // otherwise, point sorted_bank to unused portion of the fission bank - sorted_bank = &simulation::fission_bank[simulation::fission_bank.size()]; + sorted_bank = bank.data() + bank.size(); } - if (settings::ifp_on) { + if (settings::ifp_on && is_fission_bank) { allocate_temporary_vector_ifp( sorted_ifp_delayed_group_bank, sorted_ifp_lifetime_bank); } - // Use parent and progeny indices to sort fission bank - for (int64_t i = 0; i < simulation::fission_bank.size(); i++) { - const auto& site = simulation::fission_bank[i]; - int64_t offset = site.parent_id - 1 - simulation::work_index[mpi::rank]; - int64_t idx = simulation::progeny_per_particle[offset] + site.progeny_id; - if (idx >= simulation::fission_bank.size()) { + // Use parent and progeny indices to sort bank + for (int64_t i = 0; i < bank.size(); i++) { + const auto& site = bank[i]; + if (site.parent_id < 0 || + site.parent_id >= + static_cast(simulation::progeny_per_particle.size())) { + fatal_error(fmt::format("Invalid parent_id {} for banked site (expected " + "range [0, {})).", + site.parent_id, simulation::progeny_per_particle.size())); + } + int64_t idx = + simulation::progeny_per_particle[site.parent_id] + site.progeny_id; + if (idx < 0 || idx >= bank.size()) { fatal_error("Mismatch detected between sum of all particle progeny and " - "shared fission bank size."); + "bank size during sorting."); } sorted_bank[idx] = site; - if (settings::ifp_on) { + if (settings::ifp_on && is_fission_bank) { copy_ifp_data_from_fission_banks( i, sorted_ifp_delayed_group_bank[idx], sorted_ifp_lifetime_bank[idx]); } } // Copy sorted bank into the fission bank - std::copy(sorted_bank, sorted_bank + simulation::fission_bank.size(), - simulation::fission_bank.data()); - if (settings::ifp_on) { + std::copy(sorted_bank, sorted_bank + bank.size(), bank.data()); + if (settings::ifp_on && is_fission_bank) { copy_ifp_data_to_fission_banks( sorted_ifp_delayed_group_bank.data(), sorted_ifp_lifetime_bank.data()); } } +// This function redistributes SourceSite particles across MPI ranks to +// achieve load balancing while preserving the global ordering of particles. +// +// GUARANTEES: +// ----------- +// 1. Global Order Preservation: After redistribution, each rank holds a +// contiguous slice of the original global ordering. For example, if the +// input across 3 ranks was: +// - Rank 0: IDs 0-4 +// - Rank 1: IDs 5-6 +// - Rank 2: IDs 7-200 +// Then after redistribution (assuming ~67 particles per rank): +// - Rank 0: IDs 0-66 (contiguous) +// - Rank 1: IDs 67-133 (contiguous) +// - Rank 2: IDs 134-200 (contiguous) +// The global ordering is always preserved - no rank will ever hold +// non-contiguous ID ranges like "0-4 and 100-200". +// +// 2. Even Load Balancing: Particles are distributed as evenly as possible. +// If total % n_procs != 0, the first 'remainder' ranks each get one extra +// particle (i.e., floor division with remainder distributed to lower +// ranks). This follows the same logic as calculate_work(). +// +// HOW IT WORKS: +// ------------- +// The algorithm uses overlap-based redistribution: +// 1. Each rank's current data occupies a range [cumulative_before[rank], +// cumulative_before[rank+1]) in the global index space. +// 2. Each rank's target data should occupy [cumulative_target[rank], +// cumulative_target[rank+1]) in the same global index space. +// 3. For each pair of (source_rank, dest_rank), we calculate the overlap +// between what source_rank currently has and what dest_rank needs. +// 4. MPI_Alltoallv transfers exactly these overlapping regions, with +// displacements ensuring data lands at the correct position in the +// receiving buffer. +// +// EDGE CASES HANDLED: +// ------------------- +// - Single rank (n_procs == 1): Returns immediately with local size, no MPI. +// - Empty total (all ranks have 0 particles): Returns 0 immediately. +// - Imbalanced input (e.g., one rank has all particles): Works correctly; +// that rank will send portions to all other ranks based on target ranges. +// - Non-divisible totals: First 'remainder' ranks get one extra particle. +int64_t synchronize_global_secondary_bank( + SharedArray& shared_secondary_bank) +{ + // Get current size of local bank + int64_t local_size = shared_secondary_bank.size(); + + if (mpi::n_procs == 1) { + return local_size; + } + +#ifdef OPENMC_MPI + // Gather all sizes to all ranks + vector all_sizes(mpi::n_procs); + MPI_Allgather(&local_size, 1, MPI_INT64_T, all_sizes.data(), 1, MPI_INT64_T, + mpi::intracomm); + + // Calculate total and check for empty case + int64_t total = 0; + for (int64_t size : all_sizes) { + total += size; + } + + // If we don't have any items to distribute, return + if (total == 0) { + return total; + } + + int64_t base_count = total / mpi::n_procs; + int64_t remainder = total % mpi::n_procs; + + // Calculate target size for each rank + // First 'remainder' ranks get base_count + 1, rest get base_count + vector target_sizes(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; ++i) { + target_sizes[i] = base_count + (i < remainder ? 1 : 0); + } + + // Calculate send and receive counts in terms of SourceSite objects + // (not bytes) + vector send_counts(mpi::n_procs, 0); + vector recv_counts(mpi::n_procs, 0); + vector send_displs(mpi::n_procs, 0); + vector recv_displs(mpi::n_procs, 0); + + // Calculate cumulative positions (starting index for each rank in the + // global array) + vector cumulative_before(mpi::n_procs + 1, 0); + vector cumulative_target(mpi::n_procs + 1, 0); + for (int i = 0; i < mpi::n_procs; ++i) { + cumulative_before[i + 1] = cumulative_before[i] + all_sizes[i]; + cumulative_target[i + 1] = cumulative_target[i] + target_sizes[i]; + } + + // Determine send and receive amounts for each rank + int64_t my_start = cumulative_before[mpi::rank]; + int64_t my_end = cumulative_before[mpi::rank + 1]; + int64_t my_target_start = cumulative_target[mpi::rank]; + int64_t my_target_end = cumulative_target[mpi::rank + 1]; + + for (int r = 0; r < mpi::n_procs; ++r) { + // Send: overlap between my current range and rank r's target range + int64_t send_overlap_start = std::max(my_start, cumulative_target[r]); + int64_t send_overlap_end = std::min(my_end, cumulative_target[r + 1]); + if (send_overlap_start < send_overlap_end) { + int64_t count = send_overlap_end - send_overlap_start; + int64_t displ = send_overlap_start - my_start; + if (count > std::numeric_limits::max() || + displ > std::numeric_limits::max()) { + fatal_error("Secondary bank size exceeds MPI_Alltoallv int limit."); + } + send_counts[r] = static_cast(count); + send_displs[r] = static_cast(displ); + } + + // Recv: overlap between rank r's current range and my target range + int64_t recv_overlap_start = + std::max(cumulative_before[r], my_target_start); + int64_t recv_overlap_end = + std::min(cumulative_before[r + 1], my_target_end); + if (recv_overlap_start < recv_overlap_end) { + int64_t count = recv_overlap_end - recv_overlap_start; + int64_t displ = recv_overlap_start - my_target_start; + if (count > std::numeric_limits::max() || + displ > std::numeric_limits::max()) { + fatal_error("Secondary bank size exceeds MPI_Alltoallv int limit."); + } + recv_counts[r] = static_cast(count); + recv_displs[r] = static_cast(displ); + } + } + + // Prepare receive buffer with target size + SharedArray new_bank(target_sizes[mpi::rank]); + + // Perform all-to-all redistribution using the custom MPI type + MPI_Alltoallv(shared_secondary_bank.data(), send_counts.data(), + send_displs.data(), mpi::source_site, new_bank.data(), recv_counts.data(), + recv_displs.data(), mpi::source_site, mpi::intracomm); + + // Replace old bank with redistributed data + shared_secondary_bank = std::move(new_bank); + + return total; +#else + return local_size; +#endif +} + //============================================================================== // C API //============================================================================== diff --git a/src/cell.cpp b/src/cell.cpp index ebe28c3d2c..06bc29a662 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -340,6 +340,63 @@ void Cell::to_hdf5(hid_t cell_group) const close_group(group); } +//============================================================================== +// XML parsing helpers for nodes +//============================================================================== + +vector parse_cell_material_xml(pugi::xml_node node, int32_t cell_id) +{ + vector mats { + get_node_array(node, "material", true)}; + if (mats.empty()) { + fatal_error(fmt::format( + "An empty material element was specified for cell {}", cell_id)); + } + vector material; + material.reserve(mats.size()); + for (const auto& mat : mats) { + if (mat == "void") { + material.push_back(MATERIAL_VOID); + } else { + material.push_back(std::stoi(mat)); + } + } + return material; +} + +vector parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id) +{ + auto temperatures = get_node_array(node, "temperature"); + if (temperatures.empty()) { + fatal_error(fmt::format( + "An empty temperature element was specified for cell {}", cell_id)); + } + for (auto T : temperatures) { + if (T < 0) { + fatal_error(fmt::format( + "Cell {} was specified with a negative temperature", cell_id)); + } + } + return temperatures; +} + +vector parse_cell_density_xml(pugi::xml_node node, int32_t cell_id) +{ + auto densities = get_node_array(node, "density"); + if (densities.empty()) { + fatal_error(fmt::format( + "An empty density element was specified for cell {}", cell_id)); + } + for (auto rho : densities) { + if (rho <= 0) { + fatal_error(fmt::format( + "Cell {} was specified with a density less than or equal to zero", + cell_id)); + } + } + return densities; +} + //============================================================================== // CSGCell implementation //============================================================================== @@ -390,26 +447,12 @@ CSGCell::CSGCell(pugi::xml_node cell_node) // universe), more than one material (distribmats), and some materials may // be "void". if (material_present) { - vector mats { - get_node_array(cell_node, "material", true)}; - if (mats.size() > 0) { - material_.reserve(mats.size()); - for (std::string mat : mats) { - if (mat.compare("void") == 0) { - material_.push_back(MATERIAL_VOID); - } else { - material_.push_back(std::stoi(mat)); - } - } - } else { - fatal_error(fmt::format( - "An empty material element was specified for cell {}", id_)); - } + material_ = parse_cell_material_xml(cell_node, id_); } // Read the temperature element which may be distributed like materials. if (check_for_node(cell_node, "temperature")) { - sqrtkT_ = get_node_array(cell_node, "temperature"); + sqrtkT_ = parse_cell_temperature_xml(cell_node, id_); sqrtkT_.shrink_to_fit(); // Make sure this is a material-filled cell. @@ -420,14 +463,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node) id_)); } - // Make sure all temperatures are non-negative. - for (auto T : sqrtkT_) { - if (T < 0) { - fatal_error(fmt::format( - "Cell {} was specified with a negative temperature", id_)); - } - } - // Convert to sqrt(k*T). for (auto& T : sqrtkT_) { T = std::sqrt(K_BOLTZMANN * T); @@ -440,7 +475,7 @@ CSGCell::CSGCell(pugi::xml_node cell_node) // Note: calculating the actual density multiplier is deferred until materials // are finalized. density_mult_ contains the true density in the meantime. if (check_for_node(cell_node, "density")) { - density_mult_ = get_node_array(cell_node, "density"); + density_mult_ = parse_cell_density_xml(cell_node, id_); density_mult_.shrink_to_fit(); // Make sure this is a material-filled cell. @@ -461,15 +496,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node) id_)); } } - - // Make sure all densities are non-negative and greater than zero. - for (auto rho : density_mult_) { - if (rho <= 0) { - fatal_error(fmt::format( - "Cell {} was specified with a density less than or equal to zero", - id_)); - } - } } // Read the region specification. diff --git a/src/collision_track.cpp b/src/collision_track.cpp index 03cbc32b7b..2e749007ff 100644 --- a/src/collision_track.cpp +++ b/src/collision_track.cpp @@ -200,8 +200,13 @@ void collision_track_record(Particle& particle) return; int cell_id = model::cells[cell_index]->id_; - const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); - std::string nuclide = nuclide_ptr->name_; + std::string nuclide {}; + int nuclide_id = 0; + if (particle.event_nuclide() != NUCLIDE_NONE) { + const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); + nuclide = nuclide_ptr->name_; + nuclide_id = nuclide_ptr->particle_type().pdg_number(); + } int universe_id = model::universes[particle.lowest_coord().universe()]->id_; double delta_E = particle.E_last() - particle.E(); int material_index = particle.material(); @@ -224,8 +229,7 @@ void collision_track_record(Particle& particle) site.event_mt = particle.event_mt(); site.delayed_group = particle.delayed_group(); site.cell_id = cell_id; - site.nuclide_id = - 10000 * nuclide_ptr->Z_ + 10 * nuclide_ptr->A_ + nuclide_ptr->metastable_; + site.nuclide_id = nuclide_id; site.material_id = material_id; site.universe_id = universe_id; site.n_collision = particle.n_collision(); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 571182fa6b..a5cc71b523 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -50,6 +50,10 @@ namespace openmc { DAGUniverse::DAGUniverse(pugi::xml_node node) { + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + DensityOverrides density_overrides; + if (check_for_node(node, "id")) { id_ = std::stoi(get_node_value(node, "id")); } else { @@ -76,24 +80,77 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) adjust_material_ids_ = get_node_value_bool(node, "auto_mat_ids"); } - // get material assignment overloading - if (check_for_node(node, "material_overrides")) { - auto mat_node = node.child("material_overrides"); - // loop over all subelements (each subelement corresponds to a material) - for (pugi::xml_node cell_node : mat_node.children("cell_override")) { - // Store assignment reference name - int32_t ref_assignment = std::stoi(get_node_value(cell_node, "id")); + // Get material assignment overrides from nested DAGMC cell elements. + if (node.child("cell")) { + for (pugi::xml_node cell_node : node.children("cell")) { + if (!check_for_node(cell_node, "id")) { + fatal_error( + "Must specify id for each DAGMC cell override in ."); + } - // Get mat name for each assignement instances - vector instance_mats = - get_node_array(cell_node, "material_ids"); + int32_t cell_id = std::stoi(get_node_value(cell_node, "id")); - // Store mat name for each instances - material_overrides_.emplace(ref_assignment, instance_mats); + if (check_for_node(cell_node, "region")) { + fatal_error(fmt::format( + "DAGMC cell {} override cannot specify a region.", cell_id)); + } + if (check_for_node(cell_node, "fill")) { + fatal_error(fmt::format( + "DAGMC cell {} override currently only supports material fills.", + cell_id)); + } + if (check_for_node(cell_node, "universe")) { + fatal_error(fmt::format( + "DAGMC cell {} override cannot specify a universe.", cell_id)); + } + if (check_for_node(cell_node, "translation") || + check_for_node(cell_node, "rotation")) { + fatal_error(fmt::format( + "DAGMC cell {} override does not support translation or rotation.", + cell_id)); + } + if (!check_for_node(cell_node, "material")) { + fatal_error(fmt::format( + "DAGMC cell {} override must specify material.", cell_id)); + } + + auto inserted = material_overrides.emplace( + cell_id, parse_cell_material_xml(cell_node, cell_id)); + if (!inserted.second) { + fatal_error(fmt::format( + "Duplicate DAGMC cell override specified for cell {}", cell_id)); + } + + if (check_for_node(cell_node, "temperature")) { + temperature_overrides.emplace( + cell_id, parse_cell_temperature_xml(cell_node, cell_id)); + } + + if (check_for_node(cell_node, "density")) { + density_overrides.emplace( + cell_id, parse_cell_density_xml(cell_node, cell_id)); + } + } + } else if (check_for_node(node, "material_overrides")) { + if (node.child("cell")) { + fatal_error("DAGMCUniverse cannot specify both and " + " sub-elements. Use elements only."); + } + warning("DAGMCUniverse is deprecated. Use nested " + " elements under instead."); + for (pugi::xml_node co : + node.child("material_overrides").children("cell_override")) { + int32_t cell_id = std::stoi(get_node_value(co, "id")); + std::istringstream iss(co.child("material_ids").text().get()); + vector mats; + for (std::string s; iss >> s;) { + mats.push_back(s == "void" ? MATERIAL_VOID : std::stoi(s)); + } + material_overrides.emplace(cell_id, mats); } } - initialize(); + initialize(material_overrides, temperature_overrides, density_overrides); } DAGUniverse::DAGUniverse( @@ -110,9 +167,12 @@ DAGUniverse::DAGUniverse(std::shared_ptr dagmc_ptr, : dagmc_instance_(dagmc_ptr), filename_(filename), adjust_geometry_ids_(auto_geom_ids), adjust_material_ids_(auto_mat_ids) { + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + DensityOverrides density_overrides; set_id(); init_metadata(); - init_geometry(); + init_geometry(material_overrides, temperature_overrides, density_overrides); } void DAGUniverse::set_id() @@ -130,6 +190,15 @@ void DAGUniverse::set_id() } void DAGUniverse::initialize() +{ + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + initialize(material_overrides, temperature_overrides); +} + +void DAGUniverse::initialize(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides) { #ifdef OPENMC_UWUW_ENABLED // read uwuw materials from the .h5m file if present @@ -140,7 +209,7 @@ void DAGUniverse::initialize() init_metadata(); - init_geometry(); + init_geometry(material_overrides, temperature_overrides, density_overrides); } void DAGUniverse::init_dagmc() @@ -176,7 +245,9 @@ void DAGUniverse::init_metadata() MB_CHK_ERR_CONT(rval); } -void DAGUniverse::init_geometry() +void DAGUniverse::init_geometry(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides) { moab::ErrorCode rval; @@ -202,6 +273,9 @@ void DAGUniverse::init_geometry() : dagmc_instance_->id_by_index(3, c->dag_index()); c->universe_ = this->id_; c->fill_ = C_NONE; // no fill, single universe + if (dagmc_instance_->is_implicit_complement(vol_handle)) { + c->name_ = "implicit complement"; + } auto in_map = model::cell_map.find(c->id_); if (in_map == model::cell_map.end()) { @@ -230,16 +304,68 @@ void DAGUniverse::init_geometry() if (mat_str == "graveyard") { graveyard = vol_handle; } - // material void checks - if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") { + if (material_overrides.count(c->id_)) { + override_assign_material(c, material_overrides); + } else if (mat_str == "void" || mat_str == "vacuum" || + mat_str == "graveyard") { c->material_.push_back(MATERIAL_VOID); + } else if (uses_uwuw()) { + uwuw_assign_material(vol_handle, c); } else { - if (material_overrides_.count(c->id_)) { - override_assign_material(c); - } else if (uses_uwuw()) { - uwuw_assign_material(vol_handle, c); - } else { - legacy_assign_material(mat_str, c); + legacy_assign_material(mat_str, c); + } + + if (temperature_overrides.count(c->id_)) { + if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) { + fatal_error(fmt::format("DAGMC cell {} was specified with a " + "temperature but no non-void material.", + c->id_)); + } + + c->sqrtkT_.clear(); + const auto& temp_overrides = temperature_overrides.at(c->id_); + c->sqrtkT_.reserve(temp_overrides.size()); + for (auto T : temp_overrides) { + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * T)); + } + + if (settings::verbosity >= 10) { + std::stringstream override_values; + for (size_t i = 0; i < temp_overrides.size(); ++i) { + if (i > 0) { + override_values << " "; + } + override_values << temp_overrides[i]; + } + auto msg = fmt::format("Overriding DAGMC cell {} property " + "'temperature [K]' with value(s): {}", + c->id_, override_values.str()); + write_message(msg, 10); + } + } + + if (density_overrides.count(c->id_)) { + if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) { + fatal_error(fmt::format("DAGMC cell {} was specified with a density " + "but no non-void material.", + c->id_)); + } + // density_mult_ holds the true density until materials are finalized, + // at which point it is converted to a proper multiplier (same as CSG). + c->density_mult_ = density_overrides.at(c->id_); + + if (settings::verbosity >= 10) { + const auto& dens = density_overrides.at(c->id_); + std::stringstream override_values; + for (size_t i = 0; i < dens.size(); ++i) { + if (i > 0) + override_values << " "; + override_values << dens[i]; + } + write_message(fmt::format("Overriding DAGMC cell {} property " + "'density [g/cm³]' with value(s): {}", + c->id_, override_values.str()), + 10); } } @@ -252,18 +378,21 @@ void DAGUniverse::init_geometry() continue; } - // assign cell temperature - const auto& mat = model::materials[model::material_map.at(c->material_[0])]; - if (dagmc_instance_->has_prop(vol_handle, "temp")) { - rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value); - MB_CHK_ERR_CONT(rval); - double temp = std::stod(temp_value); - c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * temp)); - } else if (mat->temperature() > 0.0) { - c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * mat->temperature())); - } else { - c->sqrtkT_.push_back( - std::sqrt(K_BOLTZMANN * settings::temperature_default)); + // assign cell temperature if not explicitly overridden + if (c->sqrtkT_.empty()) { + const auto& mat = + model::materials[model::material_map.at(c->material_[0])]; + if (dagmc_instance_->has_prop(vol_handle, "temp")) { + rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value); + MB_CHK_ERR_CONT(rval); + double temp = std::stod(temp_value); + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * temp)); + } else if (mat->temperature() > 0.0) { + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * mat->temperature())); + } else { + c->sqrtkT_.push_back( + std::sqrt(K_BOLTZMANN * settings::temperature_default)); + } } model::cells.emplace_back(std::move(c)); @@ -630,7 +759,8 @@ void DAGUniverse::uwuw_assign_material( #endif // OPENMC_UWUW_ENABLED } -void DAGUniverse::override_assign_material(std::unique_ptr& c) const +void DAGUniverse::override_assign_material(std::unique_ptr& c, + const MaterialOverrides& material_overrides) const { // if Cell ID matches an override key, use it to override the material // assignment else if UWUW is used, get the material assignment from the DAGMC @@ -638,17 +768,30 @@ void DAGUniverse::override_assign_material(std::unique_ptr& c) const // Notify User that an override is being applied on a DAGMCCell write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8); + const auto& mat_overrides = material_overrides.at(c->id_); if (settings::verbosity >= 10) { - auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on " - "override information (see input XML).", - c->id_); + std::stringstream override_values; + for (size_t i = 0; i < mat_overrides.size(); ++i) { + if (i > 0) { + override_values << " "; + } + if (mat_overrides[i] == MATERIAL_VOID) { + override_values << "void"; + } else { + override_values << mat_overrides[i]; + } + } + auto msg = fmt::format("Overriding DAGMC cell {} property 'material' " + "with value(s): {}", + c->id_, override_values.str()); write_message(msg, 10); } // Override the material assignment for each cell instance using the legacy // assignement - for (auto mat_id : material_overrides_.at(c->id_)) { - if (model::material_map.find(mat_id) == model::material_map.end()) { + for (auto mat_id : mat_overrides) { + if (mat_id != MATERIAL_VOID && + model::material_map.find(mat_id) == model::material_map.end()) { fatal_error(fmt::format( "Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_)); } diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index ba8658f10e..5a34524222 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -262,9 +262,11 @@ std::pair SphericalIndependent::sample(uint64_t* seed) const MeshSpatial::MeshSpatial(pugi::xml_node node) { - if (get_node_value(node, "type", true, true) != "mesh") { - fatal_error(fmt::format( - "Incorrect spatial type '{}' for a MeshSpatial distribution")); + auto spatial_type = get_node_value(node, "type", true, true); + if (spatial_type != "mesh") { + fatal_error( + fmt::format("Incorrect spatial type '{}' for a MeshSpatial distribution", + spatial_type)); } // No in-tet distributions implemented, could include distributions for the diff --git a/src/event.cpp b/src/event.cpp index f33e132d0a..2d436bb9dc 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,6 +1,9 @@ #include "openmc/event.h" +#include "openmc/bank.h" +#include "openmc/error.h" #include "openmc/material.h" +#include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/timer.h" @@ -64,7 +67,8 @@ void process_init_events(int64_t n_particles, int64_t source_offset) simulation::time_event_init.start(); #pragma omp parallel for schedule(runtime) for (int64_t i = 0; i < n_particles; i++) { - initialize_history(simulation::particles[i], source_offset + i + 1); + initialize_particle_track( + simulation::particles[i], source_offset + i + 1, false); dispatch_xs_event(i); } simulation::time_event_init.stop(); @@ -136,7 +140,7 @@ void process_surface_crossing_events() int64_t buffer_idx = simulation::surface_crossing_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; p.event_cross_surface(); - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); if (p.alive()) dispatch_xs_event(buffer_idx); } @@ -155,7 +159,7 @@ void process_collision_events() int64_t buffer_idx = simulation::collision_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; p.event_collide(); - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); if (p.alive()) dispatch_xs_event(buffer_idx); } @@ -176,4 +180,45 @@ void process_death_events(int64_t n_particles) simulation::time_event_death.stop(); } +void process_transport_events() +{ + while (true) { + int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), + simulation::calculate_nonfuel_xs_queue.size(), + simulation::advance_particle_queue.size(), + simulation::surface_crossing_queue.size(), + simulation::collision_queue.size()}); + + if (max == 0) { + break; + } else if (max == simulation::calculate_fuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_fuel_xs_queue); + } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); + } else if (max == simulation::advance_particle_queue.size()) { + process_advance_particle_events(); + } else if (max == simulation::surface_crossing_queue.size()) { + process_surface_crossing_events(); + } else if (max == simulation::collision_queue.size()) { + process_collision_events(); + } + } +} + +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank) +{ + simulation::time_event_init.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + initialize_particle_track(simulation::particles[i], offset + i + 1, true); + const SourceSite& site = shared_secondary_bank[offset + i]; + simulation::particles[i].event_revive_from_secondary(site); + if (simulation::particles[i].alive()) { + dispatch_xs_event(i); + } + } + simulation::time_event_init.stop(); +} + } // namespace openmc diff --git a/src/finalize.cpp b/src/finalize.cpp index 59711dcc31..fd891d9dd8 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -149,6 +149,8 @@ int openmc_finalize() settings::uniform_source_sampling = false; settings::ufs_on = false; settings::urr_ptables_on = true; + settings::use_decay_photons = false; + settings::use_shared_secondary_bank = false; settings::verbosity = -1; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; @@ -219,6 +221,7 @@ int openmc_reset() settings::cmfd_run = false; simulation::n_lost_particles = 0; + simulation::simulation_tracks_completed = 0; return 0; } diff --git a/src/geometry.cpp b/src/geometry.cpp index ddb61385f1..bf654f4f9b 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -26,16 +26,22 @@ int n_coord_levels; vector overlap_check_count; +vector overlap_keys; +std::unordered_map overlap_key_index; + } // namespace model //============================================================================== // Non-member functions //============================================================================== -bool check_cell_overlap(GeometryState& p, bool error) +int check_cell_overlap(GeometryState& p, bool error) { int n_coord = p.n_coord(); + // If no overlap found, return a nonphysical index + int overlap_index = -1; + // Loop through each coordinate level for (int j = 0; j < n_coord; j++) { Universe& univ = *model::universes[p.coord(j).universe()]; @@ -44,21 +50,40 @@ bool check_cell_overlap(GeometryState& p, bool error) for (auto index_cell : univ.cells_) { Cell& c = *model::cells[index_cell]; if (c.contains(p.coord(j).r(), p.coord(j).u(), p.surface())) { +#pragma omp atomic + ++model::overlap_check_count[index_cell]; if (index_cell != p.coord(j).cell()) { if (error) { fatal_error( fmt::format("Overlapping cells detected: {}, {} on universe {}", c.id_, model::cells[p.coord(j).cell()]->id_, univ.id_)); } - return true; + + // With no fatal error (plotter is calling), now adds overlaps and + // ensures order does not matter when making overlap key + int cell_a = model::cells[index_cell]->id_; + int cell_b = model::cells[p.coord(j).cell()]->id_; + int a = std::min(cell_a, cell_b); + int b = std::max(cell_a, cell_b); + OverlapKey key {univ.id_, a, b}; +#pragma omp critical(overlap_key_update) + { + auto it = model::overlap_key_index.find(key); + if (it != model::overlap_key_index.end()) { + overlap_index = it->second; // already exists, reuse index + } else { + int idx = int(model::overlap_keys.size()); + model::overlap_keys.push_back(key); + model::overlap_key_index[key] = idx; + overlap_index = idx; + } + } + break; } -#pragma omp atomic - ++model::overlap_check_count[index_cell]; } } } - - return false; + return overlap_index; } //============================================================================== diff --git a/src/ifp.cpp b/src/ifp.cpp index a5157440df..f0f98b8ef9 100644 --- a/src/ifp.cpp +++ b/src/ifp.cpp @@ -32,13 +32,13 @@ void ifp(const Particle& p, int64_t idx) { if (is_beta_effective_or_both()) { const auto& delayed_groups = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_source_delayed_group_bank[p.current_work()]; simulation::ifp_fission_delayed_group_bank[idx] = _ifp(p.delayed_group(), delayed_groups); } if (is_generation_time_or_both()) { const auto& lifetimes = - simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_source_lifetime_bank[p.current_work()]; simulation::ifp_fission_lifetime_bank[idx] = _ifp(p.lifetime(), lifetimes); } } diff --git a/src/initialize.cpp b/src/initialize.cpp index c04a2fcbdd..78b414f786 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -161,7 +161,7 @@ void initialize_mpi(MPI_Comm intracomm) // Create bank datatype SourceSite b; - MPI_Aint disp[11]; + MPI_Aint disp[14]; MPI_Get_address(&b.r, &disp[0]); MPI_Get_address(&b.u, &disp[1]); MPI_Get_address(&b.E, &disp[2]); @@ -173,14 +173,35 @@ void initialize_mpi(MPI_Comm intracomm) MPI_Get_address(&b.parent_nuclide, &disp[8]); MPI_Get_address(&b.parent_id, &disp[9]); MPI_Get_address(&b.progeny_id, &disp[10]); - for (int i = 10; i >= 0; --i) { + MPI_Get_address(&b.wgt_born, &disp[11]); + MPI_Get_address(&b.wgt_ww_born, &disp[12]); + MPI_Get_address(&b.n_split, &disp[13]); + for (int i = 13; i >= 0; --i) { disp[i] -= disp[0]; } - int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - MPI_Datatype types[] {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, - MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; - MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site); + // Block counts for each field + int blocks[] = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + // Types for each field + MPI_Datatype types[] = { + MPI_DOUBLE, // r (3 doubles) + MPI_DOUBLE, // u (3 doubles) + MPI_DOUBLE, // E + MPI_DOUBLE, // time + MPI_DOUBLE, // wgt + MPI_INT, // delayed_group + MPI_INT, // surf_id + MPI_INT, // particle (enum) + MPI_INT, // parent_nuclide + MPI_INT64_T, // parent_id + MPI_INT64_T, // progeny_id + MPI_DOUBLE, // wgt_born + MPI_DOUBLE, // wgt_ww_born + MPI_INT64_T // n_split + }; + + MPI_Type_create_struct(14, blocks, disp, types, &mpi::source_site); MPI_Type_commit(&mpi::source_site); CollisionTrackSite bc; @@ -366,6 +387,28 @@ int parse_command_line(int argc, char* argv[]) return 0; } +// TODO: Pulse-height tallies require per-history scoring across the full +// particle tree (parent + all descendants). The shared secondary bank +// transports each secondary as an independent Particle, breaking this +// assumption. A proper fix would defer pulse-height scoring: save +// (root_source_id, cell, pht_storage) per particle, then aggregate by +// root_source_id after all secondary generations complete before scoring +// into the histogram. For now, disable shared secondary when pulse-height +// tallies are present. +static void check_pulse_height_compatibility() +{ + if (settings::use_shared_secondary_bank) { + for (const auto& t : model::tallies) { + if (t->type_ == TallyType::PULSE_HEIGHT) { + settings::use_shared_secondary_bank = false; + warning("Pulse-height tallies are not yet compatible with the shared " + "secondary bank. Disabling shared secondary bank."); + break; + } + } + } +} + bool read_model_xml() { std::string model_filename = settings::path_input; @@ -460,6 +503,8 @@ bool read_model_xml() if (check_for_node(root, "tallies")) read_tallies_xml(root.child("tallies")); + check_pulse_height_compatibility(); + // Initialize distribcell_filters prepare_distribcell(); @@ -505,6 +550,8 @@ void read_separate_xml_files() read_tallies_xml(); + check_pulse_height_compatibility(); + // Initialize distribcell_filters prepare_distribcell(); diff --git a/src/lattice.cpp b/src/lattice.cpp index e799a340e6..9e56c58e8e 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -10,6 +10,7 @@ #include "openmc/geometry.h" #include "openmc/geometry_aux.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/string_utils.h" #include "openmc/vector.h" #include "openmc/xml_interface.h" @@ -260,25 +261,33 @@ std::pair> RectLattice::distance( // Determine the oncoming edge. double x0 {copysign(0.5 * pitch_[0], u.x)}; double y0 {copysign(0.5 * pitch_[1], u.y)}; - double z0; - double d = std::min( - u.x != 0.0 ? (x0 - x) / u.x : INFTY, u.y != 0.0 ? (y0 - y) / u.y : INFTY); + // Evaluate the distance to each oncoming edge independently. Comparing these + // distances directly (rather than reconstructing the crossing position) + // avoids the floating-point cancellation that occurs for large pitches. + double dx = u.x != 0.0 ? (x0 - x) / u.x : INFTY; + double dy = u.y != 0.0 ? (y0 - y) / u.y : INFTY; + double dz = INFTY; if (is_3d_) { - z0 = copysign(0.5 * pitch_[2], u.z); - d = std::min(d, u.z != 0.0 ? (z0 - z) / u.z : INFTY); + double z0 {copysign(0.5 * pitch_[2], u.z)}; + dz = u.z != 0.0 ? (z0 - z) / u.z : INFTY; } - // Determine which lattice boundaries are being crossed + // The distance to the nearest lattice boundary is the smallest axial + // distance. + double d = std::min({dx, dy, dz}); + + // Determine which lattice boundaries are being crossed. The axis attaining + // the minimum is exactly equal to d, so at least one translation is always + // set for a finite crossing; a near-equal second axis indicates a corner + // crossing. array lattice_trans = {0, 0, 0}; - if (u.x != 0.0 && std::abs(x + u.x * d - x0) < FP_PRECISION) + if (isclose(d, dx, FP_COINCIDENT, FP_PRECISION)) lattice_trans[0] = copysign(1, u.x); - if (u.y != 0.0 && std::abs(y + u.y * d - y0) < FP_PRECISION) + if (isclose(d, dy, FP_COINCIDENT, FP_PRECISION)) lattice_trans[1] = copysign(1, u.y); - if (is_3d_) { - if (u.z != 0.0 && std::abs(z + u.z * d - z0) < FP_PRECISION) - lattice_trans[2] = copysign(1, u.z); - } + if (is_3d_ && isclose(d, dz, FP_COINCIDENT, FP_PRECISION)) + lattice_trans[2] = copysign(1, u.z); return {d, lattice_trans}; } diff --git a/src/math_functions.cpp b/src/math_functions.cpp index 81f08fa04e..3befa8bc2e 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -959,4 +959,12 @@ void get_energy_index( } } +// Return true if two floating-point values are approximately equal within a +// combined relative and absolute tolerance. +bool isclose(double a, double b, double rel_tol, double abs_tol) +{ + return std::abs(a - b) <= + std::max(rel_tol * std::max(std::abs(a), std::abs(b)), abs_tol); +} + } // namespace openmc diff --git a/src/mesh.cpp b/src/mesh.cpp index 0b7cfe4a3b..0fe0033868 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -6,7 +6,8 @@ #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t -#include // for accumulate +#include +#include // for accumulate #include #ifdef _MSC_VER @@ -1080,13 +1081,26 @@ int StructuredMesh::get_bin(Position r) const int StructuredMesh::n_bins() const { - return std::accumulate( - shape_.begin(), shape_.begin() + n_dimension_, 1, std::multiplies<>()); + // Bin indices are stored as 32-bit ints in the tally system. + int64_t n = 1; + for (int i = 0; i < n_dimension_; ++i) + n *= shape_[i]; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many bins ({}) for 32-bit tally indexing", id_, n)); + } + return static_cast(n); } int StructuredMesh::n_surface_bins() const { - return 4 * n_dimension_ * n_bins(); + // Surface bin indices are stored as 32-bit ints in the tally system. + int64_t n = static_cast(n_bins()) * 4 * n_dimension_; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many surface bins ({}) for tally indexing", id_, n)); + } + return static_cast(n); } tensor::Tensor StructuredMesh::count_sites( @@ -3670,7 +3684,7 @@ Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const // Get tet vertex coordinates from LibMesh std::array tet_verts; for (int i = 0; i < elem.n_nodes(); i++) { - auto node_ref = elem.node_ref(i); + const auto& node_ref = elem.node_ref(i); tet_verts[i] = {node_ref(0), node_ref(1), node_ref(2)}; } // Samples position within tet using Barycentric coordinates @@ -3700,7 +3714,7 @@ int LibMesh::n_vertices() const Position LibMesh::vertex(int vertex_id) const { - const auto node_ref = m_->node_ref(vertex_id); + const auto& node_ref = m_->node_ref(vertex_id); if (length_multiplier_ > 0.0) { return length_multiplier_ * Position(node_ref(0), node_ref(1), node_ref(2)); } else { diff --git a/src/output.cpp b/src/output.cpp index f8c0f2a97a..04a8caa606 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -501,6 +501,14 @@ void print_runtime() show_rate("Calculation Rate (inactive)", speed_inactive); } show_rate("Calculation Rate (active)", speed_active); + + // Display track rate when weight windows are enabled + if (settings::weight_windows_on) { + double speed_tracks = + simulation::simulation_tracks_completed / time_active.elapsed(); + fmt::print( + " {:<33} = {:.6} tracks/second\n", "Track Rate (active)", speed_tracks); + } } //============================================================================== @@ -613,8 +621,15 @@ void write_tallies() if (model::tallies.empty()) return; + // Tag tallies.out written during the forward solve of an adjoint run + const char* forward = + (FlatSourceDomain::solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) + ? "forward." + : ""; + // Set filename for tallies_out - std::string filename = fmt::format("{}tallies.out", settings::path_output); + std::string filename = + fmt::format("{}tallies.{}out", settings::path_output, forward); // Open the tallies.out file. std::ofstream tallies_out; diff --git a/src/particle.cpp b/src/particle.cpp index 46df63cd13..65ff2ad619 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -94,7 +94,7 @@ bool Particle::create_secondary( // Increment number of secondaries created (for ParticleProductionFilter) n_secondaries()++; - auto& bank = secondary_bank().emplace_back(); + SourceSite bank; bank.particle = type; bank.wgt = wgt; bank.r = r(); @@ -102,12 +102,21 @@ bool Particle::create_secondary( bank.E = settings::run_CE ? E : g(); bank.time = time(); bank_second_E() += bank.E; + bank.parent_id = current_work(); + if (settings::use_shared_secondary_bank) { + bank.progeny_id = n_progeny()++; + } + bank.wgt_born = wgt_born(); + bank.wgt_ww_born = wgt_ww_born(); + bank.n_split = n_split(); + + local_secondary_bank().emplace_back(bank); return true; } void Particle::split(double wgt) { - auto& bank = secondary_bank().emplace_back(); + SourceSite bank; bank.particle = type(); bank.wgt = wgt; bank.r = r(); @@ -122,6 +131,16 @@ void Particle::split(double wgt) int surf_id = model::surfaces[surface_index()]->id_; bank.surf_id = (surface() > 0) ? surf_id : -surf_id; } + + bank.wgt_born = wgt_born(); + bank.wgt_ww_born = wgt_ww_born(); + bank.n_split = n_split(); + bank.parent_id = current_work(); + if (settings::use_shared_secondary_bank) { + bank.progeny_id = n_progeny()++; + } + + local_secondary_bank().emplace_back(bank); } void Particle::from_source(const SourceSite* src) @@ -168,6 +187,10 @@ void Particle::from_source(const SourceSite* src) int index_plus_one = model::surface_map[std::abs(src->surf_id)] + 1; surface() = (src->surf_id > 0) ? index_plus_one : -index_plus_one; } + + wgt_born() = src->wgt_born; + wgt_ww_born() = src->wgt_ww_born; + n_split() = src->n_split; } void Particle::event_calculate_xs() @@ -315,13 +338,14 @@ void Particle::event_cross_surface() boundary().lattice_translation()[2] != 0) { // Particle crosses lattice boundary + int i_lattice = coord(boundary().coord_level() - 1).lattice(); bool verbose = settings::verbosity >= 10 || trace(); cross_lattice(*this, boundary(), verbose); event() = TallyEvent::LATTICE; // Score cell to cell partial currents if (!model::active_surface_tallies.empty()) { - auto& lat {*model::lattices[lowest_coord().lattice()]}; + auto& lat {*model::lattices[i_lattice]}; bool is_valid; Direction normal = lat.get_normal(boundary().lattice_translation(), is_valid); @@ -449,61 +473,72 @@ void Particle::event_collide() #endif } -void Particle::event_revive_from_secondary() +void Particle::event_revive_from_secondary(const SourceSite& site) +{ + // Write final position for the previous track (skip if this is a freshly + // constructed particle with no prior track, e.g., Phase 2 of shared + // secondary transport) + if (write_track() && n_event() > 0) { + write_particle_track(*this); + } + + from_source(&site); + + n_event() = 0; + if (!settings::use_shared_secondary_bank) { + n_tracks()++; + } + bank_second_E() = 0.0; + + // Subtract secondary particle energy from interim pulse-height results. + // In shared secondary mode, this subtraction was already done on the parent + // particle during create_secondary(), so skip it here. + if (!settings::use_shared_secondary_bank && + !model::active_pulse_height_tallies.empty() && this->type().is_photon()) { + // Since the birth cell of the particle has not been set we + // have to determine it before the energy of the secondary particle can be + // removed from the pulse-height of this cell. + if (lowest_coord().cell() == C_NONE) { + bool verbose = settings::verbosity >= 10 || trace(); + if (!exhaustive_find_cell(*this, verbose)) { + mark_as_lost("Could not find the cell containing particle " + + std::to_string(id())); + return; + } + // Set birth cell attribute + if (cell_born() == C_NONE) + cell_born() = lowest_coord().cell(); + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell(); + } + n_coord_last() = n_coord(); + } + pht_secondary_particles(); + } + + // Enter new particle in particle track file + if (write_track()) + add_particle_track(*this); +} + +void Particle::event_check_limit_and_revive() { // If particle has too many events, display warning and kill it - ++n_event(); + n_event()++; if (n_event() == settings::max_particle_events) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; } - // Check for secondary particles if this particle is dead - if (!alive()) { - // Write final position for this particle - if (write_track()) { - write_particle_track(*this); - } - - // If no secondary particles, break out of event loop - if (secondary_bank().empty()) - return; - - from_source(&secondary_bank().back()); - secondary_bank().pop_back(); - n_event() = 0; - bank_second_E() = 0.0; - - // Subtract secondary particle energy from interim pulse-height results - if (!model::active_pulse_height_tallies.empty() && - this->type().is_photon()) { - // Since the birth cell of the particle has not been set we - // have to determine it before the energy of the secondary particle can be - // removed from the pulse-height of this cell. - if (lowest_coord().cell() == C_NONE) { - bool verbose = settings::verbosity >= 10 || trace(); - if (!exhaustive_find_cell(*this, verbose)) { - mark_as_lost("Could not find the cell containing particle " + - std::to_string(id())); - return; - } - // Set birth cell attribute - if (cell_born() == C_NONE) - cell_born() = lowest_coord().cell(); - - // Initialize last cells from current cell - for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell(); - } - n_coord_last() = n_coord(); - } - pht_secondary_particles(); - } - - // Enter new particle in particle track file - if (write_track()) - add_particle_track(*this); + // In non-shared-secondary mode, revive from local secondary bank + if (!alive() && !settings::use_shared_secondary_bank && + !local_secondary_bank().empty()) { + SourceSite& site = local_secondary_bank().back(); + event_revive_from_secondary(site); + local_secondary_bank().pop_back(); } } @@ -515,6 +550,7 @@ void Particle::event_death() // Finish particle track output. if (write_track()) { + write_particle_track(*this); finalize_particle_track(*this); } @@ -538,11 +574,17 @@ void Particle::event_death() score_pulse_height_tally(*this, model::active_pulse_height_tallies); } + // Accumulate track count for this particle history + if (!settings::use_shared_secondary_bank) { +#pragma omp atomic + simulation::simulation_tracks_completed += n_tracks(); + } + // Record the number of progeny created by this particle. // This data will be used to efficiently sort the fission bank. - if (settings::run_mode == RunMode::EIGENVALUE) { - int64_t offset = id() - 1 - simulation::work_index[mpi::rank]; - simulation::progeny_per_particle[offset] = n_progeny(); + if (settings::run_mode == RunMode::EIGENVALUE || + settings::use_shared_secondary_bank) { + simulation::progeny_per_particle[current_work()] = n_progeny(); } } @@ -863,28 +905,27 @@ void Particle::write_restart() const write_dataset(file_id, "id", id()); write_dataset(file_id, "type", type().pdg_number()); + // Get source site data for the particle that got lost int64_t i = current_work(); + SourceSite site; if (settings::run_mode == RunMode::EIGENVALUE) { - // take source data from primary bank for eigenvalue simulation - write_dataset(file_id, "weight", simulation::source_bank[i - 1].wgt); - write_dataset(file_id, "energy", simulation::source_bank[i - 1].E); - write_dataset(file_id, "xyz", simulation::source_bank[i - 1].r); - write_dataset(file_id, "uvw", simulation::source_bank[i - 1].u); - write_dataset(file_id, "time", simulation::source_bank[i - 1].time); + site = simulation::source_bank[i]; + } else if (settings::run_mode == RunMode::FIXED_SOURCE && + settings::use_shared_secondary_bank && + i < simulation::shared_secondary_bank_read.size()) { + site = simulation::shared_secondary_bank_read[i]; } else if (settings::run_mode == RunMode::FIXED_SOURCE) { - // re-sample using rng random number seed used to generate source particle - int64_t id = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - simulation::work_index[mpi::rank] + i; + // Re-sample using the same seed used to generate the source particle. + // current_work() is 0-indexed, compute_particle_id expects 1-indexed. + int64_t id = compute_transport_seed(compute_particle_id(i + 1)); uint64_t seed = init_seed(id, STREAM_SOURCE); - // re-sample source site - auto site = sample_external_source(&seed); - write_dataset(file_id, "weight", site.wgt); - write_dataset(file_id, "energy", site.E); - write_dataset(file_id, "xyz", site.r); - write_dataset(file_id, "uvw", site.u); - write_dataset(file_id, "time", site.time); + site = sample_external_source(&seed); } + write_dataset(file_id, "weight", site.wgt); + write_dataset(file_id, "energy", site.E); + write_dataset(file_id, "xyz", site.r); + write_dataset(file_id, "uvw", site.u); + write_dataset(file_id, "time", site.time); // Close file file_close(file_id); diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index f02fcb94b5..c226d51ec2 100644 --- a/src/particle_restart.cpp +++ b/src/particle_restart.cpp @@ -1,6 +1,7 @@ #include "openmc/particle_restart.h" #include "openmc/array.h" +#include "openmc/bank.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" #include "openmc/mgxs_interface.h" @@ -106,20 +107,16 @@ void run_particle_restart() // Set all tallies to 0 for now (just tracking errors) model::tallies.clear(); - // Compute random number seed - int64_t particle_seed; - switch (previous_run_mode) { - case RunMode::EIGENVALUE: - case RunMode::FIXED_SOURCE: - particle_seed = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - p.id(); - break; - default: - throw std::runtime_error { - "Unexpected run mode: " + - std::to_string(static_cast(previous_run_mode))}; + // Allocate progeny_per_particle if needed for shared secondary mode + // (event_death() writes to this array). Set current_work to 0 since we + // only have one particle being restarted. + if (settings::use_shared_secondary_bank) { + p.current_work() = 0; + simulation::progeny_per_particle.resize(1, 0); } + + // Compute random number seed + int64_t particle_seed = compute_transport_seed(p.id()); init_particle_seeds(particle_seed, p.seeds()); // Force calculation of cross-sections by setting last energy to zero diff --git a/src/physics.cpp b/src/physics.cpp index 106bd1aa2b..dcc7713292 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -44,7 +44,7 @@ void collision(Particle& p) { // Add to collision counter for particle ++(p.n_collision()); - p.secondary_bank_index() = p.secondary_bank().size(); + p.secondary_bank_index() = p.local_secondary_bank().size(); // Sample reaction for the material the particle is in switch (p.type().pdg_number()) { @@ -127,7 +127,8 @@ void sample_neutron_reaction(Particle& p) // Make sure particle population doesn't grow out of control for // subcritical multiplication problems. - if (p.secondary_bank().size() >= settings::max_secondaries) { + if (p.local_secondary_bank().size() >= settings::max_secondaries && + !settings::use_shared_secondary_bank) { fatal_error( "The secondary particle bank appears to be growing without " "bound. You are likely running a subcritical multiplication problem " @@ -228,7 +229,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } // Set parent and progeny IDs - site.parent_id = p.id(); + site.parent_id = p.current_work(); site.progeny_id = p.n_progeny()++; // Store fission site in bank @@ -253,7 +254,10 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) ifp(p, idx); } } else { - p.secondary_bank().push_back(site); + site.wgt_born = p.wgt_born(); + site.wgt_ww_born = p.wgt_ww_born(); + site.n_split = p.n_split(); + p.local_secondary_bank().push_back(site); p.n_secondaries()++; } @@ -1223,9 +1227,20 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // Create the secondary photon bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon()); + // Pre-add photon energy to pht_storage so pht_secondary_particles() + // subtraction results in net zero + if (created_photon && !model::active_pulse_height_tallies.empty()) { + auto it = std::find(model::pulse_height_cells.begin(), + model::pulse_height_cells.end(), p.lowest_coord().cell()); + if (it != model::pulse_height_cells.end()) { + int index = std::distance(model::pulse_height_cells.begin(), it); + p.pht_storage()[index] += E; + } + } + // Tag secondary particle with parent nuclide if (created_photon && settings::use_decay_photons) { - p.secondary_bank().back().parent_nuclide = + p.local_secondary_bank().back().parent_nuclide = rx->products_[i_product].parent_nuclide_; } } diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 6991c73655..212ba765ca 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -27,7 +27,7 @@ void collision_mg(Particle& p) { // Add to the collision counter for the particle p.n_collision()++; - p.secondary_bank_index() = p.secondary_bank().size(); + p.secondary_bank_index() = p.local_secondary_bank().size(); // Sample the reaction type sample_reaction(p); @@ -179,7 +179,7 @@ void create_fission_sites(Particle& p) } // Set parent and progeny ID - site.parent_id = p.id(); + site.parent_id = p.current_work(); site.progeny_id = p.n_progeny()++; // Store fission site in bank @@ -200,7 +200,10 @@ void create_fission_sites(Particle& p) break; } } else { - p.secondary_bank().push_back(site); + site.wgt_born = p.wgt_born(); + site.wgt_ww_born = p.wgt_ww_born(); + site.n_split = p.n_split(); + p.local_secondary_bank().push_back(site); p.n_secondaries()++; } diff --git a/src/plot.cpp b/src/plot.cpp index 707d53dc2c..d26589a794 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -33,6 +33,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/string_utils.h" +#include "openmc/tallies/filter.h" namespace openmc { @@ -44,10 +45,12 @@ constexpr int PLOT_LEVEL_LOWEST {-1}; //!< lower bound on plot universe level constexpr int32_t NOT_FOUND {-2}; constexpr int32_t OVERLAP {-3}; -IdData::IdData(size_t h_res, size_t v_res) : data_({v_res, h_res, 3}, NOT_FOUND) +IdData::IdData(size_t h_res, size_t v_res, bool /*include_filter*/) + : data_({v_res, h_res, 3}, NOT_FOUND) {} -void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) +void IdData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { // set cell data if (p.n_coord() <= level) { @@ -64,25 +67,24 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) Cell* c = model::cells.at(p.lowest_coord().cell()).get(); if (p.material() == MATERIAL_VOID) { data_(y, x, 2) = MATERIAL_VOID; - return; } else if (c->type_ == Fill::MATERIAL) { Material* m = model::materials.at(p.material()).get(); data_(y, x, 2) = m->id_; } } -void IdData::set_overlap(size_t y, size_t x) +void IdData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) { for (size_t k = 0; k < data_.shape(2); ++k) data_(y, x, k) = OVERLAP; } -PropertyData::PropertyData(size_t h_res, size_t v_res) +PropertyData::PropertyData(size_t h_res, size_t v_res, bool /*include_filter*/) : data_({v_res, h_res, 2}, NOT_FOUND) {} -void PropertyData::set_value( - size_t y, size_t x, const GeometryState& p, int level) +void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { Cell* c = model::cells.at(p.lowest_coord().cell()).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; @@ -92,11 +94,79 @@ void PropertyData::set_value( } } -void PropertyData::set_overlap(size_t y, size_t x) +void PropertyData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) { data_(y, x) = OVERLAP; } +//============================================================================== +// RasterData implementation +//============================================================================== + +RasterData::RasterData(size_t h_res, size_t v_res, bool include_filter) + : id_data_({v_res, h_res, include_filter ? 4u : 3u}, NOT_FOUND), + property_data_({v_res, h_res, 2}, static_cast(NOT_FOUND)), + include_filter_(include_filter) +{} + +void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter, FilterMatch* match) +{ + // set cell data + if (p.n_coord() <= level) { + id_data_(y, x, 0) = NOT_FOUND; + id_data_(y, x, 1) = NOT_FOUND; + } else { + id_data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_; + id_data_(y, x, 1) = level == p.n_coord() - 1 + ? p.cell_instance() + : cell_instance_at_level(p, level); + } + + // set material data + Cell* c = model::cells.at(p.lowest_coord().cell()).get(); + if (p.material() == MATERIAL_VOID) { + id_data_(y, x, 2) = MATERIAL_VOID; + } else if (c->type_ == Fill::MATERIAL) { + Material* m = model::materials.at(p.material()).get(); + id_data_(y, x, 2) = m->id_; + } + + // set filter index (only if filter is being used) + if (include_filter_ && filter) { + filter->get_all_bins(p, TallyEstimator::COLLISION, *match); + if (match->bins_.empty()) { + id_data_(y, x, 3) = -1; + } else { + id_data_(y, x, 3) = match->bins_[0]; + } + match->bins_.clear(); + match->weights_.clear(); + } + + // set temperature (in K) + property_data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; + + // set density (g/cm³) + if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { + Material* m = model::materials.at(p.material()).get(); + property_data_(y, x, 1) = m->density_gpcc_; + } +} + +void RasterData::set_overlap(size_t y, size_t x, int overlap_idx) +{ + // Set cell, instance, and material to OVERLAP, but preserve filter bin + id_data_(y, x, 0) = OVERLAP; + id_data_(y, x, 1) = OVERLAP; + id_data_(y, x, 2) = OVERLAP - overlap_idx - 1; + // Note: id_data_(y, x, 3) is NOT overwritten - preserves filter bin for tally + // plotting + + property_data_(y, x, 0) = OVERLAP; + property_data_(y, x, 1) = OVERLAP; +} + //============================================================================== // Global variables //============================================================================== @@ -450,6 +520,22 @@ void Plot::set_width(pugi::xml_node plot_node) if (pl_width.size() == 2) { width_.x = pl_width[0]; width_.y = pl_width[1]; + switch (basis_) { + case PlotBasis::xy: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, width_.y, 0.0}; + break; + case PlotBasis::xz: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + case PlotBasis::yz: + u_span_ = {0.0, width_.x, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + default: + UNREACHABLE(); + } } else { fatal_error( fmt::format(" must be length 2 in slice plot {}", id())); @@ -765,7 +851,7 @@ Plot::Plot(pugi::xml_node plot_node, PlotType type) set_width(plot_node); set_meshlines(plot_node); slice_level_ = level_; // Copy level employed in SlicePlotBase::get_map - slice_color_overlaps_ = color_overlaps_; + show_overlaps_ = color_overlaps_; } //============================================================================== @@ -862,23 +948,39 @@ void Plot::draw_mesh_lines(ImageData& data) const rgb = meshlines_color_; int ax1, ax2; + Position expected_u {}; + Position expected_v {}; switch (basis_) { case PlotBasis::xy: ax1 = 0; ax2 = 1; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, width_[1], 0.0}; break; case PlotBasis::xz: ax1 = 0; ax2 = 2; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; case PlotBasis::yz: ax1 = 1; ax2 = 2; + expected_u = {0.0, width_[0], 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; default: UNREACHABLE(); } + // Meshlines rely on axis-aligned indexing in global coordinates. + constexpr double rel_tol {1e-12}; + double span_tol = rel_tol * (1.0 + u_span_.norm() + v_span_.norm()); + if ((u_span_ - expected_u).norm() > span_tol || + (v_span_ - expected_v).norm() > span_tol) { + fatal_error("Meshlines are only supported for axis-aligned slice plots."); + } + Position ll_plot {origin_}; Position ur_plot {origin_}; @@ -1008,11 +1110,11 @@ void Plot::create_voxel() const voxel_init(file_id, &(dims[0]), &dspace, &dset, &memspace); SlicePlotBase pltbase; - pltbase.width_ = width_; pltbase.origin_ = origin_; - pltbase.basis_ = PlotBasis::xy; + pltbase.u_span_ = {width_.x, 0.0, 0.0}; + pltbase.v_span_ = {0.0, width_.y, 0.0}; pltbase.pixels() = pixels(); - pltbase.slice_color_overlaps_ = color_overlaps_; + pltbase.show_overlaps_ = color_overlaps_; ProgressBar pb; for (int z = 0; z < pixels()[2]; z++) { @@ -1794,6 +1896,12 @@ void PhongRay::on_intersection() extern "C" int openmc_id_map(const void* plot, int32_t* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_id_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { @@ -1801,7 +1909,7 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1815,14 +1923,20 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) extern "C" int openmc_property_map(const void* plot, double* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_property_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { - set_errmsg("Invalid slice pointer passed to openmc_id_map"); + set_errmsg("Invalid slice pointer passed to openmc_property_map"); return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1834,6 +1948,96 @@ extern "C" int openmc_property_map(const void* plot, double* data_out) return 0; } +extern "C" int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool color_overlaps, + int level, int32_t filter_index, int32_t* geom_data, double* property_data) +{ + // Validate span vectors + Direction u_span_pos {u_span[0], u_span[1], u_span[2]}; + Direction v_span_pos {v_span[0], v_span[1], v_span[2]}; + double u_norm = u_span_pos.norm(); + double v_norm = v_span_pos.norm(); + if (u_norm == 0.0 || v_norm == 0.0) { + set_errmsg("Slice span vectors must be non-zero."); + return OPENMC_E_INVALID_ARGUMENT; + } + + constexpr double ORTHO_REL_TOL = 1e-10; + double dot = u_span_pos.dot(v_span_pos); + if (std::abs(dot) > ORTHO_REL_TOL * u_norm * v_norm) { + set_errmsg("Slice span vectors must be orthogonal."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Validate filter index if provided + if (filter_index >= 0) { + if (int err = verify_filter(filter_index)) + return err; + } + + // Initialize overlap check vector if needed + if (color_overlaps && model::overlap_check_count.size() == 0) { + model::overlap_check_count.resize(model::cells.size()); + } + + try { + // Create a temporary SlicePlotBase object to reuse get_map logic + SlicePlotBase plot_params; + plot_params.origin_ = Position {origin[0], origin[1], origin[2]}; + plot_params.u_span_ = u_span_pos; + plot_params.v_span_ = v_span_pos; + plot_params.pixels_[0] = pixels[0]; + plot_params.pixels_[1] = pixels[1]; + plot_params.show_overlaps_ = color_overlaps; + plot_params.slice_level_ = level; + + // Clear overlap data structures on new slice call + model::overlap_keys.clear(); + model::overlap_key_index.clear(); + + // Use get_map to generate data + auto data = plot_params.get_map(filter_index); + std::copy(data.id_data_.begin(), data.id_data_.end(), geom_data); + + // Copy property data if requested + if (property_data != nullptr) { + std::copy( + data.property_data_.begin(), data.property_data_.end(), property_data); + } + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_UNASSIGNED; + } + + return 0; +} + +// Gets the number of overlaps that we need data for +extern "C" int openmc_slice_data_overlap_count(size_t* count) +{ + if (!count) { + set_errmsg("Null pointer passed for overlap count."); + return OPENMC_E_INVALID_ARGUMENT; + } + *count = model::overlap_keys.size(); + + return 0; +} + +// Plotter pre-allocates array size based on what is returned with +// overlap_count; populates an array of size 3*count +extern "C" int openmc_slice_data_overlap_info( + size_t count, int32_t* overlap_info) +{ + for (size_t i = 0; i < count; ++i) { + overlap_info[i * 3] = model::overlap_keys[i].universe_id; + overlap_info[i * 3 + 1] = model::overlap_keys[i].cell1_id; + overlap_info[i * 3 + 2] = model::overlap_keys[i].cell2_id; + } + + return 0; +} + extern "C" int openmc_get_plot_index(int32_t id, int32_t* index) { auto it = model::plot_map.find(id); diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 06c6ef14d7..83128fdaa0 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -30,7 +30,8 @@ namespace openmc { RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; -bool FlatSourceDomain::adjoint_ {false}; +bool FlatSourceDomain::adjoint_requested_ {false}; +RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD}; bool FlatSourceDomain::fw_cadis_local_ {false}; double FlatSourceDomain::diagonal_stabilization_rho_ {1.0}; std::unordered_map>> @@ -556,7 +557,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // If we are in adjoint mode of a fixed source problem, the external // source is already normalized, such that all resulting fluxes are // also normalized. - if (adjoint_) { + if (solve_ == RandomRaySolve::ADJOINT) { return 1.0; } @@ -724,7 +725,7 @@ void FlatSourceDomain::random_ray_tally() for (int i = 0; i < model::tallies.size(); i++) { Tally& tally {*model::tallies[i]}; #pragma omp parallel for - for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int64_t bin = 0; bin < tally.n_filter_bins(); bin++) { for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { auto score_type = tally.scores_[score_idx]; if (score_type == SCORE_FLUX) { @@ -795,6 +796,12 @@ void FlatSourceDomain::output_to_vtk() const double z_delta = width.z / Nz; std::string filename = openmc_plot->path_plot(); + // Tag plots written during the forward solve of an adjoint run + if (solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) { + auto dot = filename.find_last_of('.'); + filename = filename.substr(0, dot) + ".forward" + filename.substr(dot); + } + // Perform sanity checks on file size uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float); write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)", @@ -1002,9 +1009,10 @@ void FlatSourceDomain::output_to_vtk() const void FlatSourceDomain::apply_external_source_to_source_region( int src_idx, SourceRegionHandle& srh) { - auto s = (adjoint_ && !model::adjoint_sources.empty()) - ? model::adjoint_sources[src_idx].get() - : model::external_sources[src_idx].get(); + auto s = + (solve_ == RandomRaySolve::ADJOINT && !model::adjoint_sources.empty()) + ? model::adjoint_sources[src_idx].get() + : model::external_sources[src_idx].get(); auto is = dynamic_cast(s); auto discrete = dynamic_cast(is->energy()); double strength_factor = is->strength(); diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 24650afb8d..00fff99a7c 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -189,7 +189,7 @@ void validate_random_ray_inputs() // Validate adjoint sources /////////////////////////////////////////////////////////////////// - if (FlatSourceDomain::adjoint_ && !model::adjoint_sources.empty()) { + if (FlatSourceDomain::adjoint_requested_ && !model::adjoint_sources.empty()) { for (int i = 0; i < model::adjoint_sources.size(); i++) { Source* s = model::adjoint_sources[i].get(); @@ -289,7 +289,8 @@ void openmc_finalize_random_ray() { FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; FlatSourceDomain::volume_normalized_flux_tallies_ = false; - FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::adjoint_requested_ = false; + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; FlatSourceDomain::fw_cadis_local_ = false; FlatSourceDomain::fw_cadis_local_targets_.clear(); FlatSourceDomain::mesh_domain_map_.clear(); @@ -356,19 +357,17 @@ void RandomRaySimulation::prepare_local_fixed_sources_adjoint() } } -void RandomRaySimulation::prepare_adjoint_simulation(bool fw_adjoint) +void RandomRaySimulation::prepare_adjoint_simulation(bool from_forward) { reset_timers(); if (mpi::master) header("ADJOINT FLUX SOLVE", 3); - if (fw_adjoint) { - // Forward simulation has already been run; - // Configure the domain for adjoint simulation and - // re-initialize OpenMC general data structures - FlatSourceDomain::adjoint_ = true; - + if (from_forward) { + // The forward solve has already run. Re-initialize OpenMC's general data + // structures for the adjoint solve and derive the adjoint source from the + // forward flux. openmc_simulation_init(); prepare_fw_fixed_sources_adjoint(); @@ -603,7 +602,8 @@ void RandomRaySimulation::print_results_random_ray( } fmt::print(" Volume Estimator Type = {}\n", estimator); - std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF"; + std::string adjoint_true = + (FlatSourceDomain::solve_ == RandomRaySolve::ADJOINT) ? "ON" : "OFF"; fmt::print(" Adjoint Flux Mode = {}\n", adjoint_true); std::string shape; @@ -675,60 +675,49 @@ void RandomRaySimulation::print_results_random_ray( void openmc_run_random_ray() { - ////////////////////////////////////////////////////////// - // Run forward simulation - ////////////////////////////////////////////////////////// + using namespace openmc; - // Check if adjoint calculation is needed, and if local adjoint source(s) - // are present. If an adjoint calculation is needed and no sources are - // specified, we will run a forward calculation first to calculate adjoint - // sources for global variance reduction, then perform an adjoint - // calculation later. - bool adjoint_needed = openmc::FlatSourceDomain::adjoint_; - bool fw_adjoint = openmc::model::adjoint_sources.empty() && adjoint_needed; + // Determine which solves to run. If adjoint results are requested and no + // user-defined adjoint source is present, an initial forward solve is needed + // to construct the adjoint source from the forward flux (FW-CADIS). If the + // user has defined an adjoint source, the forward solve is skipped and only + // the adjoint solve is run. + const bool run_adjoint = FlatSourceDomain::adjoint_requested_; + const bool have_adjoint_source = !model::adjoint_sources.empty(); + const bool run_forward = !(run_adjoint && have_adjoint_source); - // If we're going to do an adjoint simulation with forward-weighted adjoint - // sources afterwards, report that this is the initial forward flux solve. - if (!adjoint_needed || fw_adjoint) { - // Configure the domain for forward simulation - openmc::FlatSourceDomain::adjoint_ = false; - - if (adjoint_needed && openmc::mpi::master) - openmc::header("FORWARD FLUX SOLVE", 3); + // Set the initial solve type + if (!run_forward) { + FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT; + } else if (run_adjoint) { + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD_FOR_ADJOINT; } else { - // Configure domain for adjoint simulation (later) - openmc::FlatSourceDomain::adjoint_ = true; + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; } // Initialize OpenMC general data structures openmc_simulation_init(); // Validate that inputs meet requirements for random ray mode - if (openmc::mpi::master) - openmc::validate_random_ray_inputs(); + if (mpi::master) + validate_random_ray_inputs(); // Initialize Random Ray Simulation Object - openmc::RandomRaySimulation sim; + RandomRaySimulation sim; - if (!adjoint_needed || fw_adjoint) { - // Initialize fixed sources, if present + // Run the forward solve + if (run_forward) { + // When an adjoint solve follows, report this as the initial forward solve + if (run_adjoint && mpi::master) + header("FORWARD FLUX SOLVE", 3); sim.apply_fixed_sources_and_mesh_domains(); - - // Execute random ray simulation sim.simulate(); } - ////////////////////////////////////////////////////////// - // Run adjoint simulation (if enabled) - ////////////////////////////////////////////////////////// - - if (!adjoint_needed) { - return; + // Run the adjoint solve + if (run_adjoint) { + FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT; + sim.prepare_adjoint_simulation(run_forward); + sim.simulate(); } - - // Setup for adjoint simulation - sim.prepare_adjoint_simulation(fw_adjoint); - - // Execute random ray simulation - sim.simulate(); } diff --git a/src/settings.cpp b/src/settings.cpp index bb6991da95..58bced9403 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -83,6 +83,7 @@ bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; bool use_decay_photons {false}; +bool use_shared_secondary_bank {false}; bool weight_windows_on {false}; bool weight_window_checkpoint_surface {false}; bool weight_window_checkpoint_collision {true}; @@ -321,7 +322,7 @@ void get_run_parameters(pugi::xml_node node_base) get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); } if (check_for_node(random_ray_node, "adjoint")) { - FlatSourceDomain::adjoint_ = + FlatSourceDomain::adjoint_requested_ = get_node_value_bool(random_ray_node, "adjoint"); } if (check_for_node(random_ray_node, "sample_method")) { @@ -1320,6 +1321,27 @@ void read_settings_xml(pugi::xml_node root) settings::use_decay_photons = get_node_value_bool(root, "use_decay_photons"); } + + // If weight windows are on, also enable shared secondary bank (unless + // explicitly disabled by user). + if (check_for_node(root, "shared_secondary_bank")) { + bool val = get_node_value_bool(root, "shared_secondary_bank"); + if (val && run_mode == RunMode::EIGENVALUE) { + warning( + "Shared secondary bank is not supported in eigenvalue calculations. " + "Setting will be ignored."); + } else { + settings::use_shared_secondary_bank = val; + } + } else if (settings::weight_windows_on) { + if (run_mode == RunMode::EIGENVALUE) { + warning( + "Shared secondary bank is not supported in eigenvalue calculations. " + "Particle local secondary banks will be used instead."); + } else if (run_mode == RunMode::FIXED_SOURCE) { + settings::use_shared_secondary_bank = true; + } + } } void free_memory_settings() diff --git a/src/simulation.cpp b/src/simulation.cpp index 4fad196a60..900a383ab3 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -16,6 +16,7 @@ #include "openmc/particle.h" #include "openmc/photon.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/settings.h" #include "openmc/source.h" #include "openmc/state_point.h" @@ -86,7 +87,7 @@ int openmc_simulation_init() } // Determine how much work each process should do - calculate_work(); + calculate_work(settings::n_particles); // Allocate source, fission and surface source banks. allocate_banks(); @@ -200,9 +201,12 @@ int openmc_simulation_finalize() if (settings::output_tallies && mpi::master) write_tallies(); - // If weight window generators are present in this simulation, - // write a weight windows file - if (variance_reduction::weight_windows_generators.size() > 0) { + // If weight window generators are present in this simulation, write a + // weight windows file. This is skipped during the forward solve of an + // adjoint (FW-CADIS) run, where only the adjoint-derived weight windows + // are meaningful. + if (variance_reduction::weight_windows_generators.size() > 0 && + FlatSourceDomain::solve_ != RandomRaySolve::FORWARD_FOR_ADJOINT) { openmc_weight_windows_export(); } @@ -214,6 +218,20 @@ int openmc_simulation_finalize() // Stop timers and show timing statistics simulation::time_finalize.stop(); simulation::time_total.stop(); + +#ifdef OPENMC_MPI + // Reduce track count across ranks for correct reporting. In shared secondary + // bank mode, all ranks already have the global count; in non-shared mode, + // each rank only has its own count. + if (settings::weight_windows_on && !settings::use_shared_secondary_bank) { + int64_t total_tracks; + MPI_Reduce(&simulation::simulation_tracks_completed, &total_tracks, 1, + MPI_INT64_T, MPI_SUM, 0, mpi::intracomm); + if (mpi::master) + simulation::simulation_tracks_completed = total_tracks; + } +#endif + if (mpi::master) { if (settings::solver_type != SolverType::RANDOM_RAY) { if (settings::verbosity >= 6) @@ -254,9 +272,17 @@ int openmc_next_batch(int* status) // Transport loop if (settings::event_based) { - transport_event_based(); + if (settings::use_shared_secondary_bank) { + transport_event_based_shared_secondary(); + } else { + transport_event_based(); + } } else { - transport_history_based(); + if (settings::use_shared_secondary_bank) { + transport_history_based_shared_secondary(); + } else { + transport_history_based(); + } } // Accumulate time for transport @@ -324,6 +350,8 @@ const RegularMesh* ufs_mesh {nullptr}; vector k_generation; vector work_index; +int64_t simulation_tracks_completed {0}; + } // namespace simulation //============================================================================== @@ -547,7 +575,7 @@ void finalize_generation() // If using shared memory, stable sort the fission bank (by parent IDs) // so as to allow for reproducibility regardless of which order particles // are run in. - sort_fission_bank(); + sort_bank(simulation::fission_bank, true); // Distribute fission bank across processors evenly synchronize_bank(); @@ -571,26 +599,35 @@ void finalize_generation() } } -void initialize_history(Particle& p, int64_t index_source) +void sample_source_particle(Particle& p, int64_t index_source) { - // set defaults + // Sample a particle from the source bank if (settings::run_mode == RunMode::EIGENVALUE) { - // set defaults for eigenvalue simulations from primary bank p.from_source(&simulation::source_bank[index_source - 1]); } else if (settings::run_mode == RunMode::FIXED_SOURCE) { // initialize random number seed - int64_t id = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - simulation::work_index[mpi::rank] + index_source; + int64_t id = compute_transport_seed(compute_particle_id(index_source)); uint64_t seed = init_seed(id, STREAM_SOURCE); // sample from external source distribution or custom library then set auto site = sample_external_source(&seed); p.from_source(&site); } - p.current_work() = index_source; +} + +void initialize_particle_track( + Particle& p, int64_t index_source, bool is_secondary) +{ + // Note: index_source is 1-based (first particle = 1), but current_work() is + // stored as 0-based for direct use as an array index into + // progeny_per_particle, source_bank, ifp banks, etc. + if (!is_secondary) { + sample_source_particle(p, index_source); + } + + p.current_work() = index_source - 1; // set identifier for particle - p.id() = simulation::work_index[mpi::rank] + index_source; + p.id() = compute_particle_id(index_source); // set progeny count to zero p.n_progeny() = 0; @@ -598,6 +635,9 @@ void initialize_history(Particle& p, int64_t index_source) // Reset particle event counter p.n_event() = 0; + // Initialize track counter (1 for this primary/secondary track) + p.n_tracks() = 1; + // Reset split counter p.n_split() = 0; @@ -611,9 +651,7 @@ void initialize_history(Particle& p, int64_t index_source) std::fill(p.pht_storage().begin(), p.pht_storage().end(), 0); // set random number seed - int64_t particle_seed = - (simulation::total_gen + overall_generation() - 1) * settings::n_particles + - p.id(); + int64_t particle_seed = compute_transport_seed(p.id()); init_particle_seeds(particle_seed, p.seeds()); // set particle trace @@ -627,17 +665,21 @@ void initialize_history(Particle& p, int64_t index_source) p.write_track() = check_track_criteria(p); // Set the particle's initial weight window value. - p.wgt_ww_born() = -1.0; - apply_weight_windows(p); + if (!is_secondary) { + p.wgt_ww_born() = -1.0; + apply_weight_windows(p); + } // Display message if high verbosity or trace is on if (settings::verbosity >= 9 || p.trace()) { write_message("Simulating Particle {}", p.id()); } -// Add particle's starting weight to count for normalizing tallies later + // Add particle's starting weight to count for normalizing tallies later + if (!is_secondary) { #pragma omp atomic - simulation::total_weight += p.wgt(); + simulation::total_weight += p.wgt(); + } // Force calculation of cross-sections by setting last energy to zero if (settings::run_CE) { @@ -655,13 +697,34 @@ int overall_generation() return settings::gen_per_batch * (current_batch - 1) + current_gen; } -void calculate_work() +int64_t compute_particle_id(int64_t index_source) +{ + if (settings::use_shared_secondary_bank) { + return simulation::work_index[mpi::rank] + index_source + + simulation::simulation_tracks_completed; + } else { + return simulation::work_index[mpi::rank] + index_source; + } +} + +int64_t compute_transport_seed(int64_t particle_id) +{ + if (settings::use_shared_secondary_bank) { + return particle_id; + } else { + return (simulation::total_gen + overall_generation() - 1) * + settings::n_particles + + particle_id; + } +} + +void calculate_work(int64_t n_particles) { // Determine minimum amount of particles to simulate on each processor - int64_t min_work = settings::n_particles / mpi::n_procs; + int64_t min_work = n_particles / mpi::n_procs; // Determine number of processors that have one extra particle - int64_t remainder = settings::n_particles % mpi::n_procs; + int64_t remainder = n_particles % mpi::n_procs; int64_t i_bank = 0; simulation::work_index.resize(mpi::n_procs + 1); @@ -816,7 +879,7 @@ void transport_history_based_single_particle(Particle& p) p.event_collide(); } } - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); } p.event_death(); } @@ -826,11 +889,137 @@ void transport_history_based() #pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; - initialize_history(p, i_work); + initialize_particle_track(p, i_work, false); transport_history_based_single_particle(p); } } +// The shared secondary bank transport algorithm works in two phases. In the +// first phase, all primary particles are sampled then transported, and their +// secondary particles are deposited into a shared secondary bank. The second +// phase occurs in a loop, where all secondary tracks in the shared secondary +// bank are transported. Any secondary particles generated during this phase are +// deposited back into the shared secondary bank. The shared secondary bank is +// sorted for consistent ordering and load balanced across MPI ranks. This loop +// continues until there are no more secondary tracks left to transport. +void transport_history_based_shared_secondary() +{ + // Clear shared secondary banks from any prior use + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); + + if (mpi::master) { + write_message(fmt::format(" Primary source particles: {}", + settings::n_particles), + 6); + } + + simulation::progeny_per_particle.resize(simulation::work_per_rank); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Phase 1: Transport primary particles and deposit first generation of + // secondaries in the shared secondary bank +#pragma omp parallel + { + vector thread_bank; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::work_per_rank; i++) { + Particle p; + initialize_particle_track(p, i, false); + transport_history_based_single_particle(p); + for (auto& site : p.local_secondary_bank()) { + thread_bank.push_back(site); + } + } + + // Drain thread-local bank into the shared secondary bank (once per thread) +#pragma omp critical(SharedSecondaryBank) + { + for (auto& site : thread_bank) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + } + } + + simulation::simulation_tracks_completed += settings::n_particles; + + // Phase 2: Now that the secondary bank has been populated, enter loop over + // all secondary generations + int n_generation_depth = 1; + int64_t alive_secondary = 1; + while (alive_secondary) { + + // Sort the shared secondary bank by parent ID then progeny ID to + // ensure reproducibility. + sort_bank(simulation::shared_secondary_bank_write, false); + + // Synchronize the shared secondary bank amongst all MPI ranks, such + // that each MPI rank has an approximately equal number of secondary + // tracks. Also reports the total number of secondaries alive across + // all MPI ranks. + alive_secondary = synchronize_global_secondary_bank( + simulation::shared_secondary_bank_write); + + // Recalculate work for each MPI rank based on number of alive secondary + // tracks + calculate_work(alive_secondary); + + // Display the number of secondary tracks in this generation. This + // is useful for user monitoring so as to see if the secondary population is + // exploding and to determine how many generations of secondaries are being + // transported. + if (mpi::master) { + write_message(fmt::format(" Secondary generation {:<2} tracks: {}", + n_generation_depth, alive_secondary), + 6); + } + + simulation::shared_secondary_bank_read = + std::move(simulation::shared_secondary_bank_write); + simulation::shared_secondary_bank_write = SharedArray(); + simulation::progeny_per_particle.resize( + simulation::shared_secondary_bank_read.size()); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Transport all secondary tracks from the shared secondary bank +#pragma omp parallel + { + vector thread_bank; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::shared_secondary_bank_read.size(); + i++) { + Particle p; + initialize_particle_track(p, i, true); + SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; + p.event_revive_from_secondary(site); + transport_history_based_single_particle(p); + for (auto& secondary_site : p.local_secondary_bank()) { + thread_bank.push_back(secondary_site); + } + } + + // Drain thread-local bank into the shared secondary bank (once per + // thread) +#pragma omp critical(SharedSecondaryBank) + { + for (auto& secondary_site : thread_bank) { + simulation::shared_secondary_bank_write.thread_unsafe_append( + secondary_site); + } + } + } // End of transport loop over tracks in shared secondary bank + n_generation_depth++; + simulation::simulation_tracks_completed += alive_secondary; + } // End of loop over secondary generations + + // Reset work so that fission bank etc works correctly + calculate_work(settings::n_particles); +} + void transport_event_based() { int64_t remaining_work = simulation::work_per_rank; @@ -848,33 +1037,7 @@ void transport_event_based() // Initialize all particle histories for this subiteration process_init_events(n_particles, source_offset); - - // Event-based transport loop - while (true) { - // Determine which event kernel has the longest queue - int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), - simulation::calculate_nonfuel_xs_queue.size(), - simulation::advance_particle_queue.size(), - simulation::surface_crossing_queue.size(), - simulation::collision_queue.size()}); - - // Execute event with the longest queue - if (max == 0) { - break; - } else if (max == simulation::calculate_fuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_fuel_xs_queue); - } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); - } else if (max == simulation::advance_particle_queue.size()) { - process_advance_particle_events(); - } else if (max == simulation::surface_crossing_queue.size()) { - process_surface_crossing_events(); - } else if (max == simulation::collision_queue.size()) { - process_collision_events(); - } - } - - // Execute death event for all particles + process_transport_events(); process_death_events(n_particles); // Adjust remaining work and source offset variables @@ -883,4 +1046,122 @@ void transport_event_based() } } +void transport_event_based_shared_secondary() +{ + // Clear shared secondary banks from any prior use + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); + + if (mpi::master) { + write_message(fmt::format(" Primary source particles: {}", + settings::n_particles), + 6); + } + + simulation::progeny_per_particle.resize(simulation::work_per_rank); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Phase 1: Transport primary particles using event-based processing and + // deposit first generation of secondaries in the shared secondary bank + int64_t remaining_work = simulation::work_per_rank; + int64_t source_offset = 0; + + while (remaining_work > 0) { + int64_t n_particles = + std::min(remaining_work, settings::max_particles_in_flight); + + process_init_events(n_particles, source_offset); + process_transport_events(); + process_death_events(n_particles); + + // Collect secondaries from all particle buffers into shared bank + for (int64_t i = 0; i < n_particles; i++) { + for (auto& site : simulation::particles[i].local_secondary_bank()) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + simulation::particles[i].local_secondary_bank().clear(); + } + + remaining_work -= n_particles; + source_offset += n_particles; + } + + simulation::simulation_tracks_completed += settings::n_particles; + + // Phase 2: Now that the secondary bank has been populated, enter loop over + // all secondary generations + int n_generation_depth = 1; + int64_t alive_secondary = 1; + while (alive_secondary) { + + // Sort the shared secondary bank by parent ID then progeny ID to + // ensure reproducibility. + sort_bank(simulation::shared_secondary_bank_write, false); + + // Synchronize the shared secondary bank amongst all MPI ranks, such + // that each MPI rank has an approximately equal number of secondary + // tracks. + alive_secondary = synchronize_global_secondary_bank( + simulation::shared_secondary_bank_write); + + // Recalculate work for each MPI rank based on number of alive secondary + // tracks + calculate_work(alive_secondary); + + if (mpi::master) { + write_message(fmt::format(" Secondary generation {:<2} tracks: {}", + n_generation_depth, alive_secondary), + 6); + } + + simulation::shared_secondary_bank_read = + std::move(simulation::shared_secondary_bank_write); + simulation::shared_secondary_bank_write = SharedArray(); + simulation::progeny_per_particle.resize( + simulation::shared_secondary_bank_read.size()); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Ensure particle buffer is large enough for this secondary generation + int64_t sec_buffer_length = std::min( + static_cast(simulation::shared_secondary_bank_read.size()), + settings::max_particles_in_flight); + if (sec_buffer_length > + static_cast(simulation::particles.size())) { + init_event_queues(sec_buffer_length); + } + + // Transport secondary tracks using event-based processing + int64_t sec_remaining = simulation::shared_secondary_bank_read.size(); + int64_t sec_offset = 0; + + while (sec_remaining > 0) { + int64_t n_particles = + std::min(sec_remaining, settings::max_particles_in_flight); + + process_init_secondary_events( + n_particles, sec_offset, simulation::shared_secondary_bank_read); + process_transport_events(); + process_death_events(n_particles); + + // Collect secondaries from all particle buffers into shared bank + for (int64_t i = 0; i < n_particles; i++) { + for (auto& site : simulation::particles[i].local_secondary_bank()) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + simulation::particles[i].local_secondary_bank().clear(); + } + + sec_remaining -= n_particles; + sec_offset += n_particles; + } // End of subiteration loop over secondary tracks + n_generation_depth++; + simulation::simulation_tracks_completed += alive_secondary; + } // End of loop over secondary generations + + // Reset work so that fission bank etc works correctly + calculate_work(settings::n_particles); +} + } // namespace openmc diff --git a/src/source.cpp b/src/source.cpp index bee20d5c30..1e783b623a 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -523,9 +523,16 @@ void FileSource::load_sites_from_file(const std::string& path) file_close(file_id); } - // Make sure particles in source file have valid types + // Make sure particles in source file have valid types. If any particle is a + // photon, electron, or positron, enable photon transport so that the + // appropriate cross sections are loaded. for (const auto& site : this->sites_) { validate_particle_type(site.particle, "FileSource"); + if (site.particle == ParticleType::photon() || + site.particle == ParticleType::electron() || + site.particle == ParticleType::positron()) { + settings::photon_transport = true; + } } } diff --git a/src/state_point.cpp b/src/state_point.cpp index da1c141a23..02c68bf600 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -22,6 +22,7 @@ #include "openmc/nuclide.h" #include "openmc/output.h" #include "openmc/particle_type.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/tallies/derivative.h" @@ -46,9 +47,15 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // Determine width for zero padding int w = std::to_string(settings::n_max_batches).size(); + // Tag statepoints written during the forward solve of an adjoint run + const char* forward = + (FlatSourceDomain::solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) + ? "forward." + : ""; + // Set filename for state point - filename_ = fmt::format("{0}statepoint.{1:0{2}}.h5", settings::path_output, - simulation::current_batch, w); + filename_ = fmt::format("{0}statepoint.{3}{1:0{2}}.h5", + settings::path_output, simulation::current_batch, w, forward); } // If a file name was specified, ensure it has .h5 file extension diff --git a/src/tallies/filter_particle_production.cpp b/src/tallies/filter_particle_production.cpp index f8d82fdd92..899809679a 100644 --- a/src/tallies/filter_particle_production.cpp +++ b/src/tallies/filter_particle_production.cpp @@ -19,7 +19,7 @@ void ParticleProductionFilter::get_all_bins( // Loop over secondary bank entries for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) { - const auto& site = p.secondary_bank(bank_idx); + const auto& site = p.local_secondary_bank(bank_idx); // Find which particle-type slot this secondary belongs to auto it = type_to_index_.find(site.particle.pdg_number()); diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 3fe48c1b02..0856332476 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -512,7 +512,7 @@ void Tally::set_strides() // longest stride. auto n = filters_.size(); strides_.resize(n, 0); - int stride = 1; + int64_t stride = 1; for (int i = n - 1; i >= 0; --i) { strides_[i] = stride; stride *= model::tally_filters[filters_[i]]->n_bins(); @@ -644,8 +644,24 @@ void Tally::set_scores(const vector& scores) break; case HEATING: - if (settings::photon_transport) - estimator_ = TallyEstimator::COLLISION; + if (settings::photon_transport) { + // Photon heating requires a collision estimator (analog energy + // balance). However, if the tally only scores neutrons, we can keep the + // tracklength estimator since neutron heating uses kerma coefficients + // that support tracklength scoring. + bool neutron_only = false; + for (auto i_filt : filters_) { + auto pf = + dynamic_cast(model::tally_filters[i_filt].get()); + if (pf && pf->particles().size() == 1 && + pf->particles()[0].is_neutron()) { + neutron_only = true; + break; + } + } + if (!neutron_only) + estimator_ = TallyEstimator::COLLISION; + } break; case SCORE_PULSE_HEIGHT: { @@ -871,7 +887,7 @@ void Tally::accumulate() if (higher_moments_) { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape(0); ++i) { + for (int64_t i = 0; i < results_.shape(0); ++i) { // score bins (flux, total reaction rate, fission reaction rate, etc.) for (int j = 0; j < results_.shape(1); ++j) { double val = results_(i, j, TallyResult::VALUE) * norm; @@ -886,7 +902,7 @@ void Tally::accumulate() } else { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape(0); ++i) { + for (int64_t i = 0; i < results_.shape(0); ++i) { // score bins (flux, total reaction rate, fission reaction rate, etc.) for (int j = 0; j < results_.shape(1); ++j) { double val = results_(i, j, TallyResult::VALUE) * norm; diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 5dcd07331d..7bce6ec137 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -158,7 +158,7 @@ void score_fission_delayed_dg(int i_tally, int d_bin, double score, dg_match.bins_[i_bin] = d_bin; // Determine the filter scoring index - auto filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto i = 0; i < tally.filters().size(); ++i) { auto i_filt = tally.filters(i); @@ -449,7 +449,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) (score_bin == SCORE_PROMPT_NU_FISSION && g == 0)) { // Find the filter scoring index for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.0; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -497,7 +497,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) } else { // Find the filter index and weight for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -578,8 +578,8 @@ double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin) //! collision estimator. void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -945,7 +945,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (p.type().is_neutron() && p.fission()) { if (is_generation_time_or_both()) { const auto& lifetimes = - simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_source_lifetime_bank[p.current_work()]; if (lifetimes.size() == settings::ifp_n_generation) { score = lifetimes[0] * p.wgt_last(); } @@ -959,7 +959,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (p.type().is_neutron() && p.fission()) { if (is_beta_effective_or_both()) { const auto& delayed_groups = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_source_delayed_group_bank[p.current_work()]; if (delayed_groups.size() == settings::ifp_n_generation) { if (delayed_groups[0] > 0) { score = p.wgt_last(); @@ -985,12 +985,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, int ifp_data_size; if (is_beta_effective_or_both()) { ifp_data_size = static_cast( - simulation::ifp_source_delayed_group_bank[p.current_work() - 1] + simulation::ifp_source_delayed_group_bank[p.current_work()] .size()); } else { ifp_data_size = static_cast( - simulation::ifp_source_lifetime_bank[p.current_work() - 1] - .size()); + simulation::ifp_source_lifetime_bank[p.current_work()].size()); } if (ifp_data_size == settings::ifp_n_generation) { score = p.wgt_last(); @@ -1113,8 +1112,8 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, //! is not used for analog tallies. void score_general_ce_analog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1616,8 +1615,8 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, //! argument is really just used for filter weights. void score_general_mg(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { auto& tally {*model::tallies[i_tally]}; diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index 6c54edd5e1..7515f01a9f 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -28,7 +28,7 @@ KTrigger keff_trigger; //============================================================================== std::pair get_tally_uncertainty( - int i_tally, int score_index, int filter_index) + int i_tally, int score_index, int64_t filter_index) { const auto& tally {model::tallies[i_tally]}; @@ -71,7 +71,7 @@ void check_tally_triggers(double& ratio, int& tally_id, int& score) continue; const auto& results = t.results_; - for (auto filter_index = 0; filter_index < results.shape(0); + for (int64_t filter_index = 0; filter_index < results.shape(0); ++filter_index) { // Compute the tally uncertainty metrics. auto uncert_pair = diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 0614110cd3..0d268ce175 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -791,7 +791,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (method_string == "magic") { method_ = WeightWindowUpdateMethod::MAGIC; if (settings::solver_type == SolverType::RANDOM_RAY && - FlatSourceDomain::adjoint_) { + FlatSourceDomain::adjoint_requested_) { fatal_error("Random ray weight window generation with MAGIC cannot be " "done in adjoint mode."); } @@ -800,7 +800,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (settings::solver_type != SolverType::RANDOM_RAY) { fatal_error("FW-CADIS can only be run in random ray solver mode."); } - FlatSourceDomain::adjoint_ = true; + FlatSourceDomain::adjoint_requested_ = true; if (check_for_node(node, "targets")) { FlatSourceDomain::fw_cadis_local_ = true; targets_ = get_node_array(node, "targets"); @@ -837,7 +837,8 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) ratio_)); if (ratio_ <= 1.0) fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " - "specified for weight window generation")); + "specified for weight window generation", + ratio_)); // create a matching weight windows object auto wws = WeightWindows::create(); diff --git a/tests/cpp_unit_tests/test_math.cpp b/tests/cpp_unit_tests/test_math.cpp index 1ad7c4b709..983322e4b9 100644 --- a/tests/cpp_unit_tests/test_math.cpp +++ b/tests/cpp_unit_tests/test_math.cpp @@ -355,3 +355,24 @@ TEST_CASE("Test broaden_wmp_polynomials") REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); } } + +TEST_CASE("Test isclose") +{ + using openmc::isclose; + + // Identical values are always close, regardless of tolerances. + REQUIRE(isclose(1.0, 1.0, 0.0, 0.0)); + REQUIRE(isclose(0.0, 0.0, 0.0, 0.0)); + + // Absolute tolerance governs comparisons near zero. + REQUIRE(isclose(0.0, 1e-15, 0.0, 1e-14)); + REQUIRE_FALSE(isclose(0.0, 1e-13, 0.0, 1e-14)); + + // Relative tolerance scales with the magnitude of the operands. + REQUIRE(isclose(1.0e12, 1.0e12 + 1.0, 1e-12, 0.0)); + REQUIRE_FALSE(isclose(1.0e12, 1.0e12 + 10.0, 1e-12, 0.0)); + + // The looser of the two tolerances wins. + REQUIRE(isclose(1.0, 1.0 + 1e-13, 0.0, 1e-12)); + REQUIRE(isclose(1.0e6, 1.0e6 + 1e-4, 1e-9, 0.0)); +} diff --git a/tests/cpp_unit_tests/test_tally.cpp b/tests/cpp_unit_tests/test_tally.cpp index 964d30cc42..03322e7646 100644 --- a/tests/cpp_unit_tests/test_tally.cpp +++ b/tests/cpp_unit_tests/test_tally.cpp @@ -1,3 +1,4 @@ +#include "openmc/tallies/filter_energy.h" #include "openmc/tallies/tally.h" #include @@ -46,10 +47,34 @@ TEST_CASE("Test add/set_filter") REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); REQUIRE(model::filter_map[particle_filter->id()] == tally->filters(1)); - // set filters with a duplicate filter, should only add the filter to the tally once + // set filters with a duplicate filter, should only add the filter to the + // tally once filters = {cell_filter, cell_filter}; tally->set_filters(filters); REQUIRE(tally->filters().size() == 1); REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); +} +// Regression test for 64-bit tally filter-bin counts (mesh x groups > 2^31). +TEST_CASE("Tally filter-bin count does not overflow 32 bits") +{ + // Two energy filters whose bin counts multiply to 2.5e9, above INT32_MAX. + constexpr int64_t bins_per_filter = 50000; + + // Only the bin count matters here, so the edge values are an arbitrary ramp. + std::vector edges(bins_per_filter + 1); + for (int64_t i = 0; i < bins_per_filter + 1; ++i) + edges[i] = static_cast(i); + + Tally* tally = Tally::create(); + for (int i = 0; i < 2; ++i) { + Filter* filter = Filter::create("energy"); + dynamic_cast(filter)->set_bins(edges); + tally->add_filter(filter); + } + tally->set_strides(); + + // set_strides() previously accumulated this product in a 32-bit int. + REQUIRE(tally->n_filter_bins() == bins_per_filter * bins_per_filter); + REQUIRE(tally->n_filter_bins() > 2147483647); } \ No newline at end of file diff --git a/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 b/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 new file mode 100644 index 0000000000..c315462134 Binary files /dev/null and b/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat index 7533616c05..005a560205 100644 --- a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ (n,fission) 101 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 new file mode 100644 index 0000000000..850bba8415 Binary files /dev/null and b/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat index 55fb835de0..8d46c3d3b9 100644 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 22 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 new file mode 100644 index 0000000000..777d91f088 Binary files /dev/null and b/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat index 61890414ba..322e74f493 100644 --- a/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 1 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 new file mode 100644 index 0000000000..527705297f Binary files /dev/null and b/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat index 8960dde5cb..6202bcacc7 100644 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ O16 U235 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 new file mode 100644 index 0000000000..65419d5ead Binary files /dev/null and b/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat index 8c0d7aa8ee..c06061210c 100644 --- a/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -52,7 +52,7 @@ 22 77 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 new file mode 100644 index 0000000000..507665a60f Binary files /dev/null and b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat index 5173dc35cf..434f6b434b 100644 --- a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 550000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 new file mode 100644 index 0000000000..b7ff67c48e Binary files /dev/null and b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat index 005d9feb27..1449c3596f 100644 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -56,7 +56,7 @@ 1 11 U238 U235 H1 U234 100000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat b/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat deleted file mode 100644 index 514932c1a6..0000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eigenvalue - 100 - 5 - 1 - - - -2.0 -2.0 -2.0 2.0 2.0 2.0 - - - true - - - - 200 - - 1 - - diff --git a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat b/tests/regression_tests/collision_track/case_8_2threads/results_true.dat deleted file mode 100644 index d4d1d1e5ad..0000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/test.py b/tests/regression_tests/collision_track/test.py index 00e1e3de41..2f9de9ac00 100644 --- a/tests/regression_tests/collision_track/test.py +++ b/tests/regression_tests/collision_track/test.py @@ -59,28 +59,13 @@ TODO: """ -import os - import openmc -import openmc.lib import pytest from tests.testing_harness import CollisionTrackTestHarness from tests.regression_tests import config -@pytest.fixture(scope="function") -def two_threads(monkeypatch): - """Set the number of OMP threads to 2 for the test.""" - monkeypatch.setenv("OMP_NUM_THREADS", "2") - - -@pytest.fixture(scope="function") -def single_process(monkeypatch): - """Set the number of MPI process to 1 for the test.""" - monkeypatch.setitem(config, "mpi_np", "1") - - @pytest.fixture(scope="module") def model_1(): """Cylindrical core contained in a first box which is contained in a larger box. @@ -181,9 +166,9 @@ def model_1(): # ============================================================================= model.settings = openmc.Settings() - model.settings.particles = 100 + model.settings.particles = 80 model.settings.batches = 5 - model.settings.inactive = 1 + model.settings.inactive = 4 model.settings.seed = 1 bounds = [ @@ -203,19 +188,19 @@ def model_1(): @pytest.mark.parametrize( "folder, model_name, parameter", - [("case_1_Reactions", "model_1", {"max_collisions": 300, "reactions": ["(n,fission)", 101]}), + [("case_1_Reactions", "model_1", {"max_collisions": 100, "reactions": ["(n,fission)", 101]}), ("case_2_Cell_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22]}), + "max_collisions": 100, "cell_ids": [22]}), ("case_3_Material_ID", "model_1", { - "max_collisions": 300, "material_ids": [1]}), + "max_collisions": 100, "material_ids": [1]}), ("case_4_Nuclide_ID", "model_1", { - "max_collisions": 300, "nuclides": ["O16", "U235"]}), + "max_collisions": 100, "nuclides": ["O16", "U235"]}), ("case_5_Universe_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22], "universe_ids": [77]}), + "max_collisions": 100, "cell_ids": [22], "universe_ids": [77]}), ("case_6_deposited_energy_threshold", "model_1", { - "max_collisions": 300, "deposited_E_threshold": 5.5e5}), + "max_collisions": 100, "deposited_E_threshold": 5.5e5}), ("case_7_all_parameters_used_together", "model_1", { - "max_collisions": 300, + "max_collisions": 100, "reactions": ["elastic", 18, "(n,disappear)"], "material_ids": [1, 11], "universe_ids": [77], @@ -235,21 +220,3 @@ def test_collision_track_several_cases( "statepoint.5.h5", model=model, workdir=folder ) harness.main() - - -@pytest.mark.skipif(config["event"], reason="Results from history-based mode.") -def test_collision_track_2threads(model_1, two_threads, single_process): - # This test checks that the `max_collisions` setting is honored: - # no collisions beyond the specified limit should be recorded. - # - # For the result to be reproducible, the number of threads and - # the transport mode (history vs. event) must remain fixed. - assert os.environ["OMP_NUM_THREADS"] == "2" - assert config["mpi_np"] == "1" - model_1.settings.collision_track = { - "max_collisions": 200 - } - harness = CollisionTrackTestHarness( - "statepoint.5.h5", model=model_1, workdir="case_8_2threads" - ) - harness.main() diff --git a/tests/regression_tests/lattice_large_pitch/__init__.py b/tests/regression_tests/lattice_large_pitch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/lattice_large_pitch/inputs_true.dat b/tests/regression_tests/lattice_large_pitch/inputs_true.dat new file mode 100644 index 0000000000..4dd4f0bd7b --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/inputs_true.dat @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + 100.0 100.0 + 2 + 3 3 + -150.0 -150.0 + +1 2 1 +2 1 2 +1 2 1 + + + + + + + + fixed source + 1000 + 10 + + + + 6 6 + -150.0 -150.0 + 150.0 150.0 + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/lattice_large_pitch/results_true.dat b/tests/regression_tests/lattice_large_pitch/results_true.dat new file mode 100644 index 0000000000..58c4e306dc --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/results_true.dat @@ -0,0 +1,73 @@ +tally 1: +1.749265E+01 +3.275925E+01 +4.159430E+01 +1.799568E+02 +7.027454E+01 +4.967568E+02 +6.789716E+01 +4.652258E+02 +4.272935E+01 +1.861129E+02 +2.073342E+01 +4.374279E+01 +4.028297E+01 +1.641642E+02 +6.734263E+01 +4.627171E+02 +1.037657E+02 +1.078682E+03 +1.108394E+02 +1.240927E+03 +7.236770E+01 +5.302034E+02 +4.356361E+01 +1.921990E+02 +6.731536E+01 +4.615977E+02 +9.850684E+01 +9.810272E+02 +5.164863E+02 +2.670336E+04 +5.097770E+02 +2.604770E+04 +1.064847E+02 +1.137536E+03 +7.052986E+01 +5.003220E+02 +7.065046E+01 +5.036723E+02 +1.044890E+02 +1.103250E+03 +5.121544E+02 +2.632389E+04 +5.065331E+02 +2.570752E+04 +1.044794E+02 +1.101249E+03 +6.569142E+01 +4.361120E+02 +4.739812E+01 +2.313289E+02 +7.402284E+01 +5.591689E+02 +1.066905E+02 +1.149521E+03 +1.038665E+02 +1.086813E+03 +7.160008E+01 +5.217744E+02 +4.219705E+01 +1.816100E+02 +2.089838E+01 +4.464899E+01 +4.154271E+01 +1.745866E+02 +7.081253E+01 +5.049997E+02 +6.673273E+01 +4.509038E+02 +4.184624E+01 +1.771546E+02 +1.853898E+01 +3.538608E+01 diff --git a/tests/regression_tests/lattice_large_pitch/test.py b/tests/regression_tests/lattice_large_pitch/test.py new file mode 100644 index 0000000000..0f409badde --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/test.py @@ -0,0 +1,70 @@ +"""Regression test for rectangular lattices with large pitch values. + +Large pitches used to trigger a segmentation fault in ``RectLattice::distance`` +because the boundary-crossing check compared a reconstructed crossing position +against an absolute tolerance that did not scale with the geometry (see #3852). +This test transports particles across a lattice with a large pitch to ensure the +crossing logic remains robust. + +""" + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + + # Large pitch that previously caused floating-point cancellation + pitch = 100.0 + n = 3 + + air = openmc.Material() + air.set_density('g/cm3', 0.001) + air.add_nuclide('N14', 1.0) + metal = openmc.Material() + metal.set_density('g/cm3', 7.0) + metal.add_nuclide('Fe56', 1.0) + + metal_cell = openmc.Cell(fill=metal) + metal_uni = openmc.Universe(cells=[metal_cell]) + air_cell = openmc.Cell(fill=air) + air_uni = openmc.Universe(cells=[air_cell]) + + lattice = openmc.RectLattice() + lattice.lower_left = (-pitch*n/2, -pitch*n/2) + lattice.pitch = (pitch, pitch) + lattice.outer = air_uni + lattice.universes = [ + [metal_uni, air_uni, metal_uni], + [air_uni, metal_uni, air_uni], + [metal_uni, air_uni, metal_uni], + ] + + box = openmc.model.RectangularPrism(pitch*n, pitch*n, boundary_type='vacuum') + root_cell = openmc.Cell(region=-box, fill=lattice) + model.geometry = openmc.Geometry([root_cell]) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 10 + model.settings.particles = 1000 + + mesh = openmc.RegularMesh() + mesh.dimension = (6, 6) + mesh.lower_left = (-pitch*n/2, -pitch*n/2) + mesh.upper_right = (pitch*n/2, pitch*n/2) + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally(tally_id=1) + tally.filters = [mesh_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + return model + + +def test_lattice_large_pitch(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat new file mode 100644 index 0000000000..318fdc7b90 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat @@ -0,0 +1,65 @@ + + + + 2g.h5 + + + + + + + + + + + + fixed source + 100 + 2 + 0 + + + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 + + + + false + + multi-group + + false + + true + + 1 + neutron + 0.0 0.625 20000000.0 + 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 + 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 + 3.0 + 10 + 1e-38 + + + 5 1 1 + 0.0 -1000.0 -1000.0 + 929.45 1000.0 1000.0 + + true + 100 + + + + 5 1 1 + 0.0 -1000.0 -1000.0 + 929.45 1000.0 1000.0 + + + 2 + + + 1 + flux + + + diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat new file mode 100644 index 0000000000..38ff944173 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat @@ -0,0 +1,11 @@ +tally 1: +1.765369E+04 +3.087787E+08 +1.708316E+04 +2.842002E+08 +9.444106E+03 +7.488341E+07 +2.066528E+03 +2.142445E+06 +8.689619E+02 +5.652099E+05 diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py new file mode 100644 index 0000000000..336c8461f1 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py @@ -0,0 +1,91 @@ +import os + +import numpy as np +import openmc +from openmc.examples import slab_mg + +from tests.testing_harness import PyAPITestHarness + + +def create_library(): + # Instantiate the energy group data and file object + groups = openmc.mgxs.EnergyGroups([0.0, 0.625, 20.0e6]) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + + # Make the base, isotropic data + nu = [2.50, 2.50] + fiss = np.array([0.002817, 0.097]) + capture = [0.008708, 0.02518] + absorption = np.add(capture, fiss) + scatter = np.array( + [[[0.31980, 0.06694], [0.004555, -0.0003972]], + [[0.00000, 0.00000], [0.424100, 0.05439000]]]) + total = [0.33588, 0.54628] + chi = [1., 0.] + + mat_1 = openmc.XSdata('mat_1', groups) + mat_1.order = 1 + mat_1.set_nu_fission(np.multiply(nu, fiss)) + mat_1.set_absorption(absorption) + mat_1.set_scatter_matrix(scatter) + mat_1.set_total(total) + mat_1.set_chi(chi) + mg_cross_sections_file.add_xsdata(mat_1) + + # Write the file + mg_cross_sections_file.export_to_hdf5('2g.h5') + + +class MGXSTestHarness(PyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = '2g.h5' + if os.path.exists(f): + os.remove(f) + + +def test_mg_fixed_source_ww_fission_shared_secondary(): + create_library() + model = slab_mg() + + # Override settings for fixed-source mode with shared secondary bank + model.settings.run_mode = 'fixed source' + model.settings.inactive = 0 + model.settings.batches = 2 + model.settings.particles = 100 + model.settings.create_fission_neutrons = True + model.settings.shared_secondary_bank = True + model.settings.max_history_splits = 100 + + # Add weight windows on a simple 1D mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.lower_left = (0.0, -1000.0, -1000.0) + ww_mesh.upper_right = (929.45, 1000.0, 1000.0) + ww_mesh.dimension = (5, 1, 1) + + # Uniform lower bounds for 2 energy groups, 5 spatial bins + lower_bounds = np.full((2, 5, 1, 1), 0.5) + ww = openmc.WeightWindows( + ww_mesh, + lower_bounds.flatten(), + None, + 5.0, + [0.0, 0.625, 20.0e6], + 'neutron' + ) + model.settings.weight_windows = [ww] + + # Add a flux tally + mesh = openmc.RegularMesh() + mesh.lower_left = (0.0, -1000.0, -1000.0) + mesh.upper_right = (929.45, 1000.0, 1000.0) + mesh.dimension = (5, 1, 1) + + tally = openmc.Tally() + tally.filters = [openmc.MeshFilter(mesh)] + tally.scores = ['flux'] + model.tallies = [tally] + + harness = MGXSTestHarness('statepoint.2.h5', model) + harness.main() diff --git a/tests/regression_tests/mg_temperature/build_2g.py b/tests/regression_tests/mg_temperature/build_2g.py index 1fb7234499..bca87364e4 100644 --- a/tests/regression_tests/mg_temperature/build_2g.py +++ b/tests/regression_tests/mg_temperature/build_2g.py @@ -227,7 +227,7 @@ def analytical_solution_2g_therm(xsmin, xsmax=None, wgt=1.0): L = np.array([sa[0] + ss12, 0.0, -ss12, sa[1]]).reshape(2, 2) Q = np.array([nsf[0], nsf[1], 0.0, 0.0]).reshape(2, 2) arr = np.linalg.inv(L).dot(Q) - return np.amax(np.linalg.eigvals(arr)) + return np.amax(np.linalg.eigvals(arr).real) def build_inf_model(xsnames, xslibname, temperature, tempmethod='nearest'): diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_multiplicity_matrix.dat similarity index 100% rename from tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat rename to tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_multiplicity_matrix.dat diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat new file mode 100644 index 0000000000..6e60f1b199 --- /dev/null +++ b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + + + + 7 + + + 0.0 0.625 20000000.0 + + + 0.0 0.625 20000000.0 + + + 3 + + + 8 + + + 9 + + + 43 44 + total + flux + tracklength + + + 43 44 + total + total + tracklength + + + 43 44 + total + flux + tracklength + + + 43 44 + total + absorption + tracklength + + + 43 44 + total + flux + analog + + + 43 44 49 + total + nu-fission + analog + + + 43 44 + total + flux + analog + + + 43 44 49 53 + total + scatter + analog + + + 54 44 + total + flux + tracklength + + + 54 44 + total + total + tracklength + + + 54 44 + total + flux + tracklength + + + 54 44 + total + absorption + tracklength + + + 54 44 + total + flux + analog + + + 54 44 49 + total + nu-fission + analog + + + 54 44 + total + flux + analog + + + 54 44 49 53 + total + scatter + analog + + + 65 44 + total + flux + tracklength + + + 65 44 + total + total + tracklength + + + 65 44 + total + flux + tracklength + + + 65 44 + total + absorption + tracklength + + + 65 44 + total + flux + analog + + + 65 44 49 + total + nu-fission + analog + + + 65 44 + total + flux + analog + + + 65 44 49 53 + total + scatter + analog + + + diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/test.py b/tests/regression_tests/mgxs_library_ce_to_mg/test.py index 075167f588..b68fbd6c33 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg/test.py @@ -9,7 +9,7 @@ from tests.regression_tests import config class MGXSTestHarness(PyAPITestHarness): - def __init__(self, *args, **kwargs): + def __init__(self, *args, scatter_mgxs_type=None, **kwargs): # Generate inputs using parent class routine super().__init__(*args, **kwargs) @@ -19,8 +19,8 @@ class MGXSTestHarness(PyAPITestHarness): # Initialize MGXS Library for a few cross section types self.mgxs_lib = openmc.mgxs.Library(self._model.geometry) self.mgxs_lib.by_nuclide = False - self.mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission matrix', - 'nu-scatter matrix', 'multiplicity matrix'] + self.mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission matrix'] + self.mgxs_lib.mgxs_types += scatter_mgxs_type self.mgxs_lib.energy_groups = energy_groups self.mgxs_lib.correction = None self.mgxs_lib.legendre_order = 3 @@ -69,9 +69,23 @@ class MGXSTestHarness(PyAPITestHarness): os.remove(f) -def test_mgxs_library_ce_to_mg(): +def test_mgxs_library_ce_to_mg_multiplicity_matrix(): # Set the input set to use the pincell model model = pwr_pin_cell() - harness = MGXSTestHarness('statepoint.10.h5', model) + harness = MGXSTestHarness( + 'statepoint.10.h5', model, + inputs_true='inputs_true_multiplicity_matrix.dat', + scatter_mgxs_type=['nu-scatter matrix', 'multiplicity matrix'] + ) + harness.main() + + +def test_mgxs_library_ce_to_mg_scatter_matrix(): + # Set the input set to use the pincell model + model = pwr_pin_cell() + + harness = MGXSTestHarness('statepoint.10.h5', model, + inputs_true='inputs_true_scatter_matrix.dat', + scatter_mgxs_type=['scatter matrix']) harness.main() diff --git a/tests/regression_tests/particle_production_fission/__init__.py b/tests/regression_tests/particle_production_fission/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/particle_production_fission/local/inputs_true.dat b/tests/regression_tests/particle_production_fission/local/inputs_true.dat new file mode 100644 index 0000000000..e93dfc36e4 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/local/inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + fixed source + 100 + 2 + + + 1000000.0 1.0 + + + true + false + + + + neutron + + + neutron + + + 2 1 + events + analog + + + diff --git a/tests/regression_tests/particle_production_fission/local/results_true.dat b/tests/regression_tests/particle_production_fission/local/results_true.dat new file mode 100644 index 0000000000..2f5288eba1 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/local/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +4.570000E+00 +1.047890E+01 diff --git a/tests/regression_tests/particle_production_fission/shared/inputs_true.dat b/tests/regression_tests/particle_production_fission/shared/inputs_true.dat new file mode 100644 index 0000000000..b50a39cd08 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/shared/inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + fixed source + 100 + 2 + + + 1000000.0 1.0 + + + true + true + + + + neutron + + + neutron + + + 2 1 + events + analog + + + diff --git a/tests/regression_tests/particle_production_fission/shared/results_true.dat b/tests/regression_tests/particle_production_fission/shared/results_true.dat new file mode 100644 index 0000000000..5a4dfc516c --- /dev/null +++ b/tests/regression_tests/particle_production_fission/shared/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +5.180000E+00 +1.355140E+01 diff --git a/tests/regression_tests/particle_production_fission/test.py b/tests/regression_tests/particle_production_fission/test.py new file mode 100644 index 0000000000..2ad6ca5155 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/test.py @@ -0,0 +1,49 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_particle_production_fission(shared_secondary, subdir): + """Fixed-source model with fissionable material to test that + ParticleProductionFilter correctly counts fission-born neutrons, + with both local and shared secondary bank modes.""" + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() + + mat = openmc.Material() + mat.set_density('g/cm3', 18.0) + mat.add_nuclide('U235', 1.0) + model.materials.append(mat) + + sph = openmc.Sphere(r=5.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sph) + model.geometry = openmc.Geometry([cell]) + + source = openmc.IndependentSource() + source.energy = openmc.stats.delta_function(1.0e6) + + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.batches = 2 + model.settings.source = source + model.settings.create_fission_neutrons = True + model.settings.shared_secondary_bank = shared_secondary + + # ParticleProductionFilter tracking fission neutron production + ppf = openmc.ParticleProductionFilter(['neutron']) + neutron_filter = openmc.ParticleFilter(['neutron']) + tally = openmc.Tally() + tally.filters = [neutron_filter, ppf] + tally.scores = ['events'] + tally.estimator = 'analog' + model.tallies = [tally] + + harness = PyAPITestHarness('statepoint.2.h5', model) + harness.main() diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py b/tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml new file mode 100644 index 0000000000..c86e016c6e --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml new file mode 100644 index 0000000000..f3851d7ef1 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat b/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat new file mode 100644 index 0000000000..0c84541f90 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat @@ -0,0 +1,16 @@ +current batch: +4.000000E+00 +current generation: +1.000000E+00 +particle id: +3.241000E+03 +run mode: +fixed source +particle weight: +1.000000E+00 +particle energy: +3.896365E+06 +particle xyz: +8.710681E-01 3.698823E+00 -2.286229E+00 +particle uvw: +-5.882735E-01 4.665422E-01 -6.605093E-01 diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml new file mode 100644 index 0000000000..3a80fb17b5 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml @@ -0,0 +1,15 @@ + + + + fixed source + 12 + 1000 + true + + + + -10 -10 -5 10 10 5 + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py b/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py new file mode 100644 index 0000000000..770010900e --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py @@ -0,0 +1,6 @@ +from tests.testing_harness import ParticleRestartTestHarness + + +def test_particle_restart_fixed_shared_secondary(): + harness = ParticleRestartTestHarness('particle_4_3241.h5') + harness.main() diff --git a/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat b/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat new file mode 100644 index 0000000000..e25de58b07 --- /dev/null +++ b/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + false + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/local_neutron/results_true.dat b/tests/regression_tests/pulse_height/local_neutron/results_true.dat new file mode 100644 index 0000000000..ffa284d92b --- /dev/null +++ b/tests/regression_tests/pulse_height/local_neutron/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.890000E+00 +4.784900E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +6.000000E-02 +1.800000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/pulse_height/inputs_true.dat b/tests/regression_tests/pulse_height/local_photon/inputs_true.dat similarity index 94% rename from tests/regression_tests/pulse_height/inputs_true.dat rename to tests/regression_tests/pulse_height/local_photon/inputs_true.dat index 590928e435..84d033d854 100644 --- a/tests/regression_tests/pulse_height/inputs_true.dat +++ b/tests/regression_tests/pulse_height/local_photon/inputs_true.dat @@ -2,7 +2,7 @@ - + @@ -18,14 +18,12 @@ 100 5 - - 0.0 0.0 0.0 - 1000000.0 1.0 true + false diff --git a/tests/regression_tests/pulse_height/results_true.dat b/tests/regression_tests/pulse_height/local_photon/results_true.dat similarity index 100% rename from tests/regression_tests/pulse_height/results_true.dat rename to tests/regression_tests/pulse_height/local_photon/results_true.dat diff --git a/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat b/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat new file mode 100644 index 0000000000..b13c53ef81 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + true + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/shared_neutron/results_true.dat b/tests/regression_tests/pulse_height/shared_neutron/results_true.dat new file mode 100644 index 0000000000..ffa284d92b --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_neutron/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.890000E+00 +4.784900E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +6.000000E-02 +1.800000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat b/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat new file mode 100644 index 0000000000..608410c509 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + true + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/shared_photon/results_true.dat b/tests/regression_tests/pulse_height/shared_photon/results_true.dat new file mode 100644 index 0000000000..c57e8ff1c8 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_photon/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/pulse_height/test.py b/tests/regression_tests/pulse_height/test.py index 90d960f664..371fcbd0f1 100644 --- a/tests/regression_tests/pulse_height/test.py +++ b/tests/regression_tests/pulse_height/test.py @@ -1,53 +1,54 @@ import numpy as np import openmc import pytest +from openmc.utility_funcs import change_directory from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def sphere_model(): - - model = openmc.model.Model() +@pytest.mark.parametrize("shared_secondary,particle", [ + (False, "photon"), + (False, "neutron"), + (True, "photon"), + (True, "neutron") +]) +def test_pulse_height(shared_secondary, particle): + subdir = f"shared_{particle}" if shared_secondary else f"local_{particle}" + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() - # Define materials - NaI = openmc.Material() - NaI.set_density('g/cc', 3.7) - NaI.add_element('Na', 1.0) - NaI.add_element('I', 1.0) + # Define materials + NaI = openmc.Material() + NaI.set_density('g/cm3', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) - model.materials = openmc.Materials([NaI]) + # Define geometry: two spheres in each other + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) - # Define geometry: two spheres in each other - s1 = openmc.Sphere(r=1) - s2 = openmc.Sphere(r=2, boundary_type='vacuum') - inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) - outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) - model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + model.settings.photon_transport = True + model.settings.shared_secondary_bank = shared_secondary + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle=particle + ) - # Define settings - model.settings.run_mode = 'fixed source' - model.settings.batches = 5 - model.settings.particles = 100 - model.settings.photon_transport = True - model.settings.source = openmc.IndependentSource( - space=openmc.stats.Point(), - energy=openmc.stats.Discrete([1e6], [1]), - particle='photon' - ) + # Define tallies + tally = openmc.Tally(name="pht tally") + tally.scores = ['pulse-height'] + cell_filter = openmc.CellFilter(inner_sphere) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1e6, 101)) + tally.filters = [cell_filter, energy_filter] + model.tallies = [tally] - # Define tallies - tally = openmc.Tally(name="pht tally") - tally.scores = ['pulse-height'] - cell_filter = openmc.CellFilter(inner_sphere) - energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) - tally.filters = [cell_filter, energy_filter] - model.tallies = [tally] - - return model - - - -def test_pulse_height(sphere_model): - harness = PyAPITestHarness('statepoint.5.h5', sphere_model) - harness.main() + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat index 86d5ec4abd..81f8c98cac 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 86d5ec4abd..81f8c98cac 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 86d5ec4abd..81f8c98cac 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat index b00935ef38..48b0e8256f 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat index 472406fa88..be738c3e05 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat index 472406fa88..be738c3e05 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat index 15981f7fa5..d2d8289ff2 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat index 86d5ec4abd..81f8c98cac 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat index 15981f7fa5..d2d8289ff2 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat index 86d5ec4abd..81f8c98cac 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat index c60e6a0419..c49020d558 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat index c60e6a0419..c49020d558 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat index c60e6a0419..c49020d558 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 11100e88e1..0ea8c01776 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/weightwindows/local/inputs_true.dat b/tests/regression_tests/weightwindows/local/inputs_true.dat new file mode 100644 index 0000000000..57dc42ca4c --- /dev/null +++ b/tests/regression_tests/weightwindows/local/inputs_true.dat @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixed source + 500 + 2 + + + 0.001 0.001 0.001 + + + 14000000.0 1.0 + + + true + + 2 + neutron + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0008758251780046591 -1.0 -1.0 -1.0 -1.0 0.00044494017319042853 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.6620271125067025e-05 0.0006278777700923334 3.3816651814344154e-05 -1.0 -1.0 0.0024363004681119066 0.04404124277352227 0.002389900091734746 -1.0 -1.0 0.001096572213298872 0.04862206884590391 0.0011530054432113332 -1.0 -1.0 -1.0 0.00029204950777272335 2.2332845991385424e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0001298973206651331 0.0028826281529980655 0.00018941326535850932 -1.0 5.4657521927731274e-05 0.014670839729146354 0.49999999999999994 0.011271060592765664 -1.0 -1.0 0.014846491082523524 0.4897211700090108 0.013284874451810723 -1.0 -1.0 5.14657989105535e-05 0.0010115278438204965 0.0008429411802845685 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0009344129479768359 -1.0 -1.0 -1.0 0.0024828273390431962 0.04299740304329489 0.0020539079252555113 -1.0 -1.0 0.003034165905819096 0.04870927636605937 0.00313101668086297 -1.0 -1.0 -1.0 6.339910999436737e-05 7.442176086066386e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.00011276372995589871 0.0002236100157024887 9.56304298913265e-08 -1.0 -1.0 0.0001596955110781239 9.960576598335084e-06 5.3833030623153676e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 4.0453018186074215e-05 4.6013290110121894e-05 2.709738801641897e-05 -1.0 -1.0 -1.0 9.538410811938703e-05 1.992978141244964e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0723231851298364e-05 -1.0 -1.0 -1.0 -1.0 0.0008030100296698905 0.017386220476187222 0.0005027758935317528 -1.0 -1.0 0.00103568411326969 0.015609071124009234 0.0007473731339616588 -1.0 -1.0 -1.0 6.979537549963374e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0014257756927429828 5.9098070539559466e-05 -1.0 5.0168071459366625e-06 0.005588863190166347 0.5 0.003930588100414563 -1.0 -1.0 0.005163345217476071 0.48250750993887326 0.003510432129522241 -1.0 -1.0 3.371977841720992e-05 0.0006640103276501794 9.554899988419713e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.000187872910697387 -1.0 -1.0 -1.0 0.0009134587866163143 0.017081858078684467 0.0002972747357542354 -1.0 -1.0 0.0007973472495507653 0.019300903254809747 0.000902203032280694 -1.0 -1.0 6.170015233787292e-05 1.898984300674969e-05 9.85411583595722e-07 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0173913765490095e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.008758251780046591 -10.0 -10.0 -10.0 -10.0 0.004449401731904285 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00016620271125067024 0.006278777700923334 0.00033816651814344155 -10.0 -10.0 0.024363004681119065 0.4404124277352227 0.02389900091734746 -10.0 -10.0 0.01096572213298872 0.4862206884590391 0.011530054432113333 -10.0 -10.0 -10.0 0.0029204950777272335 0.00022332845991385425 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0012989732066513312 0.028826281529980655 0.0018941326535850931 -10.0 0.0005465752192773128 0.14670839729146354 4.999999999999999 0.11271060592765664 -10.0 -10.0 0.14846491082523525 4.897211700090108 0.13284874451810724 -10.0 -10.0 0.000514657989105535 0.010115278438204964 0.008429411802845685 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00934412947976836 -10.0 -10.0 -10.0 0.024828273390431962 0.4299740304329489 0.020539079252555114 -10.0 -10.0 0.03034165905819096 0.48709276366059373 0.0313101668086297 -10.0 -10.0 -10.0 0.0006339910999436737 0.0007442176086066385 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0011276372995589871 0.002236100157024887 9.56304298913265e-07 -10.0 -10.0 0.001596955110781239 9.960576598335084e-05 0.0005383303062315367 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00040453018186074213 0.00046013290110121896 0.0002709738801641897 -10.0 -10.0 -10.0 0.0009538410811938703 0.00019929781412449641 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010723231851298364 -10.0 -10.0 -10.0 -10.0 0.008030100296698905 0.17386220476187222 0.0050277589353175285 -10.0 -10.0 0.010356841132696899 0.15609071124009233 0.007473731339616587 -10.0 -10.0 -10.0 0.0006979537549963374 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.014257756927429827 0.0005909807053955946 -10.0 5.016807145936663e-05 0.055888631901663474 5.0 0.039305881004145636 -10.0 -10.0 0.05163345217476071 4.825075099388733 0.03510432129522241 -10.0 -10.0 0.00033719778417209923 0.006640103276501793 0.0009554899988419713 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00187872910697387 -10.0 -10.0 -10.0 0.009134587866163143 0.17081858078684467 0.002972747357542354 -10.0 -10.0 0.007973472495507653 0.19300903254809748 0.009022030322806941 -10.0 -10.0 0.0006170015233787292 0.0001898984300674969 9.85411583595722e-06 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010173913765490096 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + + 5 6 7 + -240 -240 -240 + 240 240 240 + + + 2 + photon + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 2.0445691096074744e-05 -1.0 -1.0 -1.0 0.00013281328509288383 0.0006267128082339883 -1.0 -1.0 -1.0 -1.0 0.0004209808495056742 5.340221214300681e-05 -1.0 -1.0 -1.0 1.553693492042344e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 3.7096847855980484e-05 -1.0 -1.0 5.41969614042385e-05 0.0007918090215756442 9.763572147337743e-05 -1.0 -1.0 0.0026398319229115233 0.04039871468258457 0.002806654735003014 -1.0 -1.0 0.0027608366482897244 0.040190978087723005 0.0025084416103979975 -1.0 -1.0 9.774868433298412e-05 0.0006126404916879651 0.00014841730774317252 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.75342408717107e-06 1.7610849405640072e-05 -1.0 -1.0 0.0001160370359598608 0.002822592083222468 0.0003897177301239376 -1.0 -1.0 0.014965179733172532 0.5 0.011723491704290401 -1.0 2.260493623875525e-05 0.015157364795884922 0.49376993953402387 0.013111419473204967 -1.0 -1.0 0.00014919915366377564 0.002197964829133137 0.0003209711829219673 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.4806422867046436e-05 -1.0 -1.0 -1.0 9.207268175745281e-05 0.0007066033561638983 -1.0 -1.0 -1.0 0.003246064903858183 0.03905493028065908 0.0025380574501073605 -1.0 -1.0 0.0032499260211917933 0.04335567738779479 0.0031577214557017056 4.521352809838806e-05 -1.0 0.00018268090756001903 0.0004353654810741932 0.00011273259573092372 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.650194311662424e-06 0.00029403373970835075 0.00013506203419326438 -1.0 -1.0 4.5065302725431816e-06 0.00027725323638744237 3.547290864255937e-05 -1.0 -1.0 -1.0 8.786172732576295e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00020445691096074745 -10.0 -10.0 -10.0 0.0013281328509288383 0.006267128082339883 -10.0 -10.0 -10.0 -10.0 0.004209808495056742 0.0005340221214300681 -10.0 -10.0 -10.0 0.00015536934920423439 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00037096847855980485 -10.0 -10.0 0.000541969614042385 0.007918090215756443 0.0009763572147337743 -10.0 -10.0 0.026398319229115234 0.4039871468258457 0.02806654735003014 -10.0 -10.0 0.027608366482897245 0.40190978087723006 0.025084416103979976 -10.0 -10.0 0.0009774868433298411 0.0061264049168796506 0.0014841730774317252 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.753424087171071e-05 0.00017610849405640073 -10.0 -10.0 0.001160370359598608 0.02822592083222468 0.003897177301239376 -10.0 -10.0 0.14965179733172532 5.0 0.11723491704290401 -10.0 0.0002260493623875525 0.15157364795884923 4.937699395340239 0.13111419473204966 -10.0 -10.0 0.0014919915366377564 0.02197964829133137 0.003209711829219673 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00014806422867046437 -10.0 -10.0 -10.0 0.0009207268175745281 0.007066033561638983 -10.0 -10.0 -10.0 0.03246064903858183 0.39054930280659084 0.025380574501073606 -10.0 -10.0 0.03249926021191793 0.4335567738779479 0.03157721455701706 0.0004521352809838806 -10.0 0.0018268090756001904 0.004353654810741932 0.001127325957309237 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.650194311662424e-05 0.0029403373970835075 0.0013506203419326437 -10.0 -10.0 4.5065302725431816e-05 0.002772532363874424 0.0003547290864255937 -10.0 -10.0 -10.0 0.0008786172732576295 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + false + + true + true + + 200 + + + + 5 10 15 + -240 -240 -240 + 240 240 240 + + + 1 + + + 0.0 0.5 20000000.0 + + + neutron photon + + + 1 2 3 + flux + + + diff --git a/tests/regression_tests/weightwindows/local/results_true.dat b/tests/regression_tests/weightwindows/local/results_true.dat new file mode 100644 index 0000000000..d3dfa119d8 --- /dev/null +++ b/tests/regression_tests/weightwindows/local/results_true.dat @@ -0,0 +1 @@ +a78972fe3c0dadfc256cfb139c5ca8c7b634738e1895ad2d6286ed5e2567c6dc518e499363908e9058432684148a706a179a39110f703d5322491184a1d0c3e4 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat deleted file mode 100644 index 897089e059..0000000000 --- a/tests/regression_tests/weightwindows/results_true.dat +++ /dev/null @@ -1 +0,0 @@ -a4a3ccca43666e2ca1e71800201b152cca20c387b93d67522c5339807348dcee5cada9acbed3238f37e2e86e76b374b06988742f07d4ea1b413e4e75d0c180b1 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/shared/inputs_true.dat similarity index 99% rename from tests/regression_tests/weightwindows/inputs_true.dat rename to tests/regression_tests/weightwindows/shared/inputs_true.dat index aa7a0379dd..6ec7d12e33 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/shared/inputs_true.dat @@ -64,6 +64,7 @@ 10 1e-38 + true true true diff --git a/tests/regression_tests/weightwindows/shared/results_true.dat b/tests/regression_tests/weightwindows/shared/results_true.dat new file mode 100644 index 0000000000..01381db32c --- /dev/null +++ b/tests/regression_tests/weightwindows/shared/results_true.dat @@ -0,0 +1 @@ +27c2d218ecf46161088003fbb39ccd63495f80d6e0b27dbc3b3e845b49b282dabf0eaba4fb619be15c6116fc963ec5ce1fe4197efbe4084972ec761e02e70eb5 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat new file mode 100644 index 0000000000..fe144dc62e --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + fixed source + 50 + 5 + + + + 0.01 1.0 + + + + + + + 14100000.0 1.0 + + + true + + 1 + neutron + 0.46135961568957096 0.2874485390202603 0.13256013950494605 0.052765209609807295 0.020958440832685638 0.006317250035749876 0.002627037175682506 0.00030032343592437284 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4611178493390162 0.28624862560842024 0.13999870622621136 0.05508479408959756 0.018281241958254993 0.0055577764872361555 0.0015265432226293397 0.00024298772352636966 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.46294957913063317 0.2857734367546051 0.13365623190336945 0.05034742166227103 0.017525851812913266 0.005096725451851077 0.0026297640951531403 0.00033732424392026144 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45772598750472554 0.281494921471495 0.13604397962762246 0.054792881472295364 0.019802376898755525 0.0073351745579128495 0.002067392946066426 9.529474085926143e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4739762983490318 0.28419061537046764 0.12757509901507888 0.0516920293019733 0.01919167874787235 0.007593035964707848 0.0017488176314107277 9.678267909681988e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47560199098558276 0.27728389146956994 0.12760802572535623 0.05251965811713552 0.022498960644951632 0.01063372459325151 0.004242071054914633 0.00045603112202665183 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47659199471934693 0.28886666944252215 0.1258637573398161 0.05818862375955657 0.020476313720744762 0.007143193244018263 0.0022364083022515273 0.0003953123781408474 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4693487369606823 0.27180862139490125 0.12833455570423483 0.055146743559938344 0.020667403529470333 0.007891508723137146 0.001643551705407358 0.0006501416781353278 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4538724931023337 0.2824491421084296 0.1275341706351566 0.055027501141893705 0.02305114640508087 0.008599303158607198 0.0025082611194161804 0.000518530311231696 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45258444753220123 0.26839102466484255 0.12991633918797083 0.05442263709947767 0.019695895223471486 0.005353204961887518 0.0015074698293840157 0.0001598680898180058 8.872047880811405e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4482467985982804 0.2813276470183359 0.13449860790768647 0.056103278190533436 0.026274320334196962 0.006884071908429303 0.0033099390738735458 0.0005337454397674922 0.0001386380399465575 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4875748950139487 0.2908586705316248 0.14212060658392278 0.06068108009637272 0.019467970468789477 0.00637102427946399 0.0028510425266191687 0.0012184421778115488 0.00029404541567146187 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.5 0.2852101551297824 0.13622307172321643 0.058302519884339946 0.023124734915152625 0.007401527839638796 0.002527635652743992 0.0008509612315162482 0.00018213334574554768 7.815977984795461e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47706397688984176 0.28313436859495966 0.13170624744221976 0.0525453512766549 0.021111753158319105 0.0067451713955609645 0.003204270539718037 0.0010308107242263192 0.0002759210144794204 0.00013727805365387318 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.48641988659284513 0.2816173231471242 0.131541850061199 0.054793200248327956 0.016517701691156576 0.006757591781856257 0.0025425350750908644 0.0006383278085839443 2.279421641064226e-05 0.0003597432472224371 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4307755159245372 0.2652713784080849 0.12753857957535653 0.05470396852881577 0.02049817309420563 0.00566559741247352 0.0007213846765465147 6.848024591311009e-05 -1.0 8.609308814924014e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.43631324024956025 0.2639077825530567 0.13368737962742414 0.05213531603720763 0.020628860469743833 0.008268384844678654 0.0028599221013934375 0.00013059757129982714 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45926287238219 0.2635824148883888 0.13104280996799478 0.055379812186041905 0.019255042561941074 0.007252095690246872 0.0018026593488140996 0.0003215836458542421 9.14840176000331e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4574378998400604 0.28714971179927723 0.1363507911051337 0.05010315954654347 0.017963278989571074 0.0058998176297971605 0.0021393135666168 0.00015677575033083482 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4821442782978812 0.27704132233083506 0.14149074035662307 0.05546042815834048 0.016800649382269998 0.004096804603054233 0.0019161108398578026 0.0005289600253155307 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + 2.306798078447855 1.4372426951013015 0.6628006975247303 0.2638260480490365 0.10479220416342819 0.03158625017874938 0.013135185878412529 0.0015016171796218643 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3055892466950807 1.4312431280421012 0.6999935311310568 0.2754239704479878 0.09140620979127496 0.027788882436180776 0.007632716113146698 0.0012149386176318483 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.314747895653166 1.4288671837730256 0.6682811595168472 0.25173710831135515 0.08762925906456634 0.025483627259255386 0.013148820475765701 0.0016866212196013073 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.288629937523628 1.407474607357475 0.6802198981381123 0.2739644073614768 0.09901188449377762 0.036675872789564246 0.01033696473033213 0.00047647370429630714 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.369881491745159 1.4209530768523382 0.6378754950753944 0.2584601465098665 0.09595839373936176 0.03796517982353924 0.008744088157053638 0.00048391339548409945 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3780099549279137 1.3864194573478497 0.6380401286267812 0.26259829058567763 0.11249480322475816 0.053168622966257545 0.021210355274573163 0.0022801556101332593 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.382959973596735 1.4443333472126108 0.6293187866990806 0.29094311879778284 0.10238156860372381 0.035715966220091315 0.011182041511257637 0.001976561890704237 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3467436848034113 1.3590431069745064 0.6416727785211741 0.2757337177996917 0.10333701764735166 0.03945754361568573 0.00821775852703679 0.0032507083906766388 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2693624655116684 1.412245710542148 0.637670853175783 0.27513750570946854 0.11525573202540434 0.04299651579303599 0.012541305597080901 0.00259265155615848 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.262922237661006 1.3419551233242126 0.6495816959398542 0.2721131854973884 0.09847947611735743 0.02676602480943759 0.007537349146920078 0.000799340449090029 0.0004436023940405703 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2412339929914022 1.4066382350916795 0.6724930395384323 0.28051639095266717 0.1313716016709848 0.03442035954214652 0.016549695369367727 0.0026687271988374613 0.0006931901997327875 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.4378744750697434 1.4542933526581239 0.7106030329196139 0.3034054004818636 0.09733985234394739 0.03185512139731995 0.014255212633095843 0.006092210889057744 0.0014702270783573093 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.5 1.4260507756489118 0.6811153586160822 0.2915125994216997 0.11562367457576313 0.03700763919819398 0.012638178263719959 0.004254806157581241 0.0009106667287277384 3.9079889923977307e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.385319884449209 1.4156718429747983 0.6585312372110987 0.2627267563832745 0.10555876579159552 0.03372585697780482 0.016021352698590185 0.005154053621131596 0.001379605072397102 0.000686390268269366 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.432099432964226 1.4080866157356209 0.657709250305995 0.2739660012416398 0.08258850845578287 0.03378795890928128 0.012712675375454322 0.0031916390429197216 0.0001139710820532113 0.0017987162361121855 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.1538775796226863 1.3263568920404245 0.6376928978767826 0.27351984264407886 0.10249086547102815 0.028327987062367603 0.0036069233827325737 0.0003424012295655504 -5.0 0.0004304654407462007 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.181566201247801 1.3195389127652835 0.6684368981371207 0.2606765801860381 0.10314430234871916 0.041341924223393264 0.014299610506967188 0.0006529878564991358 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.29631436191095 1.317912074441944 0.6552140498399739 0.27689906093020955 0.09627521280970537 0.03626047845123436 0.009013296744070498 0.0016079182292712106 4.574200880001655e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.287189499200302 1.435748558996386 0.6817539555256685 0.25051579773271737 0.08981639494785537 0.029499088148985803 0.010696567833083998 0.0007838787516541741 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.410721391489406 1.3852066116541752 0.7074537017831153 0.2773021407917024 0.08400324691134999 0.020484023015271167 0.009580554199289014 0.0026448001265776534 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 + 3.0 + 10 + 1e-38 + + + 20 20 1 + 0.0 0.0 0.0 + 160.0 160.0 160.0 + + false + + true + true + + + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/weightwindows/survival_biasing/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat similarity index 100% rename from tests/regression_tests/weightwindows/survival_biasing/results_true.dat rename to tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat diff --git a/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat similarity index 99% rename from tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat rename to tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat index d5aa135d47..bafff89c17 100644 --- a/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat +++ b/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat @@ -51,6 +51,7 @@ 0.0 0.0 0.0 160.0 160.0 160.0 + true true true diff --git a/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat new file mode 100644 index 0000000000..11e5ffa77f --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat @@ -0,0 +1 @@ +d17a437262d3316985fba4b48e21a7fcd5f32ac2d96ef0e66849f567deaeadc1e0f18d5d0bf5e9cab4246dbf823e7f43f249a1f67e928b27ae8c70b89f3cefbf \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/test.py b/tests/regression_tests/weightwindows/survival_biasing/test.py index 92604e3382..c3a3e91a22 100644 --- a/tests/regression_tests/weightwindows/survival_biasing/test.py +++ b/tests/regression_tests/weightwindows/survival_biasing/test.py @@ -1,14 +1,16 @@ +from pathlib import Path + import pytest import numpy as np - import openmc -from openmc.stats import Discrete, Point +from openmc.utility_funcs import change_directory from tests.testing_harness import HashedPyAPITestHarness -@pytest.fixture -def model(): +def build_model(shared_secondary): + openmc.reset_auto_ids() + # Material w = openmc.Material(name='Tungsten') w.add_element('W', 1.0) @@ -38,13 +40,14 @@ def model(): angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) energy = openmc.stats.Discrete([14.1e6], [1.0]) - source = openmc.Source(space=space, angle=angle, energy=energy) + source = openmc.IndependentSource(space=space, angle=angle, energy=energy) settings = openmc.Settings() settings.run_mode = 'fixed source' settings.batches = 5 settings.particles = 50 settings.source = source + settings.shared_secondary_bank = shared_secondary model = openmc.Model(geometry=geometry, materials=materials, settings=settings) @@ -61,7 +64,8 @@ def model(): tallies = openmc.Tallies([flux_tally]) model.tallies = tallies - lower_ww_bounds = np.loadtxt('ww_n.txt') + parent_dir = Path(__file__).parent + lower_ww_bounds = np.loadtxt(parent_dir / 'ww_n.txt') weight_windows = openmc.WeightWindows(mesh, lower_ww_bounds, @@ -76,6 +80,12 @@ def model(): return model -def test_weight_windows_with_survival_biasing(model): - harness = HashedPyAPITestHarness('statepoint.5.h5', model) - harness.main() +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weight_windows_with_survival_biasing(shared_secondary, subdir): + with change_directory(subdir): + model = build_model(shared_secondary) + harness = HashedPyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index c22ca1b75d..c5b6bd889d 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -1,14 +1,17 @@ +from pathlib import Path + import pytest import numpy as np import openmc from openmc.stats import Discrete, Point +from openmc.utility_funcs import change_directory from tests.testing_harness import HashedPyAPITestHarness -@pytest.fixture -def model(): +def build_model(shared_secondary): + openmc.reset_auto_ids() model = openmc.Model() # materials (M4 steel alloy) @@ -43,6 +46,7 @@ def model(): settings.batches = 2 settings.max_history_splits = 200 settings.photon_transport = True + settings.shared_secondary_bank = shared_secondary settings.weight_window_checkpoints = {'surface': True, 'collision': True} space = Point((0.001, 0.001, 0.001)) @@ -71,10 +75,10 @@ def model(): # weight windows - # load pre-generated weight windows - # (created using the same tally as above) - ww_n_lower_bnds = np.loadtxt('ww_n.txt') - ww_p_lower_bnds = np.loadtxt('ww_p.txt') + # load pre-generated weight windows from parent directory + parent_dir = Path(__file__).parent + ww_n_lower_bnds = np.loadtxt(parent_dir / 'ww_n.txt') + ww_p_lower_bnds = np.loadtxt(parent_dir / 'ww_p.txt') # create a mesh matching the one used # to generate the weight windows @@ -104,9 +108,70 @@ def model(): return model -def test_weightwindows(model): - test = HashedPyAPITestHarness('statepoint.2.h5', model) - test.main() +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weightwindows(shared_secondary, subdir): + with change_directory(subdir): + model = build_model(shared_secondary) + test = HashedPyAPITestHarness('statepoint.2.h5', model) + test.main() + + +def test_zero_bound_windows_play_no_game(tmp_path): + # A weight window lower bound of zero means no weight window information + # exists there (MCNP wwinp files use zero to turn the game off in a cell), + # so transport must proceed as if weight windows were disabled. Previously, + # zero-bound windows demanded a split at every checkpoint (weight/0 -> + # max_split), multiplying the particle population until terminated by the + # split or weight cutoff limits. + model = build_model(False) + for ww in model.settings.weight_windows: + ww.lower_ww_bounds = np.zeros_like(ww.lower_ww_bounds) + ww.upper_ww_bounds = np.zeros_like(ww.upper_ww_bounds) + sp_zero = model.run(cwd=tmp_path / 'zero_windows') + + model.settings.weight_windows_on = False + sp_off = model.run(cwd=tmp_path / 'windows_off') + + with openmc.StatePoint(sp_zero) as sp: + flux_zero = list(sp.tallies.values())[0].mean + with openmc.StatePoint(sp_off) as sp: + flux_off = list(sp.tallies.values())[0].mean + + np.testing.assert_allclose(flux_zero, flux_off, rtol=1e-12) + + +def test_zero_and_negative_bounds_equivalent(tmp_path): + # Zero and negative lower bounds both mean that no weight window + # information exists in a cell (generators mark such cells with -1, and + # MCNP wwinp files use zero), so they must produce identical transport. + # Unlike the all-zero case above, here particles are born under valid + # windows and encounter the no-information region in flight; previously a + # zero lower bound in that situation demanded a split at every checkpoint + # in the cell (weight/0 -> max_split), multiplying the particle population, + # while -1 played no game. + def run_with(bound_value, subdir): + model = build_model(False) + for ww in model.settings.weight_windows: + lb = np.array(ww.lower_ww_bounds, copy=True) + ub = np.array(ww.upper_ww_bounds, copy=True) + lb[3:, :, :, :] = bound_value + ub[3:, :, :, :] = bound_value + ww.lower_ww_bounds = lb + ww.upper_ww_bounds = ub + return model.run(cwd=tmp_path / subdir) + + sp_zero = run_with(0.0, 'zero_region') + sp_negative = run_with(-1.0, 'negative_region') + + with openmc.StatePoint(sp_zero) as sp: + flux_zero = list(sp.tallies.values())[0].mean + with openmc.StatePoint(sp_negative) as sp: + flux_negative = list(sp.tallies.values())[0].mean + + np.testing.assert_allclose(flux_zero, flux_negative, rtol=1e-12) def test_wwinp_cylindrical(): diff --git a/tests/regression_tests/weightwindows_pulse_height/__init__.py b/tests/regression_tests/weightwindows_pulse_height/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat b/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat new file mode 100644 index 0000000000..4cd24d5291 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + + 1 + photon + 0.0 2000000.0 + 0.01 + 0.05 + 3.0 + 10 + 1e-38 + + + 1 1 1 + -2 -2 -2 + 2 2 2 + + false + + true + true + + 50 + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat b/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat new file mode 100644 index 0000000000..c57e8ff1c8 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat b/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat new file mode 100644 index 0000000000..8bb499a73b --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + + 1 + photon + 0.0 2000000.0 + 0.01 + 0.05 + 3.0 + 10 + 1e-38 + + + 1 1 1 + -2 -2 -2 + 2 2 2 + + true + + true + true + + 50 + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat b/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat new file mode 100644 index 0000000000..c57e8ff1c8 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/weightwindows_pulse_height/test.py b/tests/regression_tests/weightwindows_pulse_height/test.py new file mode 100644 index 0000000000..b6b49be260 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/test.py @@ -0,0 +1,73 @@ +import numpy as np +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weightwindows_pulse_height(shared_secondary, subdir): + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() + + # Define materials (NaI scintillator) + NaI = openmc.Material() + NaI.set_density('g/cc', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) + + model.materials = openmc.Materials([NaI]) + + # Define geometry: NaI sphere inside vacuum sphere + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + model.settings.photon_transport = True + model.settings.shared_secondary_bank = shared_secondary + model.settings.max_history_splits = 50 + model.settings.weight_window_checkpoints = { + 'surface': True, + 'collision': True, + } + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle='photon' + ) + + # Define pulse-height tally + tally = openmc.Tally(name="pht tally") + tally.scores = ['pulse-height'] + cell_filter = openmc.CellFilter(inner_sphere) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) + tally.filters = [cell_filter, energy_filter] + model.tallies = [tally] + + # Define weight windows on a simple mesh covering the geometry + ww_mesh = openmc.RegularMesh() + ww_mesh.lower_left = (-2, -2, -2) + ww_mesh.upper_right = (2, 2, 2) + ww_mesh.dimension = (1, 1, 1) + + # Single energy bin for photons + e_bnds = [0.0, 2e6] + + # Uniform weight window bounds (low enough to trigger some splitting) + lower_bounds = np.array([0.01]) + + ww = openmc.WeightWindows(ww_mesh, lower_bounds, None, 5.0, e_bnds, 'photon') + model.settings.weight_windows = [ww] + + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 1ad91b7a89..c1977556d3 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -143,7 +143,8 @@ class TestHarness: def _cleanup(self): """Delete statepoints, tally, and test files.""" output = glob.glob('statepoint.*.h5') - output += ['tallies.out', 'results_test.dat', 'summary.h5'] + output += ['tallies.out', 'tallies.forward.out'] + output += ['results_test.dat', 'summary.h5'] output += glob.glob('volume_*.h5') for f in output: if os.path.exists(f): @@ -546,19 +547,19 @@ class CollisionTrackTestHarness(PyAPITestHarness): def _test_output_created(self): """Make sure collision_track.h5 has also been created.""" - super()._test_output_created() if self._model.settings.collision_track: assert os.path.exists( "collision_track.h5" ), "collision_track file has not been created." - def _compare_output(self): + def _compare_results(self): """Compare collision_track.h5 files.""" if self._model.settings.collision_track: collision_track_true = self._return_collision_track_data( "collision_track_true.h5") collision_track_test = self._return_collision_track_data( "collision_track.h5") + assert collision_track_true.shape == collision_track_test.shape np.testing.assert_allclose( collision_track_true, collision_track_test, rtol=1e-07) @@ -582,15 +583,18 @@ class CollisionTrackTestHarness(PyAPITestHarness): def _overwrite_results(self): """Also add the 'collision_track.h5' file during overwriting.""" - super()._overwrite_results() if os.path.exists("collision_track.h5"): shutil.copyfile("collision_track.h5", "collision_track_true.h5") + def _write_results(self, results_string): + # The result file for this test are written by the OpenMC executable itself + pass + @staticmethod def _return_collision_track_data(filepath): """ - Read a collision_track file and return a sorted array composed - of flatten arrays of collision information. + Read a collision_track file and return a sorted array composed of + flattened collision records. Parameters ---------- @@ -600,42 +604,58 @@ class CollisionTrackTestHarness(PyAPITestHarness): Returns ------- data : np.array - Sorted array composed of flatten arrays of collision_track data for - each collision information + Sorted array composed of flattened collision-track records. """ - data = [] - keys = [] - - # Read source file source = openmc.read_collision_track_file(filepath) - for src in source: - r = src['r'] - u = src['u'] - e = src['E'] - de = src['dE'] - time = src['time'] - wgt = src['wgt'] - delayed_group = src['delayed_group'] - cell_id = src['cell_id'] - nuclide_id = src['nuclide_id'] - material_id = src['material_id'] - universe_id = src['universe_id'] - n_collision = src['n_collision'] - event_mt = src['event_mt'] - key = ( - f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" - f"{e:.10e} {de:.10e} {time:.10e} {wgt:.10e} {event_mt} {delayed_group} {cell_id}" - f"{nuclide_id} {material_id} {universe_id} {n_collision} " - ) - keys.append(key) - values = [*r, *u, e, de, time, wgt, event_mt, - delayed_group, cell_id, nuclide_id, material_id, - universe_id, n_collision] - assert len(values) == 17 - data.append(values) + columns = [ + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + source['event_mt'], + source['delayed_group'], + source['cell_id'], + source['nuclide_id'], + source['material_id'], + source['universe_id'], + source['n_collision'], + source['particle'], + source['parent_id'], + source['progeny_id'], + ] + data = np.column_stack(columns) - data = np.array(data) - keys = np.array(keys) - sorted_idx = np.argsort(keys, kind='stable') + # Sort by the complete record, prioritizing stable integer identifiers + # before floating-point fields. This removes dependence on the order in + # which threads append otherwise reproducible collision records. + sort_columns = [ + source['parent_id'], + source['progeny_id'], + source['n_collision'], + source['particle'], + source['cell_id'], + source['material_id'], + source['universe_id'], + source['nuclide_id'], + source['event_mt'], + source['delayed_group'], + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + ] + sorted_idx = np.lexsort(tuple(reversed(sort_columns))) return data[sorted_idx] diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py index 0917f5b237..5889fa5420 100644 --- a/tests/unit_tests/dagmc/test_model.py +++ b/tests/unit_tests/dagmc/test_model.py @@ -70,28 +70,20 @@ def model(request): openmc.reset_auto_ids() -def test_dagmc_replace_material_assignment(model): - mats = {} - - mats["foo"] = openmc.Material(name="foo") - mats["foo"].add_nuclide("H1", 2.0) - mats["foo"].add_element("O", 1.0) - mats["foo"].set_density("g/cm3", 1.0) - mats["foo"].add_s_alpha_beta("c_H_in_H2O") - +def test_dagmc_sync_cell_names(model): + dag_univ = None for univ in model.geometry.get_all_universes().values(): - if not isinstance(univ, openmc.DAGMCUniverse): + if isinstance(univ, openmc.DAGMCUniverse): + dag_univ = univ break - cells_with_41 = [] - for cell in univ.cells.values(): - if cell.fill is None: - continue - if cell.fill.name == "41": - cells_with_41.append(cell.id) - univ.replace_material_assignment("41", mats["foo"]) - for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert dag_univ is not None + + for cell_id, cell in dag_univ.cells.items(): + assert cell.name == openmc.lib.cells[cell_id].name + + assert any(cell.name == "implicit complement" + for cell in dag_univ.cells.values()) def test_dagmc_add_material_override_with_id(model): @@ -114,7 +106,7 @@ def test_dagmc_add_material_override_with_id(model): cells_with_41.append(cell.id) univ.add_material_override(cell.id, mats["foo"]) for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert univ.cells[cell_id].fill == mats["foo"] def test_dagmc_add_material_override_with_cell(model): @@ -137,7 +129,7 @@ def test_dagmc_add_material_override_with_cell(model): cells_with_41.append(cell.id) univ.add_material_override(cell, mats["foo"]) for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert univ.cells[cell_id].fill == mats["foo"] def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir): @@ -174,57 +166,20 @@ def test_model_differentiate_with_dagmc(model): assert len(model.materials) == 4*2 + 4 -def test_bad_override_cell_id(model): - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="Cell ID '1' not found in DAGMC universe"): - univ.material_overrides = {1: model.materials[0]} - - -def test_bad_override_type(model): - not_a_dag_cell = openmc.Cell() - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="Unrecognized key type. Must be an integer or openmc.DAGMCCell object"): - univ.material_overrides = {not_a_dag_cell: model.materials[0]} - - -def test_bad_replacement_mat_name(model): - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="No material with name 'not_a_mat' found in the DAGMC universe"): - univ.replace_material_assignment("not_a_mat", model.materials[0]) - - def test_dagmc_xml(model): - # Set the environment - mats = {} - mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") - mats["no-void fuel"].add_nuclide("U235", 0.03) - mats["no-void fuel"].add_nuclide("U238", 0.97) - mats["no-void fuel"].add_nuclide("O16", 2.0) - mats["no-void fuel"].set_density("g/cm3", 10.0) - - mats[5] = openmc.Material(name="41") - mats[5].add_nuclide("H1", 2.0) - mats[5].add_element("O", 1.0) - mats[5].set_density("g/cm3", 1.0) - mats[5].add_s_alpha_beta("c_H_in_H2O") + override_mat = openmc.Material(name="41") + override_mat.add_nuclide("H1", 2.0) + override_mat.add_element("O", 1.0) + override_mat.set_density("g/cm3", 1.0) + override_mat.add_s_alpha_beta("c_H_in_H2O") + model.materials.append(override_mat) for univ in model.geometry.get_all_universes().values(): if isinstance(univ, openmc.DAGMCUniverse): dag_univ = univ break - for k, v in mats.items(): - if isinstance(k, int): - dag_univ.add_material_override(k, v) - model.materials.append(v) - elif isinstance(k, str): - dag_univ.replace_material_assignment(k, v) + dag_univ.add_material_override(5, override_mat) # Tesing the XML subelement generation root = ET.Element('dagmc_universe') @@ -236,12 +191,24 @@ def test_dagmc_xml(model): assert dagmc_ele.get('filename') == str(dag_univ.filename) assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower() - override_eles = dagmc_ele.find('material_overrides').findall('cell_override') - assert len(override_eles) == 4 + assert dagmc_ele.find('material_overrides') is None - for i, override_ele in enumerate(override_eles): - cell_id = override_ele.get('id') - assert dag_univ.material_overrides[int(cell_id)][0].id == int(override_ele.find('material_ids').text) + override_elements = dagmc_ele.findall('cell') + assert len(override_elements) == len(dag_univ.cells) + xml_cells = {int(elem.get('id')): elem for elem in override_elements} + for cell_id, cell in dag_univ.cells.items(): + assert cell_id in xml_cells + xml_cell = xml_cells[cell_id] + if cell.fill_type == 'void': + assert xml_cell.get('material') == 'void' + elif cell.fill_type == 'material': + assert xml_cell.get('material') == str(cell.fill.id) + elif cell.fill_type == 'distribmat': + mat_list = xml_cell.find('material').text.split() + expected = ["void" if m is None else str(m.id) for m in cell.fill] + assert mat_list == expected + else: + pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}") model.export_to_model_xml() @@ -252,7 +219,147 @@ def test_dagmc_xml(model): xml_dagmc_univ = univ break - assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.keys() + assert xml_dagmc_univ.cells.keys() == dag_univ.cells.keys() - for xml_mats, model_mats in zip(xml_dagmc_univ._material_overrides.values(), dag_univ._material_overrides.values()): - assert all([xml_mat.id == orig_mat.id for xml_mat, orig_mat in zip(xml_mats, model_mats)]) + for cell_id, cell in dag_univ.cells.items(): + xml_cell = xml_dagmc_univ.cells[cell_id] + assert xml_cell.fill_type == cell.fill_type + if cell.fill_type == 'void': + assert xml_cell.fill is None + elif cell.fill_type == 'material': + assert xml_cell.fill.id == cell.fill.id + elif cell.fill_type == 'distribmat': + xml_ids = [m.id if m is not None else None for m in xml_cell.fill] + model_ids = [m.id if m is not None else None for m in cell.fill] + assert xml_ids == model_ids + else: + pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}") + + +def test_dagmc_xml_reject_fill_override(): + mats = {'1': openmc.Material(1), 'void': None} + elem = ET.fromstring( + '' + '' + '' + ) + with pytest.raises(ValueError, match="cannot specify 'fill'"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_reject_region_override(): + mats = {'1': openmc.Material(1), 'void': None} + elem = ET.fromstring( + '' + '' + '' + ) + with pytest.raises(ValueError, match="cannot specify 'region'"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def _legacy_xml(cell_overrides): + """Helper to build a with old-format .""" + inner = ''.join( + f'{mids}' + for cid, mids in cell_overrides.items() + ) + return ET.fromstring( + f'' + f'{inner}' + f'' + ) + + +def test_dagmc_xml_legacy_single_material_compat(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning, match="deprecated"): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert 3 in univ.cells + assert univ.cells[3].fill is mat + + +def test_dagmc_xml_legacy_distribmat_compat(): + mat1, mat2 = openmc.Material(2), openmc.Material(3) + mats = {'2': mat1, '3': mat2, 'void': None} + elem = _legacy_xml({5: '2 3'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert univ.cells[5].fill_type == 'distribmat' + assert list(univ.cells[5].fill) == [mat1, mat2] + + +def test_dagmc_xml_legacy_void_compat(): + mats = {'void': None} + elem = _legacy_xml({7: 'void'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert univ.cells[7].fill_type == 'void' + + +def test_dagmc_xml_legacy_both_raises(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = ET.fromstring( + '' + '' + '1' + '' + '' + '' + ) + with pytest.raises(ValueError, match="both"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_legacy_deprecation_warning(): + mats = {'1': openmc.Material(1), 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_legacy_roundtrip(): + """Old-format XML loads correctly and re-exports using the new format.""" + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + + root = ET.Element('geometry') + univ.create_xml_subelement(root) + dagmc_elem = root.find('dagmc_universe') + + assert dagmc_elem.find('material_overrides') is None + cell_elems = dagmc_elem.findall('cell') + assert len(cell_elems) == 1 + assert int(cell_elems[0].get('id')) == 3 + assert cell_elems[0].get('material') == '1' + + +def test_dagmc_xml_temperature_roundtrip(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + + elem = ET.fromstring( + '' + '' + '' + ) + + dag_univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert dag_univ.cells[7].fill.id == 1 + assert dag_univ.cells[7].temperature == pytest.approx(825.0) + + root = ET.Element('geometry') + dag_univ.create_xml_subelement(root) + dagmc_elem = root.find('dagmc_universe') + xml_cell = dagmc_elem.find('cell') + assert xml_cell.get('temperature') == '825.0' + + dag_univ_roundtrip = openmc.DAGMCUniverse.from_xml_element(dagmc_elem, mats) + assert dag_univ_roundtrip.cells[7].fill.id == 1 + assert dag_univ_roundtrip.cells[7].temperature == pytest.approx(825.0) diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 2baa59f1bf..8d39c071b7 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -374,6 +374,49 @@ def test_rotation_from_xml(rotation): np.testing.assert_allclose(new_cell.rotation, cell.rotation) +def test_dagmccell_from_xml_element(): + """DAGMCCell.from_xml_element parses material, temperature, density, + and volume; rejects unsupported attributes.""" + mat = openmc.Material(1) + mat.add_nuclide('U235', 1.0) + mats = {'1': mat} + # In practice, from_xml_element is always called from DAGMCUniverse + # during XML parsing. A placeholder universe is used here so the test + # can exercise the method directly without a real DAGMC model file. + placeholder_univ = openmc.DAGMCUniverse('model.h5m') + + # material + temperature + density round-trip + xml = '' + cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + assert cell.id == 5 + assert cell.name == 'fuel' + assert cell.fill is mat + assert cell.density == 10.5 + assert cell.temperature == 900.0 + + # volume round-trip + xml = '' + cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + assert cell.volume == 42.0 + + # forbidden: region, fill, universe + for tag, val in [('region', '-1'), ('fill', '2'), ('universe', '0')]: + xml = f'' + with pytest.raises(ValueError, match=tag): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + # forbidden: translation, rotation + for tag, val in [('translation', '1 0 0'), ('rotation', '0 0 90')]: + xml = f'' + with pytest.raises(ValueError, match=tag): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + # missing material raises + xml = '' + with pytest.raises(ValueError, match='material'): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + def test_plot(run_in_tmpdir): zcyl = openmc.ZCylinder() c = openmc.Cell(region=-zcyl) diff --git a/tests/unit_tests/test_collision_track.py b/tests/unit_tests/test_collision_track.py index 25344a3bd0..96676e2cd5 100644 --- a/tests/unit_tests/test_collision_track.py +++ b/tests/unit_tests/test_collision_track.py @@ -34,6 +34,7 @@ def geometry(): {"max_collisions": 200, "mcpl": True} ], + ids=str ) def test_xml_serialization(parameter, run_in_tmpdir): """Check that the different use cases can be written and read in XML.""" @@ -45,7 +46,7 @@ def test_xml_serialization(parameter, run_in_tmpdir): assert read_settings.collision_track == parameter -@pytest.fixture(scope="module") +@pytest.fixture def model(): """Simple hydrogen sphere divided in two hemispheres by a z-plane to form 2 cells.""" @@ -127,3 +128,43 @@ def test_format_similarity(run_in_tmpdir, model): np.testing.assert_allclose(data_h5, data_mcpl, rtol=1e-05) # tolerance not that low due to the strings that is saved in MCPL, # not enough precision! + + +def test_photon_particles(run_in_tmpdir, model): + """Test that the collision track can be used to track photon particles.""" + model.settings.collision_track = {"max_collisions": 200, "cell_ids": [1, 2]} + + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box(*model.geometry.bounding_box), + energy=openmc.stats.delta_function(1e5), + particle='photon' + ) + model.run() + + with h5py.File("collision_track.h5", "r") as f: + source = f["collision_track_bank"] + + assert len(source) < 200 + + allowed_particles = (openmc.ParticleType.PHOTON, openmc.ParticleType.ELECTRON) + + for point in source: + particle_type = openmc.ParticleType(point['particle']) + assert particle_type in allowed_particles + + if particle_type == openmc.ParticleType.ELECTRON: + assert point['nuclide_id'] == 0 + + +def test_collision_track_two_threads(model, run_in_tmpdir): + # This test checks that the `max_collisions` setting is honored: + # no collisions beyond the specified limit should be recorded. + # + # The exact set of events in the capped bank is not reproducible with + # multiple threads because the bank stores whichever thread appends first + # until capacity is reached. + model.settings.collision_track = {"max_collisions": 200} + model.run(threads=2, particles=500) + + collision_track = openmc.read_collision_track_hdf5("collision_track.h5") + assert len(collision_track) == 200 diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py index 8f3b62f400..49c1b30499 100644 --- a/tests/unit_tests/test_d1s.py +++ b/tests/unit_tests/test_d1s.py @@ -150,3 +150,7 @@ def test_apply_time_correction(run_in_tmpdir): result_summed.get_reshaped_data() result.get_pandas_dataframe() result_summed.get_pandas_dataframe() + + # The summed tally is derived, so sum/sum_sq are None + assert result_summed.sum is None + assert result_summed.sum_sq is None diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index de8d90a434..a48c553255 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -100,6 +100,28 @@ def test_fpy(u235_yields): ufloat_close(thermal['I135'], ufloat(0.0292737, 0.000819663)) +def test_decay_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'decay', 'dec-041_Nb_090.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.Decay.from_endf(material) + + assert data.nuclide['name'] == 'Nb90' + assert not data.nuclide['stable'] + assert len(data.modes) == 2 + + +def test_fpy_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'nfy', 'nfy-092_U_235.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.FissionProductYields.from_endf(material) + + assert data.nuclide['name'] == 'U235' + assert data.energies == pytest.approx([0.0253, 500.e3, 1.4e7]) + assert 'I135' in data.cumulative[0] + + def test_sources(ba137m, nb90): # Running .sources twice should give same objects sources = ba137m.sources diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 4f18800143..32be5eb89c 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -34,6 +34,20 @@ def test_dose_coefficients(): assert energy[-1] == approx(20.0e6) assert dose[-1] == approx(338.0) + energy, dose = dose_coefficients( + 'neutron', data_source='icrp74', dose_quantity='ambient') + assert energy[0] == approx(1e-3) + assert dose[0] == approx(6.60) + assert energy[-1] == approx(20.0e6) + assert dose[-1] == approx(600) + + energy, dose = dose_coefficients( + 'photon', data_source='icrp74', dose_quantity='ambient') + assert energy[0] == approx(0.01e6) + assert dose[0] == approx(0.061) + assert energy[-1] == approx(10e6) + assert dose[-1] == approx(25.6) + # Invalid particle/geometry should raise an exception with raises(ValueError): dose_coefficients('slime', 'LAT') @@ -41,6 +55,14 @@ def test_dose_coefficients(): dose_coefficients('neutron', 'ZZ') with raises(ValueError): dose_coefficients('neutron', data_source='icrp7000') + with raises(ValueError): + dose_coefficients('neutron', dose_quantity='banana') + with raises(ValueError): + dose_coefficients( + 'neutron', data_source='icrp116', dose_quantity='ambient') + with raises(ValueError): + dose_coefficients( + 'neutron', 'ISO', data_source='icrp74', dose_quantity='ambient') with raises(ValueError) as excinfo: dose_coefficients("photons", data_source="icrp116") expected_particles = [ @@ -57,7 +79,8 @@ def test_dose_coefficients(): "proton", ] expected_msg = ( - "'photons' has no dose data in data source icrp116. " - f"Available particles for icrp116 are: {expected_particles}" + "'photons' has no effective dose data in data source icrp116. " + "Available particles for icrp116 with dose quantity effective are: " + f"{expected_particles}" ) assert str(excinfo.value) == expected_msg diff --git a/tests/unit_tests/test_data_neutron.py b/tests/unit_tests/test_data_neutron.py index d43d93ae51..c767f01712 100644 --- a/tests/unit_tests/test_data_neutron.py +++ b/tests/unit_tests/test_data_neutron.py @@ -133,6 +133,28 @@ def test_attributes(pu239): assert pu239.atomic_weight_ratio == pytest.approx(236.9986) +def test_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.IncidentNeutron.from_endf(material) + + assert data.name == 'H1' + assert data.atomic_number == 1 + assert data.mass_number == 1 + assert 2 in data.reactions + + +def test_fission_energy_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'neutrons', 'n-092_U_235.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + neutron = openmc.data.IncidentNeutron.from_endf(material) + + data = openmc.data.FissionEnergyRelease.from_endf(material, neutron) + + assert data.fragments(0.0) > 0.0 + + def test_fission_energy(pu239): fer = pu239.fission_energy assert isinstance(fer, openmc.data.FissionEnergyRelease) diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index 98b180f525..aceb82c3ab 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -149,3 +149,27 @@ def test_photodat_only(run_in_tmpdir, endf_data): photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' data = openmc.data.IncidentPhoton.from_endf(photoatomic_file) data.export_to_hdf5('tmp.h5', 'w') + + +def test_from_endf_material(endf_data): + endf_dir = Path(endf_data) + photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' + relaxation_file = endf_dir / 'atomic_relax' / 'atom-001_H_000.endf' + photoatomic = openmc.data.endf.get_evaluations(photoatomic_file)[0] + relaxation = openmc.data.endf.get_evaluations(relaxation_file)[0] + + data = openmc.data.IncidentPhoton.from_endf(photoatomic, relaxation) + + assert data.atomic_number == 1 + assert 502 in data.reactions + assert data.atomic_relaxation.binding_energy['K'] == pytest.approx(13.61) + + +def test_atomic_relaxation_from_endf_material(endf_data): + filename = Path(endf_data) / 'atomic_relax' / 'atom-001_H_000.endf' + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.AtomicRelaxation.from_endf(material) + + assert data.binding_energy['K'] == pytest.approx(13.61) + assert data.num_electrons['K'] == pytest.approx(1.0) diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index 7cd63ca712..81ed42b413 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -148,6 +148,18 @@ def test_h2o_endf(endf_data): '600K', '650K', '800K'] +def test_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + h2o = openmc.data.ThermalScattering.from_endf( + material, divide_incoherent_elastic=True) + + assert not h2o.elastic + assert h2o.atomic_weight_ratio == pytest.approx(0.99917) + assert h2o.temperatures[0] == '294K' + + def test_hzrh_attributes(hzrh): assert hzrh.atomic_weight_ratio == pytest.approx(0.99917) assert hzrh.energy_max == pytest.approx(1.9734) diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 26529e6ce9..0b1937facd 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -179,6 +179,66 @@ def test_hybrid_tally_setup(): assert ef.values[0] == pytest.approx(energies[0]) assert ef.values[-1] == pytest.approx(energies[-1]) + +def _simple_model(): + model = openmc.Model() + mat = openmc.Material(components={'H1': 1.0, 'H2': 1.0}, + density=5.0, density_units='g/cm3') + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.run_mode = 'fixed source' + return model, mat + + +def test_hybrid_tally_defaults_to_all_nuclides(run_in_tmpdir): + energies = [0., 0.625, 2.0e7] + kwargs = { + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)', '(n,gamma)'], + 'energies': energies, + 'reaction_rate_mode': 'flux', + 'chain_file': CHAIN_FILE, + } + + model, mat = _simple_model() + default_fluxes, default_micros = get_microxs_and_flux( + model, [mat], reaction_rate_opts={'reactions': ['(n,2n)']}, **kwargs + ) + + model, mat = _simple_model() + explicit_fluxes, explicit_micros = get_microxs_and_flux( + model, [mat], + reaction_rate_opts={ + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)'] + }, + **kwargs + ) + + np.testing.assert_allclose(default_fluxes[0], explicit_fluxes[0]) + np.testing.assert_allclose(default_micros[0].data, explicit_micros[0].data) + assert default_micros[0].nuclides == explicit_micros[0].nuclides + assert default_micros[0].reactions == explicit_micros[0].reactions + + +def test_flux_mode_returns_one_group_flux(run_in_tmpdir): + model, mat = _simple_model() + fluxes, micros = get_microxs_and_flux( + model, [mat], + nuclides=['H1'], + reactions=['(n,2n)'], + energies=[0., 0.625, 2.0e7], + reaction_rate_mode='flux', + chain_file=CHAIN_FILE, + ) + + assert fluxes[0].shape == (1,) + assert micros[0].data.shape == (1, 1, 1) + assert fluxes[0][0] > 0.0 + # --------------------------------------------------------------------------- # Tests for MicroXS.merge() # --------------------------------------------------------------------------- diff --git a/tests/unit_tests/test_endf.py b/tests/unit_tests/test_endf.py index 1d4982054c..cdd21d7a6b 100644 --- a/tests/unit_tests/test_endf.py +++ b/tests/unit_tests/test_endf.py @@ -2,6 +2,17 @@ from openmc.data import endf from pytest import approx +def test_evaluation_from_material(endf_data): + filename = f'{endf_data}/neutrons/n-001_H_001.endf' + material = endf.get_evaluations(filename)[0] + evaluation = endf.Evaluation(material) + + assert evaluation.material == material.MAT + assert evaluation.gnds_name == 'H1' + assert evaluation.section == material.section_text + assert evaluation.reaction_list == material[1, 451]['section_list'] + + def test_float_endf(): assert endf.float_endf('+3.2146') == approx(3.2146) assert endf.float_endf('.12345') == approx(0.12345) diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 51e648dcf9..f62306e834 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -251,9 +251,9 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" - assert m.depletable == False + assert not m.depletable m.depletable = True - assert m.depletable == True + assert m.depletable def test_properties_density(lib_init): @@ -895,22 +895,23 @@ def test_load_nuclide(lib_init): openmc.lib.load_nuclide('Pu3') +class LegacySlicePlot: + origin = (0.0, 0.0, 0.0) + width = 1.26 + height = 1.26 + basis = 'xy' + h_res = 3 + v_res = 3 + level = -1 + + def test_id_map(lib_init): expected_ids = np.array([[(3, 0, 3), (2, 0, 2), (3, 0, 3)], [(2, 0, 2), (1, 0, 1), (2, 0, 2)], [(3, 0, 3), (2, 0, 2), (3, 0, 3)]], dtype='int32') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - ids = openmc.lib.plot.id_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + ids = openmc.lib.id_map(LegacySlicePlot()) assert np.array_equal(expected_ids, ids) @@ -920,17 +921,8 @@ def test_property_map(lib_init): [ (293.6, 6.55), (293.6, 10.29769), (293.6, 6.55)], [(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)]], dtype='float') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - properties = openmc.lib.plot.property_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + properties = openmc.lib.property_map(LegacySlicePlot()) assert np.allclose(expected_properties, properties, atol=1e-04) diff --git a/tests/unit_tests/test_materials.py b/tests/unit_tests/test_materials.py index 5a382b777d..6620402fdc 100644 --- a/tests/unit_tests/test_materials.py +++ b/tests/unit_tests/test_materials.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + import openmc from openmc.deplete import Chain @@ -78,3 +80,50 @@ def test_export_duplicate_materials_to_xml(run_in_tmpdir): materials_in = openmc.Materials.from_xml("materials.xml") assert len(materials_in) == 2 + + +def test_materials_deplete_length_mismatch(): + mats = openmc.Materials([openmc.Material()]) + + with pytest.raises(ValueError, match="multigroup_fluxes length"): + mats.deplete( + multigroup_fluxes=[], + energy_group_structures=["VITAMIN-J-42"], + timesteps=[1.0], + source_rates=1.0, + ) + + with pytest.raises(ValueError, match="energy_group_structures length"): + mats.deplete( + multigroup_fluxes=[[1.0]], + energy_group_structures=[], + timesteps=[1.0], + source_rates=1.0, + ) + + +def test_materials_deplete_missing_volume(monkeypatch): + mat = openmc.Material() + mat.add_nuclide("Ni58", 1.0) + mat.set_density("g/cm3", 7.87) + + mats = openmc.Materials([mat]) + + class DummySession: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + monkeypatch.setattr(openmc.lib, "TemporarySession", DummySession) + + chain = Path(__file__).parents[1] / "chain_ni.xml" + with pytest.raises(ValueError, match="has no volume"): + mats.deplete( + multigroup_fluxes=[[1.0]], + energy_group_structures=["VITAMIN-J-42"], + timesteps=[1.0], + source_rates=1.0, + chain_file=chain, + ) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 0b28bdfbe4..9b1469fc59 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,4 +1,4 @@ -from math import pi +from math import pi, sqrt from tempfile import TemporaryDirectory from pathlib import Path import itertools @@ -490,13 +490,28 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type): simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename) -@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.") -def test_write_vtkhdf(request, run_in_tmpdir): +vtkhdf_tests = [ + ( + Path("test_mesh_dagmc_tets.vtk"), + "moab" + ), + ( + Path("test_mesh_hexes.exo"), + "libmesh" + ) +] +@pytest.mark.parametrize('mesh_file, mesh_library', vtkhdf_tests) +def test_write_vtkhdf(mesh_file, mesh_library, request, run_in_tmpdir): """Performs a minimal UnstructuredMesh simulation, reads in the resulting statepoint file and writes the mesh data to vtk and vtkhdf files. It is necessary to read in the unstructured mesh from a statepoint file to ensure it has all the required attributes """ + if mesh_library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip("DAGMC not enabled.") + if mesh_library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip("LibMesh not enabled.") + model = openmc.Model() surf1 = openmc.Sphere(r=1000.0, boundary_type="vacuum") @@ -504,8 +519,8 @@ def test_write_vtkhdf(request, run_in_tmpdir): model.geometry = openmc.Geometry([cell1]) umesh = openmc.UnstructuredMesh( - request.path.parent / "test_mesh_dagmc_tets.vtk", - "moab", + request.path.parent / mesh_file, + mesh_library, mesh_id = 1 ) mesh_filter = openmc.MeshFilter(umesh) @@ -514,6 +529,8 @@ def test_write_vtkhdf(request, run_in_tmpdir): mesh_tally = openmc.Tally(name="test_tally") mesh_tally.filters = [mesh_filter] mesh_tally.scores = ["flux"] + if mesh_library == "libmesh": + mesh_tally.estimator = "collision" model.tallies = [mesh_tally] @@ -556,6 +573,26 @@ def test_write_vtkhdf(request, run_in_tmpdir): with h5py.File("test_mesh.vtkhdf", "r"): ... + import vtk + reader = vtk.vtkHDFReader() + reader.SetFileName("test_mesh.vtkhdf") + reader.Update() + + # Get mean from file and make sure it matches original data + num_elements = reader.GetOutput().GetNumberOfCells() + assert num_elements == umesh_from_sp.n_elements + + num_vertices = reader.GetOutput().GetNumberOfPoints() + assert num_vertices == umesh_from_sp.vertices.shape[0] + + arr = reader.GetOutput().GetCellData().GetArray("mean") + mean = np.array([arr.GetTuple1(i) for i in range(my_tally.mean.size)]) + np.testing.assert_almost_equal(mean, my_tally.mean.flatten()/umesh_from_sp.volumes) + + arr = reader.GetOutput().GetCellData().GetArray("std_dev") + std_dev = np.array([arr.GetTuple1(i) for i in range(my_tally.std_dev.size)]) + np.testing.assert_almost_equal(std_dev, my_tally.std_dev.flatten()/umesh_from_sp.volumes) + def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" @@ -1028,3 +1065,92 @@ def test_rectilinear_mesh_get_indices_at_coords(): mesh.get_indices_at_coords([0.5, -0.5, 110.]) with pytest.raises(ValueError): mesh.get_indices_at_coords([0.5, -20., 110.]) + + +def test_SphericalMesh_get_indices_at_coords(): + """Test get_indices_at_coords method for SphericalMesh""" + + # Basic mesh with default phi and theta grids (single angular bin) + mesh = openmc.SphericalMesh(r_grid=(0, 5, 10)) + + assert mesh.get_indices_at_coords([3, 0, 0]) == (0, 0, 0) + assert mesh.get_indices_at_coords([0, 0, 3]) == (0, 0, 0) + assert mesh.get_indices_at_coords([0, 0, -3]) == (0, 0, 0) + assert mesh.get_indices_at_coords([7, 0, 0]) == (1, 0, 0) + assert mesh.get_indices_at_coords([10, 0, 0]) == (1, 0, 0) + + # Out-of-bounds r + with pytest.raises(ValueError): + mesh.get_indices_at_coords([11, 0, 0]) + + mesh2 = openmc.SphericalMesh(r_grid=(2, 5, 10)) + with pytest.raises(ValueError): + mesh2.get_indices_at_coords([1, 0, 0]) + + # Multi-bin angular grids: use points clearly inside bins + mesh3 = openmc.SphericalMesh( + r_grid=(0, 5, 10), + theta_grid=(0, pi/4, pi/2, pi), + phi_grid=(0, pi/2, pi, 3*pi/2, 2*pi) + ) + + # Near z-axis: theta~0 -> bin 0 + assert mesh3.get_indices_at_coords([0.01, 0, 3]) == (0, 0, 0) + + # theta in (0, pi/4) -> bin 0: [1, 0, 2] theta=arccos(2/sqrt(5))~0.46 + assert mesh3.get_indices_at_coords([1, 0, 2]) == (0, 0, 0) + + # theta in (pi/4, pi/2) -> bin 1: [2, 0, 1] theta=arccos(1/sqrt(5))~1.107 + assert mesh3.get_indices_at_coords([2, 0, 1]) == (0, 1, 0) + + # theta in (pi/2, pi) -> bin 2: [1, 0, -2] theta=arccos(-2/sqrt(5))~2.034 + assert mesh3.get_indices_at_coords([1, 0, -2]) == (0, 2, 0) + + # phi in (pi/2, pi) -> bin 1: [-1, 1, 0.5] + assert mesh3.get_indices_at_coords([-1, 1, 0.5]) == (0, 1, 1) + + # phi in (pi, 3*pi/2) -> bin 2: [-1, -1, 0.5] + assert mesh3.get_indices_at_coords([-1, -1, 0.5]) == (0, 1, 2) + + # phi in (3*pi/2, 2*pi) -> bin 3: [1, -1, 0.5] + assert mesh3.get_indices_at_coords([1, -1, 0.5]) == (0, 1, 3) + + # Non-default origin + mesh4 = openmc.SphericalMesh( + r_grid=(0, 5, 10), + origin=(100, 200, 300) + ) + + assert mesh4.get_indices_at_coords([103, 200, 300]) == (0, 0, 0) + assert mesh4.get_indices_at_coords([100, 200, 307]) == (1, 0, 0) + + with pytest.raises(ValueError): + mesh4.get_indices_at_coords([111, 200, 300]) + + # Degenerate case: point at origin with r_grid starting at 0 + mesh5 = openmc.SphericalMesh(r_grid=(0, 5)) + assert mesh5.get_indices_at_coords([0, 0, 0]) == (0, 0, 0) + + # Out-of-bounds theta: restricted theta grid + mesh6 = openmc.SphericalMesh( + r_grid=(0, 10), + theta_grid=(0, pi/4) + ) + with pytest.raises(ValueError): + mesh6.get_indices_at_coords([5, 0, 0]) # theta=pi/2 > pi/4 + + # Out-of-bounds phi: restricted phi grid + mesh7 = openmc.SphericalMesh( + r_grid=(0, 10), + phi_grid=(0, pi/2) + ) + with pytest.raises(ValueError): + mesh7.get_indices_at_coords([-5, 0, 0]) # phi=pi > pi/2 + + # Diagonal point: verify r, theta, phi all computed correctly + r = 6.0 + val = r / sqrt(3) + result = mesh3.get_indices_at_coords([val, val, val]) + assert result[0] == 1 # r=6 in second bin [5, 10] + assert result[1] == 1 # theta=arccos(1/sqrt(3))~0.955, in (pi/4, pi/2) + assert result[2] == 0 # phi=pi/4, in [0, pi/2) diff --git a/tests/unit_tests/test_mesh_hexes.exo b/tests/unit_tests/test_mesh_hexes.exo new file mode 120000 index 0000000000..272da92a4a --- /dev/null +++ b/tests/unit_tests/test_mesh_hexes.exo @@ -0,0 +1 @@ +../regression_tests/unstructured_mesh/test_mesh_hexes.exo \ No newline at end of file diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 9234b2d272..028a83b0bd 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1038,3 +1038,32 @@ def test_id_map_to_rgb(): ) # Check that overlap region is green assert np.allclose(rgb_overlap[5:, 5:], [0.0, 1.0, 0.0]) + + +def test_convert_to_multigroup_preserves_material_names(run_in_tmpdir): + """convert_to_multigroup leaves the user's material names unchanged and keys + the MGXS library by a unique sanitised name + id, so distinct materials that + share a name do not collapse to a single cross section.""" + a = openmc.Material(name="Steel Plate #1") + a.add_element("Fe", 1.0) + a.set_density("g/cm3", 7.9) + b = openmc.Material(name="Steel Plate #1") # same name, distinct material + b.add_element("Fe", 1.0) + b.set_density("g/cm3", 7.9) + + s1 = openmc.Sphere(r=1.0) + s2 = openmc.Sphere(r=2.0, boundary_type="vacuum") + c1 = openmc.Cell(fill=a, region=-s1) + c2 = openmc.Cell(fill=b, region=+s1 & -s2) + model = openmc.Model(openmc.Geometry([c1, c2]), openmc.Materials([a, b])) + + # Pre-create the library so MGXS generation (and transport) is skipped. + Path("mgxs.h5").touch() + model.convert_to_multigroup(method="material_wise", mgxs_path="mgxs.h5") + + # User names are preserved, not sanitised or de-duplicated. + assert [m.name for m in model.materials] == ["Steel Plate #1", "Steel Plate #1"] + # Each material reads a unique, sanitised library entry (name + id). + macro = [m._macroscopic for m in model.materials] + assert macro == [f"Steel_Plate__1_{a.id}", f"Steel_Plate__1_{b.id}"] + assert len(set(macro)) == 2 diff --git a/tests/unit_tests/test_slice_data.py b/tests/unit_tests/test_slice_data.py new file mode 100644 index 0000000000..cc5fb04747 --- /dev/null +++ b/tests/unit_tests/test_slice_data.py @@ -0,0 +1,168 @@ +import numpy as np +import openmc +from openmc.examples import pwr_pin_cell + + +def test_slice_data_basic(run_in_tmpdir): + """Test basic slice_data functionality.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(100, 100), + basis='xy' + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (100, 100, 3) + assert geom_data.dtype == np.int32 + assert prop_data.shape == (100, 100, 2) + assert prop_data.dtype == np.float64 + + # Check we have valid geometry + assert np.any(geom_data[:, :, 0] >= 0) # Valid cell IDs + assert np.any(prop_data[:, :, 0] > 0) # Valid temperatures + + +def test_slice_data_no_properties(run_in_tmpdir): + """Test slice_data without property data.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + assert prop_data is None + + +def test_slice_data_with_filter(run_in_tmpdir): + """Test slice_data with a cell filter.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + include_properties=False + ) + + # With filter, should have 4 fields + assert geom_data.shape == (50, 50, 4) + + # Filter bin index should be populated where cells exist + filter_bins = geom_data[:, :, 3] + valid_cells = geom_data[:, :, 0] >= 0 + assert np.any(filter_bins[valid_cells] >= 0) + + +def test_slice_data_overlaps(run_in_tmpdir): + """Test slice_data with overlap detection.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + show_overlaps=True, + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + # Check for overlap markers (-3) if any exist + # Note: This test may pass without finding overlaps if geometry is correct + + +def test_slice_data_overlaps_with_filter(run_in_tmpdir): + """Test that overlaps don't overwrite filter bin data.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + show_overlaps=True, + include_properties=False + ) + + assert geom_data.shape == (50, 50, 4) + + # If any overlaps exist, verify filter bin is still valid (not -3) + overlap_pixels = geom_data[:, :, 0] == -3 + if np.any(overlap_pixels): + # Filter bins at overlap locations should NOT be -3 + filter_bins_at_overlaps = geom_data[overlap_pixels, 3] + assert not np.all(filter_bins_at_overlaps == -3), \ + "Filter bins should be preserved even where overlaps are detected" + + +def test_slice_data_different_bases(run_in_tmpdir): + """Test slice_data with different basis planes.""" + model = pwr_pin_cell() + + for basis in ['xy', 'xz', 'yz']: + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(25, 25), + basis=basis + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_oriented_spans(run_in_tmpdir): + """Test slice_data with oriented span vectors.""" + model = pwr_pin_cell() + + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + u_span=(1.0, 0.0, 0.0), + v_span=(0.0, 0.0, 1.0), + pixels=(25, 25) + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_level(run_in_tmpdir): + """Test slice_data with specific universe level.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + level=0, # Root universe only + include_properties=False + ) + + assert geom_data.shape == (50, 50, 3) + + +def test_id_map_reverted(run_in_tmpdir): + """Test that id_map returns 3D array without filter support.""" + model = pwr_pin_cell() + id_data = model.id_map( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + basis='xy' + ) + + # Should have 3 fields (cell_id, cell_instance, material_id) + assert id_data.shape == (50, 50, 3) + assert id_data.dtype == np.int32 + + # Check valid data + assert np.any(id_data[:, :, 0] >= 0) # Valid cell IDs diff --git a/tests/unit_tests/test_slice_data_overlap_info.py b/tests/unit_tests/test_slice_data_overlap_info.py new file mode 100644 index 0000000000..c8e4150020 --- /dev/null +++ b/tests/unit_tests/test_slice_data_overlap_info.py @@ -0,0 +1,87 @@ +import pytest +import numpy as np +import openmc +import openmc.lib + +# Sentinel value matching _OVERLAP in plotmodel.py and OVERLAP in plot.cpp +_OVERLAP = -3 + + +@pytest.fixture(scope='module') +def overlap_model(): + openmc.reset_auto_ids() + + # Three cylinders: cyl1 and cyl2 overlap near x=0, cyl2 and cyl3 overlap + # near x=4. This gives us two spatially distinct overlap regions in one model. + mat1 = openmc.Material(components={'H1': 1.0}) + mat2 = openmc.Material(components={'H1': 1.0}) + mat3 = openmc.Material(components={'H1': 1.0}) + + # cyl1 and cyl2 overlap on the left, cyl2 and cyl3 overlap on the right + cyl1 = openmc.ZCylinder(x0=-2.0, r=2.5) + cyl2 = openmc.ZCylinder(x0=0.0, r=2.5) + cyl3 = openmc.ZCylinder(x0=2.0, r=2.5) + boundary = openmc.Sphere(r=20.0, boundary_type='vacuum') + cell1 = openmc.Cell(region=-cyl1, fill=mat1) + cell2 = openmc.Cell(region=-cyl2, fill=mat2) + cell3 = openmc.Cell(region=-cyl3, fill=mat3) + cell_outside = openmc.Cell(region=+cyl1 & +cyl2 & +cyl3 & -boundary) + geometry = openmc.Geometry([cell1, cell2, cell3, cell_outside]) + + settings = openmc.Settings() + settings.run_mode = 'fixed source' + settings.particles = 100 + settings.batches = 1 + model = openmc.Model(geometry=geometry, settings=settings) + + with openmc.lib.TemporarySession(model, args=['-s', '1']): + yield + + +def run_slice(origin=(0.0, 0.0, 0.0), width=(10.0, 6.0), show_overlaps=True): + # Helper that runs a slice over a region covering both overlap zones + geom_data, _ = openmc.lib.slice_data( + origin=origin, + width=width, + basis='xy', + pixels=(100, 60), + show_overlaps=show_overlaps, + include_properties=False, + ) + return geom_data + + +def test_overlaps_enabled(overlap_model): + # Run a single slice with overlap detection enabled and check all + # expected properties in one pass. + geom_data = run_slice() + overlap_info, n = openmc.lib.slice_data_overlap_info() + mat_ids = geom_data[:, :, 2] + + # mat_ids should contain values more negative than _OVERLAP; RasterData + # encodes each unique overlap as OVERLAP - overlap_idx - 1 into slot 2. + assert np.any(mat_ids < _OVERLAP) + + # overlap_keys should have 2 entries for the two distinct overlapping + # cylinder pairs in this model. + assert n == 2, f"Expected exactly 2 overlap entries, got {n}" + + # Each entry is a (universe_id, cell1_id, cell2_id) triple; verify values. + for i in range(n): + universe_id = int(overlap_info[i * 3]) + cell1_id = int(overlap_info[i * 3 + 1]) + cell2_id = int(overlap_info[i * 3 + 2]) + assert universe_id == 1 + assert cell1_id in {1, 2, 3} + assert cell2_id in {1, 2, 3} + assert cell1_id != cell2_id + + +def test_overlaps_disabled(overlap_model): + # With show_overlaps=False, set_overlap is never called and overlap_keys + # is never written to, so the image and map should both be clean. + geom_data = run_slice(show_overlaps=False) + _, n = openmc.lib.slice_data_overlap_info() + + assert not np.any(geom_data[:, :, 2] < _OVERLAP) + assert n == 0 diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index 3e84fbe517..f19ea74a67 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -145,3 +145,27 @@ def test_source_file_transport(run_in_tmpdir): # Try running OpenMC model.run() + + +def test_source_file_photon_transport(run_in_tmpdir): + # Create a source file containing a photon. Note that photon_transport is + # not explicitly enabled in the settings -- it should be turned on + # automatically because the source file contains a photon. + particle = openmc.SourceParticle(E=1.0e6, particle='photon') + openmc.write_source_file([particle], 'photon_source.h5') + + # Create simple model to use the photon source file + model = openmc.Model() + al = openmc.Material() + al.add_element('Al', 1.0) + al.set_density('g/cm3', 2.7) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=al, region=-sph) + model.geometry = openmc.Geometry([cell]) + model.settings.source = openmc.FileSource(path='photon_source.h5') + model.settings.particles = 10 + model.settings.batches = 3 + model.settings.run_mode = 'fixed source' + + # Running OpenMC should succeed + model.run() diff --git a/tests/unit_tests/test_surface_flux.py b/tests/unit_tests/test_surface_flux.py index 4e067ffe23..e4419252ed 100644 --- a/tests/unit_tests/test_surface_flux.py +++ b/tests/unit_tests/test_surface_flux.py @@ -98,6 +98,50 @@ def test_surface_filter_flux_angled(two_cell_model, run_in_tmpdir): assert flux_mean == pytest.approx(1.0 / mu) +def test_surface_tally_during_lattice_crossing(run_in_tmpdir): + openmc.reset_auto_ids() + model = openmc.Model() + + xmin = openmc.XPlane(-1.0, boundary_type="vacuum") + xmax = openmc.XPlane(1.0, boundary_type="vacuum") + ymin = openmc.YPlane(-1.0, boundary_type="vacuum") + ymax = openmc.YPlane(1.0, boundary_type="vacuum") + zmin = openmc.ZPlane(-1.0, boundary_type="vacuum") + zmax = openmc.ZPlane(1.0, boundary_type="vacuum") + + inner_cell = openmc.Cell() + inner_univ = openmc.Universe(cells=[inner_cell]) + + tile_cell = openmc.Cell(fill=inner_univ) + tile_univ = openmc.Universe(cells=[tile_cell]) + + lattice = openmc.RectLattice() + lattice.lower_left = (-1.0, -1.0) + lattice.pitch = (1.0, 2.0) + lattice.universes = [[tile_univ, tile_univ]] + + root_cell = openmc.Cell( + fill=lattice, region=+xmin & -xmax & +ymin & -ymax & +zmin & -zmax) + model.geometry = openmc.Geometry([root_cell]) + + src = openmc.IndependentSource() + src.space = openmc.stats.Point((-0.5, 0.0, 0.0)) + src.angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 1 + model.settings.particles = 5 + model.settings.source = src + + current_tally = openmc.Tally() + current_tally.filters = [openmc.SurfaceFilter(xmax)] + current_tally.scores = ['current'] + model.tallies = [current_tally] + + model.run(apply_tally_results=True) + assert current_tally.mean.flat[0] == pytest.approx(1.0) + + def test_cellfrom_filter_flux_directional(two_cell_model, run_in_tmpdir): """SurfaceFilter + CellFromFilter + flux scores only the correct direction.""" model, xmid, cell1, cell2 = two_cell_model diff --git a/tests/unit_tests/test_tally.py b/tests/unit_tests/test_tally.py index 52647a36c7..7136a621b3 100644 --- a/tests/unit_tests/test_tally.py +++ b/tests/unit_tests/test_tally.py @@ -17,3 +17,47 @@ def test_tally_init_args(): assert tally.filters == [filter] assert tally.nuclides == ['U235'] assert tally.estimator == 'tracklength' + + +def test_heating_estimator_with_photon_transport(run_in_tmpdir): + """Test that a neutron-only heating tally uses the tracklength estimator + even when photon transport is enabled. + + Without a neutron particle filter, photon heating requires the collision + estimator (analog energy balance). But neutron heating has kerma + coefficients that support tracklength scoring, so a neutron-only tally + should keep the tracklength estimator. + """ + mat = openmc.Material() + mat.add_nuclide('Si28', 1.0) + mat.set_density('g/cm3', 2.3) + 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.settings.run_mode = 'fixed source' + model.settings.particles = 100 + model.settings.batches = 1 + model.settings.photon_transport = True + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point() + ) + + neutron_filter = openmc.ParticleFilter(['neutron']) + + # Neutron-only heating — should get tracklength estimator + t_neutron = openmc.Tally(name='heating_neutron') + t_neutron.filters = [neutron_filter] + t_neutron.scores = ['heating'] + + # No particle filter — should get collision estimator + t_all = openmc.Tally(name='heating_all') + t_all.scores = ['heating'] + + model.tallies = [t_neutron, t_all] + + sp_filename = model.run() + with openmc.StatePoint(sp_filename) as sp: + assert sp.tallies[t_neutron.id].estimator == 'tracklength' + assert sp.tallies[t_all.id].estimator == 'collision' diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index d6e509522f..efa203b510 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -122,7 +122,8 @@ def model(): return model -def test_weightwindows(model, wws): +@pytest.mark.parametrize("shared_secondary", [False, True]) +def test_weightwindows(model, wws, shared_secondary): ww_files = ('ww_n.txt', 'ww_p.txt') cwd = Path(__file__).parent.absolute() @@ -131,6 +132,7 @@ def test_weightwindows(model, wws): with cdtemp(filepaths): # run once with variance reduction off model.settings.weight_windows_on = False + model.settings.shared_secondary_bank = shared_secondary analog_sp = model.run() os.rename(analog_sp, 'statepoint.analog.h5') @@ -223,7 +225,8 @@ def test_lower_ww_bounds_shape(): assert ww.lower_ww_bounds.shape == (2, 3, 4, 1) -def test_photon_heating(run_in_tmpdir): +@pytest.mark.parametrize("shared_secondary", [False, True]) +def test_photon_heating(run_in_tmpdir, shared_secondary): water = openmc.Material() water.add_nuclide('H1', 1.0) water.add_nuclide('O16', 2.0) @@ -246,7 +249,8 @@ def test_photon_heating(run_in_tmpdir): model.settings.run_mode = 'fixed source' model.settings.batches = 5 - model.settings.particles = 100 + model.settings.particles = 101 + model.settings.shared_secondary_bank = shared_secondary tally = openmc.Tally() tally.scores = ['heating'] @@ -260,7 +264,11 @@ def test_photon_heating(run_in_tmpdir): with openmc.StatePoint(sp_file) as sp: tally_mean = sp.tallies[tally.id].mean - # these values should be nearly identical + # Note: Our current physics model actually does allow this tally to + # occasionally go slightly negative. However, larger bugs can + # make this more common. We have selected a particle count for + # this test that happens to produce no negative tallies for both + # the shared and non-shared secondary PRNG streams. assert np.all(tally_mean >= 0)