mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 14:35:27 -04:00
218 lines
7.4 KiB
Python
218 lines
7.4 KiB
Python
"""Module for handling global configuration in OpenMC.
|
|
|
|
This module exports a single object, `config`, that can be used to control
|
|
various settings, primarily paths to data files. It acts like a dictionary but
|
|
with special behaviors.
|
|
|
|
Examples
|
|
--------
|
|
>>> import openmc
|
|
>>> openmc.config['cross_sections'] = '/path/to/my/cross_sections.xml'
|
|
>>> print(openmc.config)
|
|
{'resolve_paths': True, 'cross_sections': PosixPath('/path/to/my/cross_sections.xml')}
|
|
|
|
"""
|
|
from collections.abc import MutableMapping
|
|
from contextlib import contextmanager
|
|
import os
|
|
from pathlib import Path
|
|
import warnings
|
|
from typing import Any, Dict, Iterator
|
|
|
|
from openmc.data import DataLibrary
|
|
from openmc.data.decay import _DECAY_ENERGY, _DECAY_PHOTON_ENERGY
|
|
|
|
__all__ = ["config"]
|
|
|
|
|
|
class _Config(MutableMapping):
|
|
"""A configuration dictionary for OpenMC with special handling for path-like values.
|
|
|
|
This class enforces valid configuration keys and synchronizes path-related
|
|
settings with their corresponding environment variables.
|
|
|
|
Attributes
|
|
----------
|
|
cross_sections : pathlib.Path
|
|
Path to a cross_sections.xml file. Also sets/unsets the
|
|
OPENMC_CROSS_SECTIONS environment variable.
|
|
mg_cross_sections : pathlib.Path
|
|
Path to a multi-group cross_sections.h5 file. Also sets/unsets
|
|
the OPENMC_MG_CROSS_SECTIONS environment variable.
|
|
chain_file : pathlib.Path
|
|
Path to a depletion chain XML file. Also sets/unsets the
|
|
OPENMC_CHAIN_FILE environment variable. Setting or deleting this
|
|
clears internal decay data caches.
|
|
resolve_paths : bool
|
|
If True (default), all paths assigned are resolved to absolute
|
|
paths. If False, paths are stored as they are provided.
|
|
|
|
"""
|
|
_PATH_KEYS: Dict[str, str] = {
|
|
'cross_sections': 'OPENMC_CROSS_SECTIONS',
|
|
'mg_cross_sections': 'OPENMC_MG_CROSS_SECTIONS',
|
|
'chain_file': 'OPENMC_CHAIN_FILE'
|
|
}
|
|
|
|
def __init__(self, data: dict = ()):
|
|
self._mapping: Dict[str, Any] = {'resolve_paths': True}
|
|
self.update(data)
|
|
|
|
def __getitem__(self, key: str) -> Any:
|
|
return self._mapping[key]
|
|
|
|
def __delitem__(self, key: str):
|
|
"""Delete a configuration key.
|
|
|
|
This also deletes the corresponding environment variable if the key is a
|
|
path-like key, and clears decay data caches if 'chain_file' is deleted.
|
|
'resolve_paths' cannot be deleted.
|
|
|
|
"""
|
|
if key == 'resolve_paths':
|
|
raise KeyError("'resolve_paths' cannot be deleted.")
|
|
del self._mapping[key]
|
|
if key in self._PATH_KEYS:
|
|
env_var = self._PATH_KEYS[key]
|
|
if env_var in os.environ:
|
|
del os.environ[env_var]
|
|
if key == 'chain_file':
|
|
_DECAY_PHOTON_ENERGY.clear()
|
|
_DECAY_ENERGY.clear()
|
|
|
|
def __setitem__(self, key: str, value: Any):
|
|
"""Set a configuration key and its corresponding value.
|
|
|
|
For path-like keys, this method performs several actions:
|
|
1. Resolves the path to an absolute path if `resolve_paths` is True.
|
|
2. Stores the `pathlib.Path` object.
|
|
3. Sets the corresponding environment variable (e.g., OPENMC_CROSS_SECTIONS).
|
|
4. For 'chain_file', clears internal decay data caches.
|
|
5. Issues a `UserWarning` if the final path does not exist.
|
|
|
|
"""
|
|
if key in self._PATH_KEYS:
|
|
p = Path(value)
|
|
# Use .get() for robustness, defaulting to True
|
|
if self._mapping.get('resolve_paths', True):
|
|
stored_path = p.resolve(strict=False)
|
|
else:
|
|
stored_path = p
|
|
|
|
self._mapping[key] = stored_path
|
|
os.environ[self._PATH_KEYS[key]] = str(stored_path)
|
|
|
|
if key == 'chain_file':
|
|
_DECAY_PHOTON_ENERGY.clear()
|
|
_DECAY_ENERGY.clear()
|
|
|
|
if not stored_path.exists():
|
|
warnings.warn(f"Path '{stored_path}' does not exist.", UserWarning)
|
|
|
|
elif key == 'resolve_paths':
|
|
if not isinstance(value, bool):
|
|
raise TypeError("'resolve_paths' must be a boolean.")
|
|
self._mapping[key] = value
|
|
else:
|
|
valid_keys = list(self._PATH_KEYS.keys()) + ['resolve_paths']
|
|
raise KeyError(
|
|
f"Unrecognized config key: {key}. Acceptable keys are: "
|
|
f"{', '.join(repr(k) for k in valid_keys)}."
|
|
)
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
return iter(self._mapping)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._mapping)
|
|
|
|
def __repr__(self) -> str:
|
|
return repr(self._mapping)
|
|
|
|
def clear(self):
|
|
"""Clear all configuration keys except for 'resolve_paths'.
|
|
|
|
This ensures that the path resolution behavior is not accidentally reset
|
|
when clearing the configuration.
|
|
|
|
"""
|
|
# Create a copy of keys to iterate over for safe deletion
|
|
keys_to_delete = [k for k in self._mapping if k != 'resolve_paths']
|
|
for key in keys_to_delete:
|
|
del self[key]
|
|
|
|
@contextmanager
|
|
def patch(self, key: str, value: Any):
|
|
"""Context manager to temporarily change a configuration value.
|
|
|
|
After the `with` block, the configuration is restored to its original
|
|
state.
|
|
|
|
Parameters
|
|
----------
|
|
key : str
|
|
The key of the configuration value to change.
|
|
value
|
|
The new temporary value.
|
|
|
|
Examples
|
|
--------
|
|
>>> openmc.config['cross_sections'] = 'endf71.xml'
|
|
>>> with openmc.config.patch('cross_sections', 'fendl32.xml'):
|
|
... # Code in this block sees the new value
|
|
... print(f"Inside with block: {openmc.config['cross_sections']}")
|
|
>>> # Outside the block, the value is restored
|
|
>>> print(f"Outside with block: {openmc.config['cross_sections']}")
|
|
Inside with block: fendl32.xml
|
|
Outside with block: endf71.xml
|
|
|
|
"""
|
|
previous_value = self.get(key)
|
|
self[key] = value
|
|
try:
|
|
yield
|
|
finally:
|
|
if previous_value is None:
|
|
del self[key]
|
|
else:
|
|
self[key] = previous_value
|
|
|
|
|
|
def _default_config(**kwargs) -> _Config:
|
|
"""Create a configuration initialized from environment variables.
|
|
|
|
This function checks for OPENMC_CROSS_SECTIONS, OPENMC_MG_CROSS_SECTIONS,
|
|
and OPENMC_CHAIN_FILE environment variables. It also has logic to find
|
|
a chain file within a `cross_sections.xml` file if one is not
|
|
explicitly set.
|
|
|
|
Returns
|
|
-------
|
|
_Config
|
|
A new configuration object.
|
|
|
|
"""
|
|
config = _Config(kwargs)
|
|
for key,var in _Config._PATH_KEYS.items():
|
|
if var in os.environ:
|
|
config[key] = os.environ[var]
|
|
|
|
chain_file = config.get("chain_file")
|
|
xs_path = config.get("cross_sections")
|
|
if chain_file is None and xs_path is not None and xs_path.exists():
|
|
try:
|
|
data = DataLibrary.from_xml(xs_path)
|
|
except Exception:
|
|
# Let this pass silently if cross_sections.xml can't be parsed
|
|
# or if a dependency like lxml is not available.
|
|
pass
|
|
else:
|
|
for lib in reversed(data.libraries):
|
|
if lib['type'] == 'depletion_chain':
|
|
config['chain_file'] = xs_path.parent / lib['path']
|
|
break
|
|
return config
|
|
|
|
|
|
# Global configuration dictionary for OpenMC settings.
|
|
config = _default_config()
|