diff --git a/CMakeLists.txt b/CMakeLists.txt index 87b8789d101..27a34074c16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -352,6 +352,8 @@ list(APPEND libopenmc_SOURCES src/event.cpp src/file_utils.cpp src/finalize.cpp + src/gendf.cpp + src/gendf_parser.cpp src/geometry.cpp src/geometry_aux.cpp src/hdf5_interface.cpp diff --git a/bin/openmc b/bin/openmc new file mode 100755 index 00000000000..8d6dfab9426 Binary files /dev/null and b/bin/openmc differ diff --git a/geometry.xml b/geometry.xml new file mode 100644 index 00000000000..b5e29c3bbaa --- /dev/null +++ b/geometry.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 +
0.0 0.0 0.0
+ + 9 + 20 10 +19 3 11 + 8 4 +18 2 12 + 7 5 +17 6 13 + 16 14 + 15 + 29 + 40 30 +39 23 31 + 28 24 +38 22 32 + 27 25 +37 26 33 + 36 34 + 35 +
+
diff --git a/include/openmc/capi.h b/include/openmc/capi.h index d8041ef4149..ac2df4c70c1 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -275,6 +275,79 @@ int openmc_properties_export(const char* filename); // \return Error code int openmc_properties_import(const char* filename); +//============================================================================== +// GENDF Library Functions +//============================================================================== + +//! Create GENDF library instance +//! \param[in] library_path Path to GENDF library directory +//! \param[in] n_energy_bounds Number of energy boundaries +//! \param[in] energy_bounds Energy group boundaries in eV +//! \param[in] energy_structure_name Energy structure name (e.g., "CCFE-709") +//! \param[out] lib_id Library instance ID +//! \return Error code +int openmc_gendf_library_create(const char* library_path, int n_energy_bounds, + const double* energy_bounds, const char* energy_structure_name, + int32_t* lib_id); + +//! Free GENDF library instance +//! \param[in] lib_id Library ID +//! \return Error code +int openmc_gendf_library_free(int32_t lib_id); + +//! Get number of energy groups in GENDF library +//! \param[in] lib_id Library ID +//! \param[out] n_groups Number of groups +//! \return Error code +int openmc_gendf_library_get_n_groups(int32_t lib_id, int* n_groups); + +//! Get energy group boundaries from GENDF library +//! \param[in] lib_id Library ID +//! \param[out] bounds Pointer to energy bounds array +//! \param[out] n Number of boundaries +//! \return Error code +int openmc_gendf_library_get_energy_bounds( + int32_t lib_id, const double** bounds, int* n); + +//! Check if nuclide is available in GENDF library +//! \param[in] lib_id Library ID +//! \param[in] nuclide Nuclide name +//! \param[out] has Whether nuclide is available +//! \return Error code +int openmc_gendf_library_has_nuclide( + int32_t lib_id, const char* nuclide, bool* has); + +//! Get list of available nuclides in GENDF library +//! \param[in] lib_id Library ID +//! \param[out] nuclides Array of nuclide name strings (caller must free) +//! \param[out] n Number of nuclides +//! \return Error code +int openmc_gendf_library_available_nuclides( + int32_t lib_id, char*** nuclides, int* n); + +//! Get cross-section data for nuclide and reaction +//! \param[in] lib_id Library ID +//! \param[in] nuclide Nuclide name +//! \param[in] mt ENDF MT reaction number +//! \param[in] n_energy_bounds Number of energy boundaries +//! \param[in] energy_bounds Energy boundaries for validation +//! \param[out] xs_data Cross-section array (caller must free with +//! openmc_gendf_free_xs) \param[out] n_groups Number of groups returned +//! \return Error code +int openmc_gendf_get_xs(int32_t lib_id, const char* nuclide, int32_t mt, + int n_energy_bounds, const double* energy_bounds, double** xs_data, + int* n_groups); + +//! Free cross-section array allocated by openmc_gendf_get_xs +//! \param[in] xs_data Array to free +void openmc_gendf_free_xs(double* xs_data); + +//! Free nuclide array allocated by openmc_gendf_library_available_nuclides +//! \param[in] nuclides Array of nuclide names to free (may be NULL) +//! \param[in] n Number of nuclides in array +//! \note Safe to call with NULL pointer or n=0 +void openmc_gendf_free_nuclides(char** nuclides, int n); + // Error codes extern int OPENMC_E_UNASSIGNED; extern int OPENMC_E_ALLOCATE; diff --git a/include/openmc/gendf.h b/include/openmc/gendf.h new file mode 100644 index 00000000000..2ee501277c3 --- /dev/null +++ b/include/openmc/gendf.h @@ -0,0 +1,256 @@ +//! \file gendf.h +//! \brief GENDF (Group-averaged ENDF) cross-section library for depletion + +#ifndef OPENMC_GENDF_H +#define OPENMC_GENDF_H + +#include +#include +#include + +#include "openmc/memory.h" // for unique_ptr +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +// GENDF tolerance constants (H1 fix - unified across C++ and Python) +//============================================================================== + +//! Relative tolerance for energy boundary matching +constexpr double GENDF_RTOL_MATCH = 1.0e-6; + +//! Relative tolerance for energy boundary mismatch warnings +constexpr double GENDF_RTOL_WARN = 1.0e-4; + +//! Absolute tolerance for energy matching (only used when values are near zero) +constexpr double GENDF_ATOL = 0.0; + +//============================================================================== +// Parser validation options and results (G9 fix) +//============================================================================== + +//! Validation options for GENDF parsing +struct GENDFParserOptions { + bool validate_za {true}; //!< Check ZA is physically valid + bool validate_xs_positive {true}; //!< Check XS values are non-negative + bool warn_short_lines {true}; //!< Warn about skipped short lines + bool require_mf1_header {true}; //!< Require MF=1, MT=451 header + int min_file_lines {10}; //!< Minimum expected lines in file +}; + +//! Result from parsing with diagnostics +struct GENDFParseResult { + int za {0}; //!< Z*1000 + A + int zam {0}; //!< Z*1000 + A + isomeric state + std::unordered_map> xs_data; //!< MT -> cross-sections + std::unordered_map> energy_data; //!< MT -> energy boundaries + int lines_read {0}; //!< Total lines read + int lines_skipped {0}; //!< Lines skipped (too short) + int negative_xs_count {0}; //!< Count of negative XS clamped + vector warnings; //!< Warning messages + bool success {false}; //!< Parsing succeeded + std::string error_message; //!< Error message if failed +}; + +//============================================================================== +//! Material data from a single GENDF file (MF=3 cross-sections only) +//============================================================================== + +class GENDFMaterial { +public: + // Constructors + GENDFMaterial() = default; + + //! Construct material from GENDF file + //! \param[in] filename Path to GENDF .asc file + explicit GENDFMaterial(const std::string& filename); + + // Methods + + //! Load material data from GENDF file + //! \param[in] filename Path to GENDF .asc file + void load_from_file(const std::string& filename); + + //! Get cross-section data for specific MT number with energy-aware alignment + //! \param[in] mt ENDF MT reaction number + //! \param[in] n_groups Number of energy groups + //! \param[in] library_bounds Library energy group boundaries for threshold alignment + //! \return Vector of cross-section values (one per group) + vector get_xs(int mt, int n_groups, const vector& library_bounds) const; + + //! Check if MT reaction exists in this material + //! \param[in] mt ENDF MT reaction number + //! \return True if reaction exists + bool has_mt(int mt) const; + + //! Get energy boundaries for specific MT number + //! \param[in] mt ENDF MT reaction number + //! \return Vector of energy boundaries for this reaction + const vector& get_energies(int mt) const; + + //! Check if MT has energy data + //! \param[in] mt ENDF MT reaction number + //! \return True if energy data exists + bool has_energies(int mt) const; + + // Accessors + const std::string& nuclide_name() const { return nuclide_name_; } + int za() const { return za_; } //!< Z*1000 + A + int zam() const { return zam_; } //!< Z*1000 + A + isomeric state + +private: + // Data members + std::string nuclide_name_; //!< Nuclide name (e.g., "U235") + int za_ {0}; //!< Z*1000 + A + int zam_ {0}; //!< Z*1000 + A + isomeric state + + //! Cross-section data: map MT -> vector (one value per group) + std::unordered_map> xs_data_; + + //! Energy data: map MT -> vector (energy boundaries for threshold alignment) + std::unordered_map> energy_data_; + + // Parsing methods + + //! Parse GENDF file (MF=3 only for speed) + //! \param[in] filename Path to GENDF .asc file + void parse_mf3_only(const std::string& filename); +}; + +//============================================================================== +//! GENDF library manager with caching +//============================================================================== + +class GENDFLibrary { +public: + // Constructors + + //! Construct GENDF library + //! \param[in] library_path Path to directory containing GENDF .asc files + //! \param[in] energy_bounds Energy group boundaries in [eV] + //! \param[in] energy_structure_name Optional name of energy structure + explicit GENDFLibrary( + const std::string& library_path, + const vector& energy_bounds, + const std::string& energy_structure_name = ""); + + // Methods + + //! Get cross-section for nuclide and MT + //! \param[in] nuclide Nuclide name (e.g., "U235") + //! \param[in] mt ENDF MT reaction number + //! \param[in] energy_bounds Energy group boundaries in [eV] + //! \return Vector of cross-section values (one per group) + vector get_xs( + const std::string& nuclide, + int mt, + const vector& energy_bounds); + + //! Check if nuclide is available in library + //! \param[in] nuclide Nuclide name + //! \return True if nuclide file exists + bool has_nuclide(const std::string& nuclide) const; + + //! Get list of available nuclides + //! \return Vector of nuclide names + vector available_nuclides() const; + + // Accessors + const vector& energy_bounds() const { return energy_bounds_; } + int n_groups() const { return n_groups_; } + const std::string& energy_structure() const { return energy_structure_; } + const std::string& library_path() const { return library_path_; } + +private: + // Data members + std::string library_path_; //!< Path to GENDF library directory + std::string energy_structure_; //!< Energy group structure name + vector energy_bounds_; //!< Energy group boundaries in [eV] + int n_groups_ {0}; //!< Number of energy groups + + //! Material cache: map nuclide name -> GENDFMaterial + std::unordered_map> material_cache_; + + //! Mutex for thread-safe cache access (mutable for const method compatibility) + //! Uses shared_mutex for read/write locking: multiple readers OR single writer + mutable std::shared_mutex cache_mutex_; + + //! File index: map nuclide name -> file path + std::unordered_map file_index_; + + // Private methods + + //! Load material (with caching) + //! \param[in] nuclide Nuclide name + //! \return Reference to cached material + GENDFMaterial& load_material(const std::string& nuclide); + + //! Build file index by scanning library directory + void build_file_index(); + + //! Get file path for nuclide + //! \param[in] nuclide Nuclide name + //! \return Full path to GENDF file + std::string get_file_path(const std::string& nuclide) const; +}; + +//============================================================================== +// Global variables +//============================================================================== + +namespace data { + +//! Map of library ID -> GENDFLibrary instance +extern std::unordered_map> gendf_libraries; + +//! Next available library ID +extern int n_gendf_libraries; + +} // namespace data + +//============================================================================== +// Non-member functions +//============================================================================== + +//! Strip leading zeros from mass number in nuclide name +//! \param[in] name Nuclide name with possible leading zeros (e.g., "Al027") +//! \return Name with leading zeros stripped (e.g., "Al27") +std::string strip_mass_leading_zeros(const std::string& name); + +//! Convert GENDF filename stem to OpenMC nuclide name +//! Handles metastable suffixes: mg->_m1, ng->_m2, og->_m3, pg->_m4, qg->_m5, g->ground +//! \param[in] stem GENDF filename stem (e.g., "U235g", "Am242mg") +//! \return OpenMC nuclide name (e.g., "U235", "Am242_m1") +std::string convert_gendf_to_openmc_name(const std::string& stem); + +//! Parse GENDF file using MF=3-only parser (7.6x faster) +//! \param[in] filename Path to GENDF .asc file +//! \param[out] xs_data Map of MT -> vector (cross-sections) +//! \param[out] energy_data Map of MT -> vector (energy boundaries) +//! \param[out] za Z*1000 + A +//! \param[out] zam Z*1000 + A + isomeric state +void parse_gendf_mf3_only( + const std::string& filename, + std::unordered_map>& xs_data, + std::unordered_map>& energy_data, + int& za, + int& zam); + +//! Parse GENDF file with validation and diagnostics (G9 fix) +//! \param[in] filename Path to GENDF .asc file +//! \param[in] options Parser validation options +//! \return Parse result with diagnostics +GENDFParseResult parse_gendf_validated( + const std::string& filename, + const GENDFParserOptions& options = GENDFParserOptions{}); + +//! Validate ZA value is physically reasonable +//! \param[in] za Z*1000 + A value +//! \param[out] error Error message if invalid +//! \return True if valid +bool validate_za(int za, std::string& error); + +} // namespace openmc + +#endif // OPENMC_GENDF_H diff --git a/include/openmc/version.h b/include/openmc/version.h new file mode 100644 index 00000000000..1d254f1c2e0 --- /dev/null +++ b/include/openmc/version.h @@ -0,0 +1,21 @@ +#ifndef OPENMC_VERSION_H +#define OPENMC_VERSION_H + +#include "openmc/array.h" + +namespace openmc { + +// OpenMC major, minor, and release numbers +// clang-format off +constexpr int VERSION_MAJOR {0}; +constexpr int VERSION_MINOR {0}; +constexpr int VERSION_RELEASE {0}; +constexpr bool VERSION_DEV {false}; +constexpr const char* VERSION_COMMIT_COUNT = ""; +constexpr const char* VERSION_COMMIT_HASH = "e8d61dd7e9ce918e98ec0b024b5a8bdf46f6bc36"; +constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; +// clang-format on + +} // namespace openmc + +#endif // OPENMC_VERSION_H diff --git a/lib64/cmake/OpenMC/OpenMCConfig.cmake b/lib64/cmake/OpenMC/OpenMCConfig.cmake new file mode 100644 index 00000000000..93caffb2a7b --- /dev/null +++ b/lib64/cmake/OpenMC/OpenMCConfig.cmake @@ -0,0 +1,37 @@ +get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +# Compute the install prefix from this file's location +get_filename_component(_OPENMC_PREFIX "${OpenMC_CMAKE_DIR}/../../.." ABSOLUTE) + +find_package(fmt CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) +find_package(pugixml CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) +find_package(xtl CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) +find_package(xtensor CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) +if(OFF) + find_package(DAGMC REQUIRED HINTS ) +endif() + +if(OFF) + include(FindPkgConfig) + list(APPEND CMAKE_PREFIX_PATH ) + set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH True) + pkg_check_modules(LIBMESH REQUIRED >=1.7.0 IMPORTED_TARGET) +endif() + +find_package(PNG) + +if(NOT TARGET OpenMC::libopenmc) + include("${OpenMC_CMAKE_DIR}/OpenMCTargets.cmake") +endif() + +if(on) + find_package(MPI REQUIRED) +endif() + +if(ON) + find_package(OpenMP REQUIRED) +endif() + +if(OFF AND NOT ${DAGMC_BUILD_UWUW}) + message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") +endif() diff --git a/lib64/cmake/OpenMC/OpenMCConfigVersion.cmake b/lib64/cmake/OpenMC/OpenMCConfigVersion.cmake new file mode 100644 index 00000000000..dc7293852fe --- /dev/null +++ b/lib64/cmake/OpenMC/OpenMCConfigVersion.cmake @@ -0,0 +1,11 @@ +set(PACKAGE_VERSION "0.0.0") + +# 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/lib64/cmake/OpenMC/OpenMCTargets-relwithdebinfo.cmake b/lib64/cmake/OpenMC/OpenMCTargets-relwithdebinfo.cmake new file mode 100644 index 00000000000..d9b00607926 --- /dev/null +++ b/lib64/cmake/OpenMC/OpenMCTargets-relwithdebinfo.cmake @@ -0,0 +1,28 @@ +#---------------------------------------------------------------- +# Generated CMake target import file for configuration "RelWithDebInfo". +#---------------------------------------------------------------- + +# Commands may need to know the format version. +set(CMAKE_IMPORT_FILE_VERSION 1) + +# Import target "OpenMC::openmc" for configuration "RelWithDebInfo" +set_property(TARGET OpenMC::openmc APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO) +set_target_properties(OpenMC::openmc PROPERTIES + IMPORTED_LOCATION_RELWITHDEBINFO "${_IMPORT_PREFIX}/bin/openmc" + ) + +list(APPEND _cmake_import_check_targets OpenMC::openmc ) +list(APPEND _cmake_import_check_files_for_OpenMC::openmc "${_IMPORT_PREFIX}/bin/openmc" ) + +# Import target "OpenMC::libopenmc" for configuration "RelWithDebInfo" +set_property(TARGET OpenMC::libopenmc APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO) +set_target_properties(OpenMC::libopenmc PROPERTIES + IMPORTED_LOCATION_RELWITHDEBINFO "${_IMPORT_PREFIX}/lib64/libopenmc.so" + IMPORTED_SONAME_RELWITHDEBINFO "libopenmc.so" + ) + +list(APPEND _cmake_import_check_targets OpenMC::libopenmc ) +list(APPEND _cmake_import_check_files_for_OpenMC::libopenmc "${_IMPORT_PREFIX}/lib64/libopenmc.so" ) + +# Commands beyond this point should not need to know the version. +set(CMAKE_IMPORT_FILE_VERSION) diff --git a/lib64/cmake/OpenMC/OpenMCTargets.cmake b/lib64/cmake/OpenMC/OpenMCTargets.cmake new file mode 100644 index 00000000000..021e47148f4 --- /dev/null +++ b/lib64/cmake/OpenMC/OpenMCTargets.cmake @@ -0,0 +1,116 @@ +# Generated by CMake + +if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}" LESS 2.8) + message(FATAL_ERROR "CMake >= 2.8.12 required") +endif() +if(CMAKE_VERSION VERSION_LESS "2.8.12") + message(FATAL_ERROR "CMake >= 2.8.12 required") +endif() +cmake_policy(PUSH) +cmake_policy(VERSION 2.8.12...3.29) +#---------------------------------------------------------------- +# Generated CMake target import file. +#---------------------------------------------------------------- + +# Commands may need to know the format version. +set(CMAKE_IMPORT_FILE_VERSION 1) + +# Protect against multiple inclusion, which would fail when already imported targets are added once more. +set(_cmake_targets_defined "") +set(_cmake_targets_not_defined "") +set(_cmake_expected_targets "") +foreach(_cmake_expected_target IN ITEMS OpenMC::openmc OpenMC::libopenmc) + list(APPEND _cmake_expected_targets "${_cmake_expected_target}") + if(TARGET "${_cmake_expected_target}") + list(APPEND _cmake_targets_defined "${_cmake_expected_target}") + else() + list(APPEND _cmake_targets_not_defined "${_cmake_expected_target}") + endif() +endforeach() +unset(_cmake_expected_target) +if(_cmake_targets_defined STREQUAL _cmake_expected_targets) + unset(_cmake_targets_defined) + unset(_cmake_targets_not_defined) + unset(_cmake_expected_targets) + unset(CMAKE_IMPORT_FILE_VERSION) + cmake_policy(POP) + return() +endif() +if(NOT _cmake_targets_defined STREQUAL "") + string(REPLACE ";" ", " _cmake_targets_defined_text "${_cmake_targets_defined}") + string(REPLACE ";" ", " _cmake_targets_not_defined_text "${_cmake_targets_not_defined}") + message(FATAL_ERROR "Some (but not all) targets in this export set were already defined.\nTargets Defined: ${_cmake_targets_defined_text}\nTargets not yet defined: ${_cmake_targets_not_defined_text}\n") +endif() +unset(_cmake_targets_defined) +unset(_cmake_targets_not_defined) +unset(_cmake_expected_targets) + + +# Compute the installation prefix relative to this file. +get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +if(_IMPORT_PREFIX STREQUAL "/") + set(_IMPORT_PREFIX "") +endif() + +# Create imported target OpenMC::openmc +add_executable(OpenMC::openmc IMPORTED) + +set_target_properties(OpenMC::openmc PROPERTIES + INTERFACE_COMPILE_FEATURES "cxx_std_17" +) + +# Create imported target OpenMC::libopenmc +add_library(OpenMC::libopenmc SHARED IMPORTED) + +set_target_properties(OpenMC::libopenmc PROPERTIES + INTERFACE_COMPILE_DEFINITIONS "OPENMC_MPI" + INTERFACE_COMPILE_FEATURES "cxx_std_17" + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;/home/perry/Codes/HDF5/HDF5-1.10.9-Linux/HDF_Group/HDF5/1.10.9/include" + INTERFACE_LINK_LIBRARIES "hdf5::hdf5-shared;hdf5::hdf5_hl-shared;xtensor;fmt::fmt;dl;pugixml::pugixml;PNG::PNG;OpenMP::OpenMP_CXX;MPI::MPI_CXX" +) + +# Load information for each installed configuration. +file(GLOB _cmake_config_files "${CMAKE_CURRENT_LIST_DIR}/OpenMCTargets-*.cmake") +foreach(_cmake_config_file IN LISTS _cmake_config_files) + include("${_cmake_config_file}") +endforeach() +unset(_cmake_config_file) +unset(_cmake_config_files) + +# Cleanup temporary variables. +set(_IMPORT_PREFIX) + +# Loop over all imported files and verify that they actually exist +foreach(_cmake_target IN LISTS _cmake_import_check_targets) + if(CMAKE_VERSION VERSION_LESS "3.28" + OR NOT DEFINED _cmake_import_check_xcframework_for_${_cmake_target} + OR NOT IS_DIRECTORY "${_cmake_import_check_xcframework_for_${_cmake_target}}") + foreach(_cmake_file IN LISTS "_cmake_import_check_files_for_${_cmake_target}") + if(NOT EXISTS "${_cmake_file}") + message(FATAL_ERROR "The imported target \"${_cmake_target}\" references the file + \"${_cmake_file}\" +but this file does not exist. Possible reasons include: +* The file was deleted, renamed, or moved to another location. +* An install or uninstall procedure did not complete successfully. +* The installation package was faulty and contained + \"${CMAKE_CURRENT_LIST_FILE}\" +but not all the files it references. +") + endif() + endforeach() + endif() + unset(_cmake_file) + unset("_cmake_import_check_files_for_${_cmake_target}") +endforeach() +unset(_cmake_target) +unset(_cmake_import_check_targets) + +# This file does not depend on other imported targets which have +# been exported from the same project but in a separate export set. + +# Commands beyond this point should not need to know the version. +set(CMAKE_IMPORT_FILE_VERSION) +cmake_policy(POP) diff --git a/model.xml b/model.xml new file mode 100644 index 00000000000..e735a584cab --- /dev/null +++ b/model.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + fixed source + 2000 + 10 + + + + 0.0 1.25 2.5 3.75 5.0 + 0.0 0.3141592653589793 0.6283185307179586 0.9424777960769379 1.2566370614359172 1.5707963267948966 1.8849555921538759 2.199114857512855 2.5132741228718345 2.827433388230814 3.141592653589793 3.4557519189487724 3.7699111843077517 4.084070449666731 4.39822971502571 4.71238898038469 5.026548245743669 5.340707511102648 5.654866776461628 5.969026041820607 6.283185307179586 + -5.0 -4.0 -3.0 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0 5.0 + 0.0 5.001 0.0 + + + 1 + + + 1 + flux + collision + + + diff --git a/openmc/data/data.py b/openmc/data/data.py index 5ecadd37be0..6e37378bfc4 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -199,6 +199,7 @@ '(n,3np)': (-3, -1), '(n,n2p)': (-2, -2), '(n,npa)': (-5, -3), + "(n,n')": (0, 0), '(n,gamma)': (1, 0), '(n,p)': (0, -1), '(n,d)': (-1, -1), diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 65b59582cf8..7ac5a9dda4d 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -66,6 +66,7 @@ REACTION_MT = {name: mt for mt, name in REACTION_NAME.items()} REACTION_MT['fission'] = 18 +REACTION_MT["(n,n')"] = 4 FISSION_MTS = (18, 19, 20, 21, 38) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 052e2245963..0e2f11adfc3 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -5,12 +5,43 @@ A depletion front-end tool. """ +# Energy group structure constants for isomeric branching +# These must be defined before submodule imports to avoid circular import +CCFE_709_N_GROUPS = 709 +UKAEA_1102_N_GROUPS = 1102 + +ISOMERIC_ENERGY_STRUCTURES = { + 'CCFE-709': CCFE_709_N_GROUPS, + 'UKAEA-1102': UKAEA_1102_N_GROUPS +} + + +def get_energy_structure_name(n_groups): + """Get energy structure name from number of groups. + + Parameters + ---------- + n_groups : int + Number of energy groups + + Returns + ------- + str or None + 'CCFE-709', 'UKAEA-1102', or None if unknown + """ + for name, count in ISOMERIC_ENERGY_STRUCTURES.items(): + if n_groups == count: + return name + return None + + from .nuclide import * from .chain import * from .openmc_operator import * from .coupled_operator import * from .independent_operator import * from .microxs import * +from .gendf import * from .reaction_rates import * from .atom_number import * from .stepresult import * diff --git a/openmc/deplete/_matrix_funcs.py b/openmc/deplete/_matrix_funcs.py index c7f7df76fbd..239e3bb0ad6 100644 --- a/openmc/deplete/_matrix_funcs.py +++ b/openmc/deplete/_matrix_funcs.py @@ -1,78 +1,78 @@ """Functions to form the special matrix for depletion""" -def celi_f1(chain, rates, fission_yields=None): - return (5 / 12 * chain.form_matrix(rates[0], fission_yields) - + 1 / 12 * chain.form_matrix(rates[1], fission_yields)) +def celi_f1(chain, rates, fission_yields=None, isomeric_branching=None): + return (5 / 12 * chain.form_matrix(rates[0], fission_yields, isomeric_branching) + + 1 / 12 * chain.form_matrix(rates[1], fission_yields, isomeric_branching)) -def celi_f2(chain, rates, fission_yields=None): - return (1 / 12 * chain.form_matrix(rates[0], fission_yields) - + 5 / 12 * chain.form_matrix(rates[1], fission_yields)) +def celi_f2(chain, rates, fission_yields=None, isomeric_branching=None): + return (1 / 12 * chain.form_matrix(rates[0], fission_yields, isomeric_branching) + + 5 / 12 * chain.form_matrix(rates[1], fission_yields, isomeric_branching)) -def cf4_f1(chain, rates, fission_yields=None): - return 1 / 2 * chain.form_matrix(rates, fission_yields) +def cf4_f1(chain, rates, fission_yields=None, isomeric_branching=None): + return 1 / 2 * chain.form_matrix(rates, fission_yields, isomeric_branching) -def cf4_f2(chain, rates, fission_yields=None): - return (-1 / 2 * chain.form_matrix(rates[0], fission_yields) - + chain.form_matrix(rates[1], fission_yields)) +def cf4_f2(chain, rates, fission_yields=None, isomeric_branching=None): + return (-1 / 2 * chain.form_matrix(rates[0], fission_yields, isomeric_branching) + + chain.form_matrix(rates[1], fission_yields, isomeric_branching)) -def cf4_f3(chain, rates, fission_yields=None): - return (1 / 4 * chain.form_matrix(rates[0], fission_yields) - + 1 / 6 * chain.form_matrix(rates[1], fission_yields) - + 1 / 6 * chain.form_matrix(rates[2], fission_yields) - - 1 / 12 * chain.form_matrix(rates[3], fission_yields)) +def cf4_f3(chain, rates, fission_yields=None, isomeric_branching=None): + return (1 / 4 * chain.form_matrix(rates[0], fission_yields, isomeric_branching) + + 1 / 6 * chain.form_matrix(rates[1], fission_yields, isomeric_branching) + + 1 / 6 * chain.form_matrix(rates[2], fission_yields, isomeric_branching) + - 1 / 12 * chain.form_matrix(rates[3], fission_yields, isomeric_branching)) -def cf4_f4(chain, rates, fission_yields=None): - return (-1 / 12 * chain.form_matrix(rates[0], fission_yields) - + 1 / 6 * chain.form_matrix(rates[1], fission_yields) - + 1 / 6 * chain.form_matrix(rates[2], fission_yields) - + 1 / 4 * chain.form_matrix(rates[3], fission_yields)) +def cf4_f4(chain, rates, fission_yields=None, isomeric_branching=None): + return (-1 / 12 * chain.form_matrix(rates[0], fission_yields, isomeric_branching) + + 1 / 6 * chain.form_matrix(rates[1], fission_yields, isomeric_branching) + + 1 / 6 * chain.form_matrix(rates[2], fission_yields, isomeric_branching) + + 1 / 4 * chain.form_matrix(rates[3], fission_yields, isomeric_branching)) -def rk4_f1(chain, rates, fission_yields=None): - return 1 / 2 * chain.form_matrix(rates, fission_yields) +def rk4_f1(chain, rates, fission_yields=None, isomeric_branching=None): + return 1 / 2 * chain.form_matrix(rates, fission_yields, isomeric_branching) -def rk4_f4(chain, rates, fission_yields=None): - return (1 / 6 * chain.form_matrix(rates[0], fission_yields) - + 1 / 3 * chain.form_matrix(rates[1], fission_yields) - + 1 / 3 * chain.form_matrix(rates[2], fission_yields) - + 1 / 6 * chain.form_matrix(rates[3], fission_yields)) +def rk4_f4(chain, rates, fission_yields=None, isomeric_branching=None): + return (1 / 6 * chain.form_matrix(rates[0], fission_yields, isomeric_branching) + + 1 / 3 * chain.form_matrix(rates[1], fission_yields, isomeric_branching) + + 1 / 3 * chain.form_matrix(rates[2], fission_yields, isomeric_branching) + + 1 / 6 * chain.form_matrix(rates[3], fission_yields, isomeric_branching)) -def leqi_f1(chain, inputs, fission_yields): - f1 = chain.form_matrix(inputs[0], fission_yields) - f2 = chain.form_matrix(inputs[1], fission_yields) +def leqi_f1(chain, inputs, fission_yields=None, isomeric_branching=None): + f1 = chain.form_matrix(inputs[0], fission_yields, isomeric_branching) + f2 = chain.form_matrix(inputs[1], fission_yields, isomeric_branching) dt_l, dt = inputs[2], inputs[3] return -dt / (12 * dt_l) * f1 + (dt + 6 * dt_l) / (12 * dt_l) * f2 -def leqi_f2(chain, inputs, fission_yields=None): - f1 = chain.form_matrix(inputs[0], fission_yields) - f2 = chain.form_matrix(inputs[1], fission_yields) +def leqi_f2(chain, inputs, fission_yields=None, isomeric_branching=None): + f1 = chain.form_matrix(inputs[0], fission_yields, isomeric_branching) + f2 = chain.form_matrix(inputs[1], fission_yields, isomeric_branching) dt_l, dt = inputs[2], inputs[3] return -5 * dt / (12 * dt_l) * f1 + (5 * dt + 6 * dt_l) / (12 * dt_l) * f2 -def leqi_f3(chain, inputs, fission_yields=None): - f1 = chain.form_matrix(inputs[0], fission_yields) - f2 = chain.form_matrix(inputs[1], fission_yields) - f3 = chain.form_matrix(inputs[2], fission_yields) +def leqi_f3(chain, inputs, fission_yields=None, isomeric_branching=None): + f1 = chain.form_matrix(inputs[0], fission_yields, isomeric_branching) + f2 = chain.form_matrix(inputs[1], fission_yields, isomeric_branching) + f3 = chain.form_matrix(inputs[2], fission_yields, isomeric_branching) dt_l, dt = inputs[3], inputs[4] return (-dt ** 2 / (12 * dt_l * (dt + dt_l)) * f1 + (dt ** 2 + 6 * dt * dt_l + 5 * dt_l ** 2) / (12 * dt_l * (dt + dt_l)) * f2 + dt_l / (12 * (dt + dt_l)) * f3) -def leqi_f4(chain, inputs, fission_yields=None): - f1 = chain.form_matrix(inputs[0], fission_yields) - f2 = chain.form_matrix(inputs[1], fission_yields) - f3 = chain.form_matrix(inputs[2], fission_yields) +def leqi_f4(chain, inputs, fission_yields=None, isomeric_branching=None): + f1 = chain.form_matrix(inputs[0], fission_yields, isomeric_branching) + f2 = chain.form_matrix(inputs[1], fission_yields, isomeric_branching) + f3 = chain.form_matrix(inputs[2], fission_yields, isomeric_branching) dt_l, dt = inputs[3], inputs[4] return (-dt ** 2 / (12 * dt_l * (dt + dt_l)) * f1 + (dt ** 2 + 2 * dt * dt_l + dt_l ** 2) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index ee742f22bc5..6727c02b1c9 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -733,7 +733,7 @@ def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None): start = time.time() results = deplete( self._solver, self.chain, n, rates, dt, i, matrix_func, - self.transfer_rates, self.external_source_rates) + self.transfer_rates, self.external_source_rates, self.operator) return time.time() - start, results @abstractmethod @@ -871,6 +871,13 @@ def integrate( n = self.operator.initial_condition() t, self._i_res = self._get_start_data() + # Notify user if isomeric branching is enabled + if output and comm.rank == 0: + if (hasattr(self.operator, '_isomeric_branching') and + self.operator._isomeric_branching and + any(bool(d) for d in self.operator._isomeric_branching)): + print("[openmc.deplete] Depletion is using Isomeric Branching") + for i, (dt, source_rate) in enumerate(self): if output and comm.rank == 0: print(f"[openmc.deplete] t={t} s, dt={dt} s, source={source_rate}") diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index b24c048cc92..0c9e0470570 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -251,4 +251,4 @@ def set_density(self, total_density): """ for i, density_slice in enumerate(total_density): - self.set_mat_slice(i, density_slice) + self.set_mat_slice(i, density_slice) \ No newline at end of file diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index a835face72b..3261b740f1c 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -31,6 +31,7 @@ ReactionInfo = namedtuple('ReactionInfo', ('mts', 'secondaries')) REACTIONS = { + "(n,n')": ReactionInfo({4}, ()), '(n,2nd)': ReactionInfo({11}, ('H2',)), '(n,2n)': ReactionInfo(set(chain([16], range(875, 892))), ()), '(n,3n)': ReactionInfo({17}, ()), @@ -120,9 +121,192 @@ __all__ = ["Chain", "REACTIONS"] +def _parse_isomeric_state(nuclide: str) -> tuple: + """Parse nuclide into base name and isomeric level. + + Parameters + ---------- + nuclide : str + Nuclide name (e.g., 'Hf177', 'Hf177_m1', 'Hf177_m2') + + Returns + ------- + tuple + (base_name, isomeric_level) where level is 0 for ground, 1 for m1, etc. + + Examples + -------- + >>> _parse_isomeric_state('Hf177') + ('Hf177', 0) + >>> _parse_isomeric_state('Hf177_m1') + ('Hf177', 1) + >>> _parse_isomeric_state('Hf177_m2') + ('Hf177', 2) + """ + if '_m' in nuclide: + base, suffix = nuclide.rsplit('_m', 1) + try: + return base, int(suffix) + except ValueError: + # Malformed suffix, treat as ground state + return nuclide, 0 + return nuclide, 0 + + +def _load_isomeric_branching(root): + """Load isomeric branching data from XML root. + + Parameters + ---------- + root : xml.etree.ElementTree + Root of chain XML + + Returns + ------- + dict or None + Isomeric branching data or None if not present + """ + from openmc._xml import get_text + import numpy as np + + isomeric_data = {} + + for nuclide_elem in root.findall('nuclide'): + nuc_name = get_text(nuclide_elem, 'name') + if not nuc_name: + continue + + nuc_reactions = {} + + for reaction_elem in nuclide_elem.findall('reaction'): + rx_type = reaction_elem.get('type') + if not rx_type: + continue + + iso_elem = reaction_elem.find('isomeric_yields') + if iso_elem is None: + continue + + iso_type = iso_elem.get('type', 'energy_dependent') + + if iso_type == 'energy_dependent': + # Parse energies + energies_elem = iso_elem.find('energies') + if energies_elem is None or energies_elem.text is None: + continue + + try: + energies = np.array([float(e) for e in energies_elem.text.split()]) + except (ValueError, AttributeError): + continue + + # Parse targets + targets_elem = iso_elem.find('targets') + if targets_elem is None or targets_elem.text is None: + continue + targets = targets_elem.text.split() + + # Parse branching ratios + branching_elem = iso_elem.find('branching_ratios') + if branching_elem is None or branching_elem.text is None: + continue + + lines = [] + for line in branching_elem.text.strip().split('\n'): + line = line.strip() + if line and not line.startswith('