diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9293e319b42..5973c3cd48d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,6 @@ jobs: - name: Environment Variables run: | - echo "DAGMC_ROOT=$HOME/DAGMC" echo "OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml" >> $GITHUB_ENV echo "OPENMC_ENDF_DATA=$HOME/endf-b-vii.1" >> $GITHUB_ENV @@ -131,6 +130,11 @@ jobs: echo "$HOME/NJOY2016/build" >> $GITHUB_PATH $GITHUB_WORKSPACE/tools/ci/gha-install.sh + - name: display-config + shell: bash + run: | + openmc -v + - name: cache-xs uses: actions/cache@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index b4011434e78..575e45373ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,6 +510,14 @@ endif() if(OPENMC_USE_DAGMC) target_compile_definitions(libopenmc PRIVATE DAGMC) target_link_libraries(libopenmc dagmc-shared) + + if(OPENMC_USE_UWUW) + target_compile_definitions(libopenmc PRIVATE OPENMC_UWUW) + target_link_libraries(libopenmc uwuw-shared) + endif() +elseif(OPENMC_USE_UWUW) + set(OPENMC_USE_UWUW OFF) + message(FATAL_ERROR "DAGMC must be enabled when UWUW is enabled.") endif() if(OPENMC_USE_LIBMESH) @@ -546,11 +554,6 @@ if(OPENMC_USE_NCRYSTAL) target_link_libraries(libopenmc NCrystal::NCrystal) endif() -if (OPENMC_USE_UWUW) - target_compile_definitions(libopenmc PRIVATE UWUW) - target_link_libraries(libopenmc uwuw-shared) -endif() - #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/cmake/Modules/FindLIBMESH.cmake b/cmake/Modules/FindLIBMESH.cmake index 048dfc2a8b8..df9208c18b2 100644 --- a/cmake/Modules/FindLIBMESH.cmake +++ b/cmake/Modules/FindLIBMESH.cmake @@ -15,7 +15,7 @@ if(DEFINED ENV{METHOD}) endif() find_package(PkgConfig REQUIRED) -set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${LIBMESH_PC}") -set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH True) + +set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH TRUE) pkg_check_modules(LIBMESH REQUIRED ${LIBMESH_PC_FILE}>=1.7.0 IMPORTED_TARGET) pkg_get_variable(LIBMESH_PREFIX ${LIBMESH_PC_FILE} prefix) diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 44a5e0d5a3f..b3b901de427 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -39,6 +39,6 @@ if(@OPENMC_USE_MCPL@) find_package(MCPL REQUIRED) endif() -if(@OPENMC_USE_UWUW@) - find_package(UWUW 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() \ No newline at end of file diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index 21944018e7d..e7d6d320f1e 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -22,6 +22,7 @@ Composite Surfaces :nosignatures: :template: myclass.rst + openmc.model.ConicalFrustum openmc.model.CruciformPrism openmc.model.CylinderSector openmc.model.HexagonalPrism diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 3a3d02231ad..6f14ebfa51c 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -474,7 +474,7 @@ applied as universes in the OpenMC geometry file. A geometry represented entirely by a DAGMC geometry will contain only the DAGMC universe. Using a :class:`openmc.DAGMCUniverse` looks like the following:: - dag_univ = openmc.DAGMCUniverse(filename='dagmc.h5m') + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') geometry = openmc.Geometry(dag_univ) geometry.export_to_xml() @@ -495,13 +495,22 @@ It is important in these cases to understand the DAGMC model's position with respect to the CSG geometry. DAGMC geometries can be plotted with OpenMC to verify that the model matches one's expectations. -**Note:** DAGMC geometries used in OpenMC are currently required to be clean, -meaning that all surfaces have been `imprinted and merged -`_ successfully -and that the model is `watertight -`_. -Future implementations of DAGMC geometry will support small volume overlaps and -un-merged surfaces. +By default, when you specify a .h5m file for a :class:`~openmc.DAGMCUniverse` +instance, it will store the absolute path to the .h5m file. If you prefer to +store the relative path, you can set the ``'resolve_paths'`` configuration +variable:: + + openmc.config['resolve_paths'] = False + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + +.. note:: + DAGMC geometries used in OpenMC are currently required to be clean, + meaning that all surfaces have been `imprinted and merged + `_ successfully + and that the model is `watertight + `_. + Future implementations of DAGMC geometry will support small volume overlaps and + un-merged surfaces. Cell, Surface, and Material IDs ------------------------------- diff --git a/openmc/config.py b/openmc/config.py index b823d6b06b2..ab53ab61b5f 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -1,4 +1,5 @@ from collections.abc import MutableMapping +from contextlib import contextmanager import os from pathlib import Path import warnings @@ -11,7 +12,7 @@ class _Config(MutableMapping): def __init__(self, data=()): - self._mapping = {} + self._mapping = {'resolve_paths': True} self.update(data) def __getitem__(self, key): @@ -42,10 +43,12 @@ def __setitem__(self, key, value): # Reset photon source data since it relies on chain file _DECAY_PHOTON_ENERGY.clear() _DECAY_ENERGY.clear() + elif key == 'resolve_paths': + self._mapping[key] = value else: raise KeyError(f'Unrecognized config key: {key}. Acceptable keys ' - 'are "cross_sections", "mg_cross_sections" and ' - '"chain_file"') + 'are "cross_sections", "mg_cross_sections", ' + '"chain_file", and "resolve_paths".') def __iter__(self): return iter(self._mapping) @@ -61,6 +64,24 @@ def _set_path(self, key, value): if not p.exists(): warnings.warn(f"'{value}' does not exist.") + @contextmanager + def patch(self, key, value): + """Temporarily change a value in the configuration. + + Parameters + ---------- + key : str + Key to change + value : object + New value + """ + previous_value = self.get(key) + self[key] = value + yield + if previous_value is None: + del self[key] + else: + self[key] = previous_value def _default_config(): """Return default configuration""" diff --git a/openmc/dagmc.py b/openmc/dagmc.py index 8cc61785f2a..8e3c33b95f2 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -86,9 +86,9 @@ class DAGMCUniverse(openmc.UniverseBase): material name is found in the DAGMC file, the material will be replaced with the openmc.Material object in the value. """ - + def __init__(self, - filename, + filename: cv.PathLike, universe_id=None, name='', auto_geom_ids=False, @@ -179,9 +179,9 @@ def add_material_override(self, mat_name=None, cell_id=None, overrides=None): self.material_overrides[mat_name] = overrides @filename.setter - def filename(self, val): - cv.check_type('DAGMC filename', val, (Path, str)) - self._filename = val + def filename(self, val: cv.PathLike): + cv.check_type('DAGMC filename', val, cv.PathLike) + self._filename = input_path(val) @property def auto_geom_ids(self): @@ -241,8 +241,7 @@ def _n_geom_elements(self, geom_type): def decode_str_tag(tag_val): return tag_val.tobytes().decode().replace('\x00', '') - dagmc_filepath = Path(self.filename).resolve() - with h5py.File(dagmc_filepath) as dagmc_file: + with h5py.File(self.filename) as dagmc_file: category_data = dagmc_file['tstt/tags/CATEGORY/values'] category_strs = map(decode_str_tag, category_data) n = sum([v == geom_type.capitalize() for v in category_strs]) @@ -580,7 +579,7 @@ def boundingbox(self): warnings.warn("Bounding box is not available for cells in a DAGMC " "universe", Warning) return BoundingBox.infinite() - + def get_all_cells(self, memo=None): warnings.warn("get_all_cells is not available for cells in a DAGMC " "universe", Warning) diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/effective_dose/dose.py index ae981ee7dc8..c7f458d1c6c 100644 --- a/openmc/data/effective_dose/dose.py +++ b/openmc/data/effective_dose/dose.py @@ -2,40 +2,61 @@ import numpy as np -_FILES = ( - ('electron', 'electrons.txt'), - ('helium', 'helium_ions.txt'), - ('mu-', 'negative_muons.txt'), - ('pi-', 'negative_pions.txt'), - ('neutron', 'neutrons.txt'), - ('photon', 'photons.txt'), - ('photon kerma', 'photons_kerma.txt'), - ('mu+', 'positive_muons.txt'), - ('pi+', 'positive_pions.txt'), - ('positron', 'positrons.txt'), - ('proton', 'protons.txt') -) - -_DOSE_ICRP116 = {} - - -def _load_dose_icrp116(): - """Load effective dose tables from text files""" - for particle, filename in _FILES: - path = Path(__file__).parent / filename - data = np.loadtxt(path, skiprows=3, encoding='utf-8') - data[:, 0] *= 1e6 # Change energies to eV - _DOSE_ICRP116[particle] = data - - -def dose_coefficients(particle, geometry='AP'): - """Return effective dose conversion coefficients from ICRP-116 - - This function provides fluence (and air kerma) to effective dose conversion - coefficients for various types of external exposures based on values in - `ICRP Publication 116 `_. - Corrected values found in a correigendum are used rather than the values in - theoriginal report. +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', +} + +_DOSE_TABLES = {} + + +def _load_dose_icrp(data_source: str, particle: str): + """Load effective dose tables from text files. + + Parameters + ---------- + data_source : {'icrp74', 'icrp116'} + The dose conversion data source to use + particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} + Incident particle + + """ + path = Path(__file__).parent / _FILES[data_source, particle] + data = np.loadtxt(path, skiprows=3, encoding='utf-8') + data[:, 0] *= 1e6 # Change energies to eV + _DOSE_TABLES[data_source, particle] = data + + +def dose_coefficients(particle, geometry='AP', data_source='icrp116'): + """Return effective 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 + ` and `ICRP Publication 116 + `. + + For ICRP 74 data, the photon effective dose per fluence is determined by + multiplying the air kerma per fluence values (Table A.1) by the effective + dose per air kerma (Table A.17). The neutron effective dose per fluence is + found in Table A.41. For ICRP 116 data, the photon effective dose per + fluence is found in Table A.1 and the neutron effective dose per fluence is + found in Table A.5. Parameters ---------- @@ -44,6 +65,8 @@ def dose_coefficients(particle, geometry='AP'): geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} Irradiation geometry assumed. Refer to ICRP-116 (Section 3.2) for the meaning of the options here. + data_source : {'icrp74', 'icrp116'} + The data source for the effective dose conversion coefficients. Returns ------- @@ -54,19 +77,24 @@ def dose_coefficients(particle, geometry='AP'): 'photon kerma', the coefficients are given in [Sv/Gy]. """ - if not _DOSE_ICRP116: - _load_dose_icrp116() + + cv.check_value('geometry', geometry, {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'}) + cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) + + if (data_source, particle) not in _FILES: + raise ValueError(f"{particle} has no dose data in data source {data_source}.") + elif (data_source, particle) not in _DOSE_TABLES: + _load_dose_icrp(data_source, particle) # Get all data for selected particle - data = _DOSE_ICRP116.get(particle) - if data is None: - raise ValueError(f"{particle} has no effective dose data") + data = _DOSE_TABLES[data_source, particle] # Determine index for selected geometry if particle in ('neutron', 'photon', 'proton', 'photon kerma'): - index = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO').index(geometry) + columns = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') else: - index = ('AP', 'PA', 'ISO').index(geometry) + columns = ('AP', 'PA', 'ISO') + index = columns.index(geometry) # Pull out energy and dose from table energy = data[:, 0].copy() diff --git a/openmc/data/effective_dose/electrons.txt b/openmc/data/effective_dose/icrp116/electrons.txt similarity index 100% rename from openmc/data/effective_dose/electrons.txt rename to openmc/data/effective_dose/icrp116/electrons.txt diff --git a/openmc/data/effective_dose/helium_ions.txt b/openmc/data/effective_dose/icrp116/helium_ions.txt similarity index 100% rename from openmc/data/effective_dose/helium_ions.txt rename to openmc/data/effective_dose/icrp116/helium_ions.txt diff --git a/openmc/data/effective_dose/negative_muons.txt b/openmc/data/effective_dose/icrp116/negative_muons.txt similarity index 100% rename from openmc/data/effective_dose/negative_muons.txt rename to openmc/data/effective_dose/icrp116/negative_muons.txt diff --git a/openmc/data/effective_dose/negative_pions.txt b/openmc/data/effective_dose/icrp116/negative_pions.txt similarity index 100% rename from openmc/data/effective_dose/negative_pions.txt rename to openmc/data/effective_dose/icrp116/negative_pions.txt diff --git a/openmc/data/effective_dose/neutrons.txt b/openmc/data/effective_dose/icrp116/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/neutrons.txt rename to openmc/data/effective_dose/icrp116/neutrons.txt diff --git a/openmc/data/effective_dose/photons.txt b/openmc/data/effective_dose/icrp116/photons.txt similarity index 100% rename from openmc/data/effective_dose/photons.txt rename to openmc/data/effective_dose/icrp116/photons.txt diff --git a/openmc/data/effective_dose/photons_kerma.txt b/openmc/data/effective_dose/icrp116/photons_kerma.txt similarity index 100% rename from openmc/data/effective_dose/photons_kerma.txt rename to openmc/data/effective_dose/icrp116/photons_kerma.txt diff --git a/openmc/data/effective_dose/positive_muons.txt b/openmc/data/effective_dose/icrp116/positive_muons.txt similarity index 100% rename from openmc/data/effective_dose/positive_muons.txt rename to openmc/data/effective_dose/icrp116/positive_muons.txt diff --git a/openmc/data/effective_dose/positive_pions.txt b/openmc/data/effective_dose/icrp116/positive_pions.txt similarity index 100% rename from openmc/data/effective_dose/positive_pions.txt rename to openmc/data/effective_dose/icrp116/positive_pions.txt diff --git a/openmc/data/effective_dose/positrons.txt b/openmc/data/effective_dose/icrp116/positrons.txt similarity index 100% rename from openmc/data/effective_dose/positrons.txt rename to openmc/data/effective_dose/icrp116/positrons.txt diff --git a/openmc/data/effective_dose/protons.txt b/openmc/data/effective_dose/icrp116/protons.txt similarity index 100% rename from openmc/data/effective_dose/protons.txt rename to openmc/data/effective_dose/icrp116/protons.txt diff --git a/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py b/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py new file mode 100644 index 00000000000..f8e970137e2 --- /dev/null +++ b/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py @@ -0,0 +1,69 @@ +from prettytable import PrettyTable +import numpy as np + +# Data from Table A.1 (air kerma per fluence) +energy_a1 = np.array([ + 0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, + 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0 +]) +air_kerma = np.array([7.43, 3.12, 1.68, 0.721, 0.429, 0.323, 0.289, 0.307, 0.371, 0.599, 0.856, 1.38, + 1.89, 2.38, 2.84, 3.69, 4.47, 6.14, 7.55, 9.96, 12.1, 14.1, 16.1, 20.1, 24.0]) + +# Data from Table A.17 (effective dose per air kerma) +energy_a17 = np.array([ + 0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.15, 0.2, 0.3, + 0.4, 0.5, 0.6, 0.8, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0 +]) +dose_per_airkerma = { + 'AP': np.array([ + 0.00653, 0.0402, 0.122, 0.416, 0.788, 1.106, 1.308, 1.407, 1.433, 1.394, + 1.256, 1.173, 1.093, 1.056, 1.036, 1.024, 1.010, 1.003, 0.992, 0.993, + 0.993, 0.991, 0.990 + ]), + 'PA': np.array([ + 0.00248, 0.00586, 0.0181, 0.128, 0.370, 0.640, 0.846, 0.966, 1.019, + 1.030, 0.959, 0.915, 0.880, 0.871, 0.869, 0.870, 0.875, 0.880, 0.901, + 0.918, 0.924, 0.927, 0.929 + ]), + 'RLAT': np.array([ + 0.00172, 0.00549, 0.0151, 0.0782, 0.205, 0.345, 0.455, 0.522, 0.554, + 0.571, 0.551, 0.549, 0.557, 0.570, 0.585, 0.600, 0.628, 0.651, 0.728, + 0.796, 0.827, 0.846, 0.860 + ]), + 'LLAT': np.array([ + 0.00172, 0.00549, 0.0155, 0.0904, 0.241, 0.405, 0.528, 0.598, 0.628, + 0.641, 0.620, 0.615, 0.615, 0.623, 0.635, 0.648, 0.670, 0.691, 0.757, + 0.813, 0.836, 0.850, 0.859 + ]), + 'ROT': np.array([ + 0.00326, 0.0153, 0.0462, 0.191, 0.426, 0.661, 0.828, 0.924, 0.961, + 0.960, 0.892, 0.854, 0.824, 0.814, 0.812, 0.814, 0.821, 0.831, 0.871, + 0.909, 0.925, 0.934, 0.941 + ]), + 'ISO': np.array([ + 0.00271, 0.0123, 0.0362, 0.143, 0.326, 0.511, 0.642, 0.720, 0.749, + 0.748, 0.700, 0.679, 0.664, 0.667, 0.675, 0.684, 0.703, 0.719, 0.774, + 0.824, 0.846, 0.859, 0.868 + ]) +} + +# Interpolate air kerma onto energy grid for Table A.17 +air_kerma = np.interp(energy_a17, energy_a1, air_kerma) + +# Compute effective dose per fluence +dose_per_fluence = { + geometry: air_kerma * dose_per_airkerma + for geometry, dose_per_airkerma in dose_per_airkerma.items() +} + +# Create table +table = PrettyTable() +table.field_names = ['Energy (MeV)', 'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'] +table.float_format = '.7' +for i, energy in enumerate(energy_a17): + row = [energy] + for geometry in table.field_names[1:]: + row.append(dose_per_fluence[geometry][i]) + table.add_row(row) +print('Photons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries.\n') +print(table.get_string(border=False)) diff --git a/openmc/data/effective_dose/icrp74/neutrons.txt b/openmc/data/effective_dose/icrp74/neutrons.txt new file mode 100644 index 00000000000..14aab48bd19 --- /dev/null +++ b/openmc/data/effective_dose/icrp74/neutrons.txt @@ -0,0 +1,50 @@ +Neutrons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries. + +Energy (MeV) AP PA LLAT RLAT ROT ISO +1.00E-09 5.24 3.52 1.68 1.36 2.99 2.4 +1.00E-08 6.55 4.39 2.04 1.7 3.72 2.89 +2.50E-08 7.6 5.16 2.31 1.99 4.4 3.3 +1.00E-07 9.95 6.77 2.86 2.58 5.75 4.13 +2.00E-07 11.2 7.63 3.21 2.92 6.43 4.59 +5.00E-07 12.8 8.76 3.72 3.35 7.27 5.2 +1.00E-06 13.8 9.55 4.12 3.67 7.84 5.63 +2.00E-06 14.5 10.2 4.39 3.89 8.31 5.96 +5.00E-06 15 10.7 4.66 4.08 8.72 6.28 +1.00E-05 15.1 11 4.8 4.16 8.9 6.44 +2.00E-05 15.1 11.1 4.89 4.2 8.92 6.51 +5.00E-05 14.8 11.1 4.95 4.19 8.82 6.51 +1.00E-04 14.6 11 4.95 4.15 8.69 6.45 +2.00E-04 14.4 10.9 4.92 4.1 8.56 6.32 +5.00E-04 14.2 10.7 4.86 4.03 8.4 6.14 +1.00E-03 14.2 10.7 4.84 4 8.34 6.04 +2.00E-03 14.4 10.8 4.87 4 8.39 6.05 +5.00E-03 15.7 11.6 5.25 4.29 9.06 6.52 +1.00E-02 18.3 13.5 6.14 5.02 10.6 7.7 +2.00E-02 23.8 17.3 7.95 6.48 13.8 10.2 +3.00E-02 29 21 9.74 7.93 16.9 12.7 +5.00E-02 38.5 27.6 13.1 10.6 22.7 17.3 +7.00E-02 47.2 33.5 16.1 13.1 27.8 21.5 +1.00E-01 59.8 41.3 20.1 16.4 34.8 27.2 +1.50E-01 80.2 52.2 25.5 21.2 45.4 35.2 +2.00E-01 99 61.5 30.3 25.6 54.8 42.4 +3.00E-01 133 77.1 38.6 33.4 71.6 54.7 +5.00E-01 188 103 53.2 46.8 99.4 75 +7.00E-01 231 124 66.6 58.3 123 92.8 +9.00E-01 267 144 79.6 69.1 144 108 +1 282 154 86 74.5 154 116 +1.2 310 175 99.8 85.8 173 130 +2 383 247 153 129 234 178 +3 432 308 195 171 283 220 +4 458 345 224 198 315 250 +5 474 366 244 217 335 272 +6 483 380 261 232 348 282 +7 490 391 274 244 358 290 +8 494 399 285 253 366 297 +9 497 406 294 261 373 303 +1.00E+01 499 412 302 268 378 309 +1.20E+01 499 422 315 278 385 322 +1.40E+01 496 429 324 286 390 333 +1.50E+01 494 431 328 290 391 338 +1.60E+01 491 433 331 293 393 342 +1.80E+01 486 435 335 299 394 345 +2.00E+01 480 436 338 305 395 343 diff --git a/openmc/data/effective_dose/icrp74/photons.txt b/openmc/data/effective_dose/icrp74/photons.txt new file mode 100644 index 00000000000..1ce3d67e03e --- /dev/null +++ b/openmc/data/effective_dose/icrp74/photons.txt @@ -0,0 +1,26 @@ +Photons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries. + + Energy (MeV) AP PA LLAT RLAT ROT ISO + 0.0100000 0.0485179 0.0184264 0.0127796 0.0127796 0.0242218 0.0201353 + 0.0150000 0.1254240 0.0182832 0.0171288 0.0171288 0.0477360 0.0383760 + 0.0200000 0.2049600 0.0304080 0.0260400 0.0253680 0.0776160 0.0608160 + 0.0300000 0.2999360 0.0922880 0.0651784 0.0563822 0.1377110 0.1031030 + 0.0400000 0.3380520 0.1587300 0.1033890 0.0879450 0.1827540 0.1398540 + 0.0500000 0.3572380 0.2067200 0.1308150 0.1114350 0.2135030 0.1650530 + 0.0600000 0.3780120 0.2444940 0.1525920 0.1314950 0.2392920 0.1855380 + 0.0700000 0.4192860 0.2878680 0.1782040 0.1555560 0.2753520 0.2145600 + 0.0800000 0.4399310 0.3128330 0.1927960 0.1700780 0.2950270 0.2299430 + 0.1000000 0.5171740 0.3821300 0.2378110 0.2118410 0.3561600 0.2775080 + 0.1500000 0.7523440 0.5744410 0.3713800 0.3300490 0.5343080 0.4193000 + 0.2000000 1.0040880 0.7832400 0.5264400 0.4699440 0.7310240 0.5812240 + 0.3000000 1.5083400 1.2144000 0.8487000 0.7686600 1.1371200 0.9163200 + 0.4000000 1.9958400 1.6461900 1.1774700 1.0773000 1.5384600 1.2606300 + 0.5000000 2.4656800 2.0682200 1.5113000 1.3923000 1.9325600 1.6065000 + 0.6000000 2.9081600 2.4708000 1.8403200 1.7040000 2.3117600 1.9425600 + 0.8000000 3.7269000 3.2287500 2.4723000 2.3173200 3.0294900 2.5940700 + 1.0000000 4.4834100 3.9336000 3.0887700 2.9099700 3.7145700 3.2139300 + 2.0000000 7.4896000 6.8025500 5.7153500 5.4964000 6.5760500 5.8437000 + 4.0000000 12.0153000 11.1078000 9.8373000 9.6316000 10.9989000 9.9704000 + 6.0000000 15.9873000 14.8764000 13.4596000 13.3147000 14.8925000 13.6206000 + 8.0000000 19.9191000 18.6327000 17.0850000 17.0046000 18.7734000 17.2659000 + 10.0000000 23.7600000 22.2960000 20.6160000 20.6400000 22.5840000 20.8320000 diff --git a/openmc/material.py b/openmc/material.py index f550fd64900..1213ea669d4 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -5,6 +5,7 @@ from numbers import Real from pathlib import Path import re +import sys import warnings import lxml.etree as ET @@ -16,6 +17,7 @@ import openmc.checkvalue as cv from ._xml import clean_indentation, reorder_attributes from .mixin import IDManagerMixin +from .utility_funcs import input_path from openmc.checkvalue import PathLike from openmc.stats import Univariate, Discrete, Mixture from openmc.data.data import _get_element_symbol @@ -25,6 +27,9 @@ DENSITY_UNITS = ('g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3', 'sum', 'macro') +# Smallest normalized floating point number +_SMALLEST_NORMAL = sys.float_info.min + NuclideTuple = namedtuple('NuclideTuple', ['name', 'percent', 'percent_type']) @@ -321,7 +326,7 @@ def get_decay_photon_energy( probs = [] for nuc, atoms_per_bcm in self.get_nuclide_atom_densities().items(): source_per_atom = openmc.data.decay_photon_energy(nuc) - if source_per_atom is not None: + if source_per_atom is not None and atoms_per_bcm > 0.0: dists.append(source_per_atom) probs.append(1e24 * atoms_per_bcm * multiplier) @@ -334,6 +339,11 @@ def get_decay_photon_energy( if isinstance(combined, (Discrete, Mixture)): combined.clip(clip_tolerance, inplace=True) + # If clipping resulted in a single distribution within a mixture, pick + # out that single distribution + if isinstance(combined, Mixture) and len(combined.distribution) == 1: + combined = combined.distribution[0] + return combined @classmethod @@ -1338,10 +1348,16 @@ def _get_nuclide_xml(self, nuclide: NuclideTuple) -> ET.Element: xml_element = ET.Element("nuclide") xml_element.set("name", nuclide.name) + # Prevent subnormal numbers from being written to XML, which causes an + # exception on the C++ side when calling std::stod + val = nuclide.percent + if abs(val) < _SMALLEST_NORMAL: + val = 0.0 + if nuclide.percent_type == 'ao': - xml_element.set("ao", str(nuclide.percent)) + xml_element.set("ao", str(val)) else: - xml_element.set("wo", str(nuclide.percent)) + xml_element.set("wo", str(val)) return xml_element @@ -1643,7 +1659,7 @@ def cross_sections(self) -> Path | None: @cross_sections.setter def cross_sections(self, cross_sections): if cross_sections is not None: - self._cross_sections = Path(cross_sections) + self._cross_sections = input_path(cross_sections) def append(self, material): """Append material to collection diff --git a/openmc/mesh.py b/openmc/mesh.py index a706b8fa811..6afe5d36eea 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -5,8 +5,6 @@ from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real -from pathlib import Path -import tempfile import h5py import lxml.etree as ET @@ -19,6 +17,7 @@ from ._xml import get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES +from .utility_funcs import input_path class MeshBase(IDManagerMixin, ABC): @@ -2072,7 +2071,7 @@ class UnstructuredMesh(MeshBase): Parameters ---------- - filename : str or pathlib.Path + filename : path-like Location of the unstructured mesh file library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally @@ -2158,8 +2157,8 @@ def filename(self): @filename.setter def filename(self, filename): - cv.check_type('Unstructured Mesh filename', filename, (str, Path)) - self._filename = filename + cv.check_type('Unstructured Mesh filename', filename, PathLike) + self._filename = input_path(filename) @property def library(self): diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index a2cb0243849..df290329647 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -778,12 +778,6 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): def __neg__(self): return -self.cone & (+self.plane if self.up else -self.plane) - def __pos__(self): - if self.up: - return (+self.cone & +self.plane) | -self.plane - else: - return (+self.cone & -self.plane) | +self.plane - class YConeOneSided(CompositeSurface): """One-sided cone parallel the y-axis @@ -836,7 +830,6 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): self.up = up __neg__ = XConeOneSided.__neg__ - __pos__ = XConeOneSided.__pos__ class ZConeOneSided(CompositeSurface): @@ -890,7 +883,6 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): self.up = up __neg__ = XConeOneSided.__neg__ - __pos__ = XConeOneSided.__pos__ class Polygon(CompositeSurface): @@ -1725,3 +1717,123 @@ def __neg__(self) -> openmc.Region: prism &= ~corners return prism + + +def _rotation_matrix(v1, v2): + """Compute rotation matrix that would rotate v1 into v2. + + Parameters + ---------- + v1 : numpy.ndarray + Unrotated vector + v2 : numpy.ndarray + Rotated vector + + Returns + ------- + 3x3 rotation matrix + + """ + # Normalize vectors and compute cosine + u1 = v1 / np.linalg.norm(v1) + u2 = v2 / np.linalg.norm(v2) + cos_angle = np.dot(u1, u2) + + I = np.identity(3) + + # Handle special case where vectors are parallel or anti-parallel + if isclose(abs(cos_angle), 1.0, rel_tol=1e-8): + return np.sign(cos_angle)*I + else: + # Calculate rotation angle + sin_angle = np.sqrt(1 - cos_angle*cos_angle) + + # Calculate axis of rotation + axis = np.cross(u1, u2) + axis /= np.linalg.norm(axis) + + # Create cross-product matrix K + kx, ky, kz = axis + K = np.array([ + [0.0, -kz, ky], + [kz, 0.0, -kx], + [-ky, kx, 0.0] + ]) + + # Create rotation matrix using Rodrigues' rotation formula + return I + K * sin_angle + (K @ K) * (1 - cos_angle) + + +class ConicalFrustum(CompositeSurface): + """Conical frustum. + + A conical frustum, also known as a right truncated cone, is a cone that is + truncated by two parallel planes that are perpendicular to the axis of the + cone. The lower and upper base of the conical frustum are circular faces. + This surface is equivalent to the TRC macrobody in MCNP. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + center_base : iterable of float + Cartesian coordinates of the center of the bottom planar face. + axis : iterable of float + Vector from the center of the bottom planar face to the center of the + top planar face that defines the axis of the cone. The length of this + vector is the height of the conical frustum. + r1 : float + Radius of the lower cone base + r2 : float + Radius of the upper cone base + **kwargs + Keyword arguments passed to underlying plane classes + + Attributes + ---------- + cone : openmc.Cone + Cone surface + plane_bottom : openmc.Plane + Plane surface defining the bottom of the frustum + plane_top : openmc.Plane + Plane surface defining the top of the frustum + + """ + _surface_names = ('cone', 'plane_bottom', 'plane_top') + + def __init__(self, center_base: Sequence[float], axis: Sequence[float], + r1: float, r2: float, **kwargs): + center_base = np.array(center_base) + axis = np.array(axis) + + # Determine length of axis height vector + h = np.linalg.norm(axis) + + # To create the frustum oriented with the correct axis, first we will + # create a cone along the z axis and then rotate it according to the + # given axis. Thus, we first need to determine the apex using the z axis + # as a reference. + x0, y0, z0 = center_base + if r1 != r2: + apex = z0 + r1*h/(r1 - r2) + r_sq = ((r1 - r2)/h)**2 + cone = openmc.ZCone(x0, y0, apex, r2=r_sq, **kwargs) + else: + # In the degenerate case r1 == r2, the cone becomes a cylinder + cone = openmc.ZCylinder(x0, y0, r1, **kwargs) + + # Create the parallel planes + plane_bottom = openmc.ZPlane(z0, **kwargs) + plane_top = openmc.ZPlane(z0 + h, **kwargs) + + # Determine rotation matrix corresponding to specified axis + u = np.array([0., 0., 1.]) + rotation = _rotation_matrix(u, axis) + + # Rotate the surfaces + self.cone = cone.rotate(rotation, pivot=center_base) + self.plane_bottom = plane_bottom.rotate(rotation, pivot=center_base) + self.plane_top = plane_top.rotate(rotation, pivot=center_base) + + def __neg__(self) -> openmc.Region: + return +self.plane_bottom & -self.plane_top & -self.cone diff --git a/openmc/region.py b/openmc/region.py index e509b152805..e1cb834757a 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import MutableSequence from copy import deepcopy @@ -30,8 +31,9 @@ def __and__(self, other): def __or__(self, other): return Union((self, other)) - def __invert__(self): - return Complement(self) + @abstractmethod + def __invert__(self) -> Region: + pass @abstractmethod def __contains__(self, point): @@ -442,6 +444,9 @@ def __iand__(self, other): self.append(other) return self + def __invert__(self) -> Union: + return Union(~n for n in self) + # Implement mutable sequence protocol by delegating to list def __getitem__(self, key): return self._nodes[key] @@ -530,6 +535,9 @@ def __ior__(self, other): self.append(other) return self + def __invert__(self) -> Intersection: + return Intersection(~n for n in self) + # Implement mutable sequence protocol by delegating to list def __getitem__(self, key): return self._nodes[key] @@ -603,7 +611,7 @@ class Complement(Region): """ - def __init__(self, node): + def __init__(self, node: Region): self.node = node def __contains__(self, point): @@ -622,6 +630,9 @@ def __contains__(self, point): """ return point not in self.node + def __invert__(self) -> Region: + return self.node + def __str__(self): return '~' + str(self.node) @@ -637,18 +648,7 @@ def node(self, node): @property def bounding_box(self) -> BoundingBox: - # Use De Morgan's laws to distribute the complement operator so that it - # only applies to surface half-spaces, thus allowing us to calculate the - # bounding box in the usual recursive manner. - if isinstance(self.node, Union): - temp_region = Intersection(~n for n in self.node) - elif isinstance(self.node, Intersection): - temp_region = Union(~n for n in self.node) - elif isinstance(self.node, Complement): - temp_region = self.node.node - else: - temp_region = ~self.node - return temp_region.bounding_box + return (~self.node).bounding_box def get_surfaces(self, surfaces=None): """Recursively find and return all the surfaces referenced by the node diff --git a/openmc/settings.py b/openmc/settings.py index 96e6368e4f3..0a78fb564f8 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -8,12 +8,14 @@ import lxml.etree as ET import openmc.checkvalue as cv +from openmc.checkvalue import PathLike from openmc.stats.multivariate import MeshSpatial -from . import (RegularMesh, SourceBase, MeshSource, IndependentSource, - VolumeCalculation, WeightWindows, WeightWindowGenerator) from ._xml import clean_indentation, get_text, reorder_attributes -from openmc.checkvalue import PathLike -from .mesh import _read_meshes +from .mesh import _read_meshes, RegularMesh +from .source import SourceBase, MeshSource, IndependentSource +from .utility_funcs import input_path +from .volume import VolumeCalculation +from .weight_windows import WeightWindows, WeightWindowGenerator class RunMode(Enum): @@ -699,14 +701,18 @@ def surf_source_read(self) -> dict: return self._surf_source_read @surf_source_read.setter - def surf_source_read(self, surf_source_read: dict): - cv.check_type('surface source reading options', surf_source_read, Mapping) - for key, value in surf_source_read.items(): + def surf_source_read(self, ssr: dict): + cv.check_type('surface source reading options', ssr, Mapping) + for key, value in ssr.items(): cv.check_value('surface source reading key', key, ('path')) if key == 'path': - cv.check_type('path to surface source file', value, str) - self._surf_source_read = surf_source_read + cv.check_type('path to surface source file', value, PathLike) + self._surf_source_read = dict(ssr) + + # Resolve path to surface source file + if 'path' in ssr: + self._surf_source_read['path'] = input_path(ssr['path']) @property def surf_source_write(self) -> dict: @@ -1066,8 +1072,8 @@ def weight_windows_file(self) -> PathLike | None: @weight_windows_file.setter def weight_windows_file(self, value: PathLike): - cv.check_type('weight windows file', value, (str, Path)) - self._weight_windows_file = value + cv.check_type('weight windows file', value, PathLike) + self._weight_windows_file = input_path(value) @property def weight_window_generators(self) -> list[WeightWindowGenerator]: @@ -1241,7 +1247,7 @@ def _create_surf_source_read_subelement(self, root): element = ET.SubElement(root, "surf_source_read") if 'path' in self._surf_source_read: subelement = ET.SubElement(element, "path") - subelement.text = self._surf_source_read['path'] + subelement.text = str(self._surf_source_read['path']) def _create_surf_source_write_subelement(self, root): if self._surf_source_write: @@ -1501,7 +1507,7 @@ def _create_weight_window_generators_subelement(self, root, mesh_memo=None): def _create_weight_windows_file_element(self, root): if self.weight_windows_file is not None: element = ET.Element("weight_windows_file") - element.text = self.weight_windows_file + element.text = str(self.weight_windows_file) root.append(element) def _create_weight_window_checkpoints_subelement(self, root): @@ -1645,9 +1651,11 @@ def _sourcepoint_from_xml_element(self, root): def _surf_source_read_from_xml_element(self, root): elem = root.find('surf_source_read') if elem is not None: + ssr = {} value = get_text(elem, 'path') if value is not None: - self.surf_source_read['path'] = value + ssr['path'] = value + self.surf_source_read = ssr def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') diff --git a/openmc/source.py b/openmc/source.py index 7c8e6af8dab..878f52c3b22 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -3,6 +3,7 @@ from collections.abc import Iterable, Sequence from enum import IntEnum from numbers import Real +from pathlib import Path import warnings from typing import Any from pathlib import Path @@ -19,6 +20,7 @@ from openmc.stats.univariate import Univariate from ._xml import get_text from .mesh import MeshBase, StructuredMesh, UnstructuredMesh +from .utility_funcs import input_path class SourceBase(ABC): @@ -664,7 +666,7 @@ class CompiledSource(SourceBase): Parameters ---------- - library : str or None + library : path-like Path to a compiled shared library parameters : str Parameters to be provided to the compiled shared library function @@ -686,7 +688,7 @@ class CompiledSource(SourceBase): Attributes ---------- - library : str or None + library : pathlib.Path Path to a compiled shared library parameters : str Parameters to be provided to the compiled shared library function @@ -702,17 +704,13 @@ class CompiledSource(SourceBase): """ def __init__( self, - library: str | None = None, + library: PathLike, parameters: str | None = None, strength: float = 1.0, constraints: dict[str, Any] | None = None ) -> None: super().__init__(strength=strength, constraints=constraints) - - self._library = None - if library is not None: - self.library = library - + self.library = library self._parameters = None if parameters is not None: self.parameters = parameters @@ -722,13 +720,13 @@ def type(self) -> str: return "compiled" @property - def library(self) -> str: + def library(self) -> Path: return self._library @library.setter - def library(self, library_name): - cv.check_type('library', library_name, str) - self._library = library_name + def library(self, library_name: PathLike): + cv.check_type('library', library_name, PathLike) + self._library = input_path(library_name) @property def parameters(self) -> str: @@ -748,7 +746,7 @@ def populate_xml_element(self, element): XML element containing source data """ - element.set("library", self.library) + element.set("library", str(self.library)) if self.parameters is not None: element.set("parameters", self.parameters) @@ -794,7 +792,7 @@ class FileSource(SourceBase): Parameters ---------- - path : str or pathlib.Path + path : path-like Path to the source file from which sites should be sampled strength : float Strength of the source (default is 1.0) @@ -829,14 +827,12 @@ class FileSource(SourceBase): def __init__( self, - path: PathLike | None = None, + path: PathLike, strength: float = 1.0, constraints: dict[str, Any] | None = None ): super().__init__(strength=strength, constraints=constraints) - self._path = None - if path is not None: - self.path = path + self.path = path @property def type(self) -> str: @@ -848,8 +844,8 @@ def path(self) -> PathLike: @path.setter def path(self, p: PathLike): - cv.check_type('source file', p, str) - self._path = p + cv.check_type('source file', p, PathLike) + self._path = input_path(p) def populate_xml_element(self, element): """Add necessary file source information to an XML element @@ -861,7 +857,7 @@ def populate_xml_element(self, element): """ if self.path is not None: - element.set("file", self.path) + element.set("file", str(self.path)) @classmethod def from_xml_element(cls, elem: ET.Element) -> openmc.FileSource: diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 10822c06fa7..0dc6f385685 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -97,6 +97,45 @@ def integral(self): return 1.0 +def _intensity_clip(intensity: Sequence[float], tolerance: float = 1e-6) -> np.ndarray: + """Clip low-importance points from an array of intensities. + + Given an array of intensities, this function returns an array of indices for + points that contribute non-negligibly to the total sum of intensities. + + Parameters + ---------- + intensity : sequence of float + Intensities in arbitrary units. + tolerance : float + Maximum fraction of intensities that will be discarded. + + Returns + ------- + Array of indices + + """ + # Get indices of intensities from largest to smallest + index_sort = np.argsort(intensity)[::-1] + + # Get intensities from largest to smallest + sorted_intensity = np.asarray(intensity)[index_sort] + + # Determine cumulative sum of probabilities + cumsum = np.cumsum(sorted_intensity) + cumsum /= cumsum[-1] + + # Find index that satisfies cutoff + index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance) + + # Now get indices up to cutoff + new_indices = index_sort[:index_cutoff + 1] + + # Put back in the order of the original array and return + new_indices.sort() + return new_indices + + class Discrete(Univariate): """Distribution characterized by a probability mass function. @@ -283,32 +322,20 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete: cv.check_less_than("tolerance", tolerance, 1.0, equality=True) cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) - # Determine (reversed) sorted order of probabilities + # Compute intensities intensity = self.p * self.x - index_sort = np.argsort(intensity)[::-1] - # Get probabilities in above order - sorted_intensity = intensity[index_sort] - - # Determine cumulative sum of probabilities - cumsum = np.cumsum(sorted_intensity) - cumsum /= cumsum[-1] - - # Find index which satisfies cutoff - index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance) - - # Now get indices up to cutoff - new_indices = index_sort[:index_cutoff + 1] - new_indices.sort() + # Get indices for intensities above threshold + indices = _intensity_clip(intensity, tolerance=tolerance) # Create new discrete distribution if inplace: - self.x = self.x[new_indices] - self.p = self.p[new_indices] + self.x = self.x[indices] + self.p = self.p[indices] return self else: - new_x = self.x[new_indices] - new_p = self.p[new_indices] + new_x = self.x[indices] + new_p = self.p[indices] return type(self)(new_x, new_p) @@ -1206,7 +1233,7 @@ def probability(self, probability): for p in probability: cv.check_greater_than('mixture distribution probabilities', p, 0.0, True) - self._probability = probability + self._probability = np.array(probability, dtype=float) @property def distribution(self): @@ -1312,40 +1339,63 @@ def integral(self): ]) def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture: - r"""Remove low-importance points from contained discrete distributions. + r"""Remove low-importance points / distributions - Given a probability mass function :math:`p(x)` with :math:`\{x_1, x_2, - x_3, \dots\}` the possible values of the random variable with - corresponding probabilities :math:`\{p_1, p_2, p_3, \dots\}`, this - function will remove any low-importance points such that :math:`\sum_i - x_i p_i` is preserved to within some threshold. + Like :meth:`Discrete.clip`, this method will remove low-importance + points from discrete distributions contained within the mixture but it + will also clip any distributions that have negligible contributions to + the overall intensity. .. versionadded:: 0.14.0 Parameters ---------- tolerance : float - Maximum fraction of :math:`\sum_i x_i p_i` that will be discarded - for any discrete distributions within the mixture distribution. + Maximum fraction of intensities that will be discarded. inplace : bool Whether to modify the current object in-place or return a new one. Returns ------- - Discrete distribution with low-importance points removed + Distribution with low-importance points / distributions removed """ + # Determine integral of original distribution to compare later + original_integral = self.integral() + + # Determine indices for any distributions that contribute non-negligibly + # to overall intensity + intensities = [prob*dist.integral() for prob, dist in + zip(self.probability, self.distribution)] + indices = _intensity_clip(intensities, tolerance=tolerance) + + # Clip mixture of distributions + probability = self.probability[indices] + distribution = [self.distribution[i] for i in indices] + + # Clip points from Discrete distributions + distribution = [ + dist.clip(tolerance, inplace) if isinstance(dist, Discrete) else dist + for dist in distribution + ] + if inplace: - for dist in self.distribution: - if isinstance(dist, Discrete): - dist.clip(tolerance, inplace=True) - return self + # Set attributes of current object and return + self.probability = probability + self.distribution = distribution + new_dist = self else: - distribution = [ - dist.clip(tolerance) if isinstance(dist, Discrete) else dist - for dist in self.distribution - ] - return type(self)(self.probability, distribution) + # Create new distribution + new_dist = type(self)(probability, distribution) + + # Show warning if integral of new distribution is not within + # tolerance of original + diff = (original_integral - new_dist.integral())/original_integral + if diff > tolerance: + warn("Clipping mixture distribution resulted in an integral that is " + f"lower by a fraction of {diff} when tolerance={tolerance}.") + + return new_dist def combine_distributions( diff --git a/openmc/surface.py b/openmc/surface.py index 2f10750a89b..537823ba16a 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable from copy import deepcopy @@ -2631,7 +2632,7 @@ def __or__(self, other): else: return Union((self, other)) - def __invert__(self): + def __invert__(self) -> Halfspace: return -self.surface if self.side == '+' else +self.surface def __contains__(self, point): diff --git a/openmc/universe.py b/openmc/universe.py index deaaf6a7cb1..5409222229d 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -17,6 +17,7 @@ from .checkvalue import check_type, check_value from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES +from .utility_funcs import input_path class UniverseBase(ABC, IDManagerMixin): @@ -661,7 +662,6 @@ def __repr__(self): string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) return string - @property def bounding_box(self) -> openmc.BoundingBox: regions = [c.region for c in self.cells.values() @@ -759,7 +759,6 @@ def create_xml_subelement(self, xml_element, memo=None): cell_element.set("universe", str(self._id)) xml_element.append(cell_element) - def _partial_deepcopy(self): """Clone all of the openmc.Universe object's attributes except for its cells, as they are copied within the clone function. This should only to be @@ -768,5 +767,3 @@ def _partial_deepcopy(self): clone = openmc.Universe(name=self.name) clone.volume = self.volume return clone - - diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 3dff45380c1..da9f73b1651 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -3,8 +3,10 @@ from pathlib import Path from tempfile import TemporaryDirectory +import openmc from .checkvalue import PathLike + @contextmanager def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = False): """Context manager for executing in a provided working directory @@ -35,3 +37,23 @@ def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = Fals os.chdir(orig_dir) if tmpdir: tmp.cleanup() + + +def input_path(filename: PathLike) -> Path: + """Return a path object for an input file based on global configuration + + Parameters + ---------- + filename : PathLike + Path to input file + + Returns + ------- + pathlib.Path + Path object + + """ + if openmc.config['resolve_paths']: + return Path(filename).resolve() + else: + return Path(filename) diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 6af723cca09..9b112b139b0 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -11,7 +11,7 @@ #include "openmc/settings.h" #include "openmc/string_utils.h" -#ifdef UWUW +#ifdef OPENMC_UWUW #include "uwuw.hpp" #endif #include @@ -29,7 +29,7 @@ const bool DAGMC_ENABLED = true; const bool DAGMC_ENABLED = false; #endif -#ifdef UWUW +#ifdef OPENMC_UWUW const bool UWUW_ENABLED = true; #else const bool UWUW_ENABLED = false; @@ -131,6 +131,11 @@ void DAGUniverse::initialize() { geom_type() = GeometryType::DAG; +#ifdef OPENMC_UWUW + // read uwuw materials from the .h5m file if present + read_uwuw_materials(); +#endif + init_dagmc(); init_metadata(); @@ -479,16 +484,16 @@ void DAGUniverse::to_hdf5(hid_t universes_group) const bool DAGUniverse::uses_uwuw() const { -#ifdef UWUW +#ifdef OPENMC_UWUW return uwuw_ && !uwuw_->material_library.empty(); #else return false; -#endif // UWUW +#endif // OPENMC_UWUW } std::string DAGUniverse::get_uwuw_materials_xml() const { -#ifdef UWUW +#ifdef OPENMC_UWUW if (!uses_uwuw()) { throw std::runtime_error("This DAGMC Universe does not use UWUW materials"); } @@ -508,12 +513,12 @@ std::string DAGUniverse::get_uwuw_materials_xml() const return ss.str(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif // UWUW +#endif // OPENMC_UWUW } void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const { -#ifdef UWUW +#ifdef OPENMC_UWUW if (!uses_uwuw()) { throw std::runtime_error( "This DAGMC universe does not use UWUW materials."); @@ -526,7 +531,7 @@ void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const mats_xml.close(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } void DAGUniverse::legacy_assign_material( @@ -588,7 +593,7 @@ void DAGUniverse::legacy_assign_material( void DAGUniverse::read_uwuw_materials() { -#ifdef UWUW +#ifdef OPENMC_UWUW // If no filename was provided, don't read UWUW materials if (filename_ == "") return; @@ -628,16 +633,13 @@ void DAGUniverse::read_uwuw_materials() } #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } void DAGUniverse::uwuw_assign_material( moab::EntityHandle vol_handle, std::unique_ptr& c) const { -#ifdef UWUW - // read materials from uwuw material file - read_uwuw_materials(); - +#ifdef OPENMC_UWUW // lookup material in uwuw if present std::string uwuw_mat = dmd_ptr->volume_material_property_data_eh[vol_handle]; if (uwuw_->material_library.count(uwuw_mat) != 0) { @@ -649,11 +651,11 @@ void DAGUniverse::uwuw_assign_material( } else { fatal_error(fmt::format("Material with value '{}' not found in the " "UWUW material library", - mat_str)); + uwuw_mat)); } #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } //============================================================================== // DAGMC Cell implementation diff --git a/src/distribution.cpp b/src/distribution.cpp index 3026630b335..a6b4acd58b1 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -394,6 +394,9 @@ Mixture::Mixture(pugi::xml_node node) distribution_.push_back(std::make_pair(cumsum, std::move(dist))); } + // Save integral of distribution + integral_ = cumsum; + // Normalize cummulative probabilities to 1 for (auto& pair : distribution_) { pair.first /= cumsum; diff --git a/src/output.cpp b/src/output.cpp index 5fdbea1304e..a430fe9a6c6 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -347,7 +347,7 @@ void print_build_info() #ifdef COVERAGEBUILD coverage = y; #endif -#ifdef UWUW +#ifdef OPENMC_UWUW uwuw = y; #endif diff --git a/tests/conftest.py b/tests/conftest.py index 639d669f3a8..cd86da53900 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +import openmc from tests.regression_tests import config as regression_config @@ -27,3 +28,9 @@ def run_in_tmpdir(tmpdir): yield finally: orig.chdir() + + +@pytest.fixture(scope='session', autouse=True) +def resolve_paths(): + with openmc.config.patch('resolve_paths', False): + yield diff --git a/tests/regression_tests/filter_mesh/inputs_true.dat b/tests/regression_tests/filter_mesh/inputs_true.dat index a58d58bab95..0667c034148 100644 --- a/tests/regression_tests/filter_mesh/inputs_true.dat +++ b/tests/regression_tests/filter_mesh/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/filter_translations/inputs_true.dat b/tests/regression_tests/filter_translations/inputs_true.dat index a80ccd87675..5004c3217ab 100644 --- a/tests/regression_tests/filter_translations/inputs_true.dat +++ b/tests/regression_tests/filter_translations/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat index bb9c99026d0..1ecb7a2d3b4 100644 --- a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index ee6cb88622b..10f0bad9841 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -9,7 +9,7 @@ - + diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index ee6cb88622b..10f0bad9841 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -9,7 +9,7 @@ - + diff --git a/tests/regression_tests/score_current/inputs_true.dat b/tests/regression_tests/score_current/inputs_true.dat index 9ea8e5e4ae1..ddbd6c24b05 100644 --- a/tests/regression_tests/score_current/inputs_true.dat +++ b/tests/regression_tests/score_current/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/source/inputs_true.dat b/tests/regression_tests/source/inputs_true.dat index c1a616dd109..e787f24d413 100644 --- a/tests/regression_tests/source/inputs_true.dat +++ b/tests/regression_tests/source/inputs_true.dat @@ -85,13 +85,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 @@ -108,13 +108,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 @@ -132,13 +132,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 diff --git a/tests/regression_tests/source_dlopen/test.py b/tests/regression_tests/source_dlopen/test.py index 88ff9dd8509..0581d6deec4 100644 --- a/tests/regression_tests/source_dlopen/test.py +++ b/tests/regression_tests/source_dlopen/test.py @@ -72,8 +72,7 @@ def model(): model.tallies = openmc.Tallies([tally]) # custom source from shared library - source = openmc.CompiledSource() - source.library = 'build/libsource.so' + source = openmc.CompiledSource('build/libsource.so') model.settings.source = source return model diff --git a/tests/regression_tests/source_parameterized_dlopen/test.py b/tests/regression_tests/source_parameterized_dlopen/test.py index 1cc253528cb..151fb37356e 100644 --- a/tests/regression_tests/source_parameterized_dlopen/test.py +++ b/tests/regression_tests/source_parameterized_dlopen/test.py @@ -71,8 +71,7 @@ def model(): model.tallies = openmc.Tallies([tally]) # custom source from shared library - source = openmc.CompiledSource() - source.library = 'build/libsource.so' + source = openmc.CompiledSource('build/libsource.so') source.parameters = '1e3' model.settings.source = source diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 1d3c0f173b0..9d3f53a7403 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -19,7 +19,10 @@ def test_config_basics(): assert isinstance(openmc.config, Mapping) for key, value in openmc.config.items(): assert isinstance(key, str) - assert isinstance(value, os.PathLike) + if key == 'resolve_paths': + assert isinstance(value, bool) + else: + assert isinstance(value, os.PathLike) # Set and delete openmc.config['cross_sections'] = '/path/to/cross_sections.xml' @@ -32,6 +35,13 @@ def test_config_basics(): openmc.config['🐖'] = '/like/to/eat/bacon' +def test_config_patch(): + openmc.config['cross_sections'] = '/path/to/cross_sections.xml' + with openmc.config.patch('cross_sections', '/path/to/other.xml'): + assert str(openmc.config['cross_sections']) == '/path/to/other.xml' + assert str(openmc.config['cross_sections']) == '/path/to/cross_sections.xml' + + def test_config_set_envvar(): openmc.config['cross_sections'] = '/path/to/cross_sections.xml' assert os.environ['OPENMC_CROSS_SECTIONS'] == '/path/to/cross_sections.xml' diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 348143e0b0a..2d80cf8384f 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -22,8 +22,22 @@ def test_dose_coefficients(): assert energy[-1] == approx(10e9) assert dose[-1] == approx(699.0) + energy, dose = dose_coefficients('photon', data_source='icrp74') + assert energy[0] == approx(0.01e6) + assert dose[0] == approx(7.43*0.00653) + assert energy[-1] == approx(10.0e6) + assert dose[-1] == approx(24.0*0.990) + + energy, dose = dose_coefficients('neutron', 'LLAT', data_source='icrp74') + assert energy[0] == approx(1e-3) + assert dose[0] == approx(1.68) + assert energy[-1] == approx(20.0e6) + assert dose[-1] == approx(338.0) + # Invalid particle/geometry should raise an exception with raises(ValueError): dose_coefficients('slime', 'LAT') with raises(ValueError): dose_coefficients('neutron', 'ZZ') + with raises(ValueError): + dose_coefficients('neutron', data_source='icrp7000') diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 94ba82571be..c6a07cff977 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -683,3 +683,17 @@ def intensity(src): stable.add_nuclide('Gd156', 1.0) stable.volume = 1.0 assert stable.get_decay_photon_energy() is None + + +def test_avoid_subnormal(run_in_tmpdir): + # Write a materials.xml with a material that has a nuclide density that is + # represented as a subnormal floating point value + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + mat.add_nuclide('H2', 1.0e-315) + mats = openmc.Materials([mat]) + mats.export_to_xml() + + # When read back in, the density should be zero + mats = openmc.Materials.from_xml() + assert mats[0].get_nuclide_atom_densities()['H2'] == 0.0 diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index 8c9e0afe4d9..cbcd1983125 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -106,7 +106,7 @@ def test_complement(reset): assert_unbounded(outside_equiv) # string represention - assert str(inside) == '~(1 | -2 | 3)' + assert str(inside) == '(-1 2 -3)' # evaluate method assert (0, 0, 0) in inside diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 650bfd18680..02a47625162 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -91,7 +91,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.sourcepoint == {'batches': [50, 150, 500, 1000], 'separate': True, 'write': True, 'overwrite': True, 'mcpl': True} assert s.statepoint == {'batches': [50, 150, 500, 1000]} - assert s.surf_source_read == {'path': 'surface_source_1.h5'} + assert s.surf_source_read['path'].name == 'surface_source_1.h5' assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200} assert s.confidence_intervals assert s.ptables diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 9a19f6f24dd..32650d54936 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -53,7 +53,7 @@ def test_spherical_uniform(): def test_source_file(): filename = 'source.h5' src = openmc.FileSource(path=filename) - assert src.path == filename + assert src.path.name == filename elem = src.to_xml_element() assert 'strength' in elem.attrib @@ -61,9 +61,9 @@ def test_source_file(): def test_source_dlopen(): - library = './libsource.so' - src = openmc.CompiledSource(library=library) - assert src.library == library + library = 'libsource.so' + src = openmc.CompiledSource(library) + assert src.library.name == library elem = src.to_xml_element() assert 'library' in elem.attrib diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 0414fc22559..643e115564b 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -262,7 +262,7 @@ def test_mixture(): d2 = openmc.stats.Uniform(3, 7) p = [0.5, 0.5] mix = openmc.stats.Mixture(p, [d1, d2]) - assert mix.probability == p + np.testing.assert_allclose(mix.probability, p) assert mix.distribution == [d1, d2] assert len(mix) == 4 @@ -274,7 +274,7 @@ def test_mixture(): elem = mix.to_xml_element('distribution') d = openmc.stats.Mixture.from_xml_element(elem) - assert d.probability == p + np.testing.assert_allclose(d.probability, p) assert d.distribution == [d1, d2] assert len(d) == 4 @@ -296,6 +296,20 @@ def test_mixture_clip(): mix_same = mix.clip(1e-6, inplace=True) assert mix_same is mix + # Make sure clip removes low probability distributions + d_small = openmc.stats.Uniform(0., 1.) + d_large = openmc.stats.Uniform(2., 5.) + mix = openmc.stats.Mixture([1e-10, 1.0], [d_small, d_large]) + mix_clip = mix.clip(1e-3) + assert mix_clip.distribution == [d_large] + + # Make sure warning is raised if tolerance is exceeded + d1 = openmc.stats.Discrete([1.0, 1.001], [1.0, 0.7e-6]) + d2 = openmc.stats.Tabular([0.0, 1.0], [0.7e-6], interpolation='histogram') + mix = openmc.stats.Mixture([1.0, 1.0], [d1, d2]) + with pytest.warns(UserWarning): + mix_clip = mix.clip(1e-6) + def test_polar_azimuthal(): # default polar-azimuthal should be uniform in mu and phi diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index d862ae6b0de..963bbe00d19 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -552,3 +552,47 @@ def test_box(): assert (0., 0.9, 0.) in -s assert (0., 0., -3.) not in +s assert (0., 0., 3.) not in +s + + +def test_conical_frustum(): + center_base = (0.0, 0.0, -3) + axis = (0., 0., 3.) + r1 = 2.0 + r2 = 0.5 + s = openmc.model.ConicalFrustum(center_base, axis, r1, r2) + assert isinstance(s.cone, openmc.Cone) + assert isinstance(s.plane_bottom, openmc.Plane) + assert isinstance(s.plane_top, openmc.Plane) + + # Make sure boundary condition propagates + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + assert s.cone.boundary_type == 'reflective' + assert s.plane_bottom.boundary_type == 'reflective' + assert s.plane_top.boundary_type == 'reflective' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert ll[2] == pytest.approx(-3.0) + assert ur[2] == pytest.approx(0.0) + + # __contains__ on associated half-spaces + assert (0., 0., -1.) in -s + assert (0., 0., -4.) not in -s + assert (0., 0., 1.) not in -s + assert (1., 1., -2.99) in -s + assert (1., 1., -0.01) in +s + + # translate method + s_t = s.translate((1., 1., 0.)) + assert (1., 1., -0.01) in -s_t + + # Make sure repr works + repr(s) + + # Denegenerate case with r1 = r2 + s = openmc.model.ConicalFrustum(center_base, axis, r1, r1) + assert (1., 1., -0.01) in -s diff --git a/tests/unit_tests/test_temp_interp.py b/tests/unit_tests/test_temp_interp.py index 4c2882347b3..4566070cfd9 100644 --- a/tests/unit_tests/test_temp_interp.py +++ b/tests/unit_tests/test_temp_interp.py @@ -152,7 +152,7 @@ def model(tmp_path_factory): mat = openmc.Material() mat.add_nuclide('U235', 1.0) model.materials.append(mat) - model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + model.materials.cross_sections = 'cross_sections_fake.xml' sph = openmc.Sphere(r=100.0, boundary_type='reflective') cell = openmc.Cell(fill=mat, region=-sph) @@ -257,7 +257,7 @@ def test_temperature_slightly_above(run_in_tmpdir): mat2.add_nuclide('U235', 1.0) mat2.temperature = 600.0 model.materials.extend([mat1, mat2]) - model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + model.materials.cross_sections = 'cross_sections_fake.xml' sph1 = openmc.Sphere(r=1.0) sph2 = openmc.Sphere(r=4.0, boundary_type='reflective') diff --git a/tools/ci/gha-install-libmesh.sh b/tools/ci/gha-install-libmesh.sh index cb808ae5b3b..d4557d2d3a2 100755 --- a/tools/ci/gha-install-libmesh.sh +++ b/tools/ci/gha-install-libmesh.sh @@ -16,7 +16,6 @@ else ../libmesh/configure --prefix=$HOME/LIBMESH --enable-exodus --disable-netcdf-4 --disable-eigen --disable-lapack --disable-mpi fi make -j4 install -export LIBMESH_PC=$HOME/LIBMESH/lib/pkgconfig/ rm -rf $HOME/LIBMESH/build popd diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index f046e863470..282389a8f19 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -31,7 +31,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if dagmc: cmake_cmd.append('-DOPENMC_USE_DAGMC=ON') - cmake_cmd.append('-DCMAKE_PREFIX_PATH=~/DAGMC') + dagmc_path = os.environ.get('HOME') + '/DAGMC' + cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + dagmc_path) if libmesh: cmake_cmd.append('-DOPENMC_USE_LIBMESH=ON') @@ -40,8 +41,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if ncrystal: cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - ncrystal_cmake_path = os.environ.get('HOME') + '/ncrystal_inst/lib/cmake' - cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_cmake_path}') + ncrystal_path = os.environ.get('HOME') + '/ncrystal_inst' + cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_path}') # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on')