Consistency in treatment of paths for files specified within the Model class (#3153)

This commit is contained in:
Paul Romano 2024-10-10 12:17:40 -05:00 committed by GitHub
parent fb3aaa46ac
commit 579777a3e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 143 additions and 72 deletions

View file

@ -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
<https://svalinn.github.io/DAGMC/usersguide/cubit_basics.html>`_ successfully
and that the model is `watertight
<https://svalinn.github.io/DAGMC/usersguide/tools.html#make-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
<https://svalinn.github.io/DAGMC/usersguide/cubit_basics.html>`_ successfully
and that the model is `watertight
<https://svalinn.github.io/DAGMC/usersguide/tools.html#make-watertight>`_.
Future implementations of DAGMC geometry will support small volume overlaps and
un-merged surfaces.
Cell, Surface, and Material IDs
-------------------------------

View file

@ -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 @@ __all__ = ["config"]
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 @@ class _Config(MutableMapping):
# 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 @@ class _Config(MutableMapping):
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"""

View file

@ -16,6 +16,7 @@ import openmc.data
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
@ -1643,7 +1644,7 @@ class Materials(cv.CheckedList):
@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

View file

@ -5,8 +5,6 @@ from collections.abc import Iterable, Sequence
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 openmc.utility_funcs import change_directory
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 @@ class UnstructuredMesh(MeshBase):
@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):

View file

@ -8,12 +8,14 @@ from pathlib import Path
import lxml.etree as ET
import openmc.checkvalue as cv
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 openmc.stats.multivariate import MeshSpatial
from ._xml import clean_indentation, get_text, reorder_attributes
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 @@ class Settings:
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 @@ class Settings:
@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 @@ class Settings:
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 @@ class Settings:
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 @@ class Settings:
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')

View file

@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
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.multivariate import UnitSphere, Spatial
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 @@ class CompiledSource(SourceBase):
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 @@ class CompiledSource(SourceBase):
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 @@ class FileSource(SourceBase):
@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 @@ class FileSource(SourceBase):
"""
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:

View file

@ -17,6 +17,7 @@ from ._xml import get_text
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):
@ -766,7 +767,7 @@ class DAGMCUniverse(UniverseBase):
Parameters
----------
filename : str
filename : path-like
Path to the DAGMC file used to represent this universe.
universe_id : int, optional
Unique identifier of the universe. If not specified, an identifier will
@ -820,7 +821,7 @@ class DAGMCUniverse(UniverseBase):
"""
def __init__(self,
filename,
filename: cv.PathLike,
universe_id=None,
name='',
auto_geom_ids=False,
@ -850,9 +851,9 @@ class DAGMCUniverse(UniverseBase):
return self._filename
@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):
@ -915,8 +916,7 @@ class DAGMCUniverse(UniverseBase):
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])

View file

@ -3,8 +3,10 @@ import os
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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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')