diff --git a/openmc/data/library.py b/openmc/data/library.py index 905dbbe4a7..d5f18aa170 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -1,5 +1,6 @@ import os import xml.etree.ElementTree as ET +import pathlib import h5py @@ -48,19 +49,30 @@ class DataLibrary(EqualityMixin): Parameters ---------- - filename : str + filename : str or Path Path to the file to be registered. + If an ``xml`` file, treat as the depletion chain file without + materials. """ - # Support pathlib - # TODO: Remove when support is Python 3.6+ only - filename = str(filename) + if not isinstance(filename, pathlib.Path): + path = pathlib.Path(filename) + else: + path = filename - with h5py.File(filename, 'r') as h5file: - filetype = h5file.attrs['filetype'].decode()[5:] - materials = list(h5file) + if path.suffix == '.xml': + filetype = 'depletion_chain' + materials = [] + elif path.suffix == '.h5': + with h5py.File(path, 'r') as h5file: + filetype = h5file.attrs['filetype'].decode()[5:] + materials = list(h5file) + else: + raise ValueError( + "File type {} not supported by {}" + .format(path.name, self.__class__.__name__)) - library = {'path': filename, 'type': filetype, 'materials': materials} + library = {'path': str(path), 'type': filetype, 'materials': materials} self.libraries.append(library) def export_to_xml(self, path='cross_sections.xml'): @@ -85,8 +97,11 @@ class DataLibrary(EqualityMixin): dir_element.text = os.path.realpath(common_dir) for library in self.libraries: - lib_element = ET.SubElement(root, "library") - lib_element.set('materials', ' '.join(library['materials'])) + if library['type'] == "depletion_chain": + lib_element = ET.SubElement(root, "depletion_chain") + else: + lib_element = ET.SubElement(root, "library") + lib_element.set('materials', ' '.join(library['materials'])) lib_element.set('path', os.path.relpath(library['path'], common_dir)) lib_element.set('type', library['type']) @@ -106,7 +121,7 @@ class DataLibrary(EqualityMixin): ---------- path : str, optional Path to XML file to read. If not provided, the - `OPENMC_CROSS_SECTIONS` environment variable will be used. + :envvar:`OPENMC_CROSS_SECTIONS` environment variable will be used. Returns ------- @@ -146,4 +161,13 @@ class DataLibrary(EqualityMixin): 'materials': materials} data.libraries.append(library) + # get depletion chain data + + dep_node = root.find("depletion_chain") + if dep_node is not None: + filename = os.path.join(directory, dep_node.attrib['path']) + library = {'path': filename, 'type': 'depletion_chain', + 'materials': []} + data.libraries.append(library) + return data diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8502909a21..2e598fb74a 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -8,7 +8,10 @@ from collections import namedtuple import os from pathlib import Path from abc import ABCMeta, abstractmethod +from xml.etree import ElementTree as ET +from warnings import warn +from openmc.data import DataLibrary from .chain import Chain OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) @@ -43,8 +46,9 @@ class TransportOperator(metaclass=ABCMeta): Parameters ---------- chain_file : str, optional - Path to the depletion chain XML file. Defaults to the - :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. + Path to the depletion chain XML file. Defaults to the file + listed under ``depletion_chain`` in + :envvar:`OPENMC_CROSS_SECTIONS` environment variable. Attributes ---------- @@ -62,8 +66,21 @@ class TransportOperator(metaclass=ABCMeta): if chain_file is None: chain_file = os.environ.get("OPENMC_DEPLETE_CHAIN", None) if chain_file is None: - raise IOError("No chain specified, either manually or in " - "environment variable OPENMC_DEPLETE_CHAIN.") + data = DataLibrary.from_xml() + # search for depletion_chain path from end of list + for lib in reversed(data.libraries): + if lib['type'] == 'depletion_chain': + break + else: + raise IOError( + "No chain specified, either manually or " + "under depletion_chain in environment variable " + "OPENMC_CROSS_SECTIONS.") + chain_file = lib['path'] + else: + warn("Use of OPENMC_DEPLETE_CHAIN is deprecated in favor " + "of adding depletion_chain to OPENMC_CROSS_SECTIONS", + FutureWarning) self.chain = Chain.from_xml(chain_file) @abstractmethod diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 4635d99d15..b8b6a3644a 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -107,7 +107,8 @@ class Chain(object): yield sublibrary files. The depletion chain used during a depletion simulation is indicated by either an argument to :class:`openmc.deplete.Operator` or through the - :envvar:`OPENMC_DEPLETE_CHAIN` environment variable. + ``depletion_chain`` item in the :envvar:`OPENMC_CROSS_SECTIONS` + environment variable. Attributes ---------- diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 66cac392d9..d284d98567 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -64,8 +64,9 @@ class Operator(TransportOperator): settings : openmc.Settings OpenMC Settings object chain_file : str, optional - Path to the depletion chain XML file. Defaults to the - :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. + Path to the depletion chain XML file. Defaults to the file + listed under ``depletion_chain`` in + :envvar:`OPENMC_CROSS_SECTIONS` environment variable. prev_results : ResultsList, optional Results from a previous depletion calculation. If this argument is specified, the depletion calculation will start from the latest state diff --git a/openmc/model/model.py b/openmc/model/model.py index cf6603638e..f66bd8f16d 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -133,8 +133,9 @@ class Model(object): Array of timesteps in units of [s]. Note that values are not cumulative. chain_file : str, optional - Path to the depletion chain XML file. Defaults to the - :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. + Path to the depletion chain XML file. Defaults to the chain + found under the ``depletion_chain`` in the + :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. method : str Integration method used for depletion (e.g., 'cecm', 'predictor') **kwargs diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index ea20ba53c8..2585e9315f 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -2,6 +2,7 @@ from collections.abc import Mapping import os +from pathlib import Path import numpy as np import pytest @@ -33,6 +34,30 @@ def test_data_library(tmpdir): assert new_lib.libraries[-1]['type'] == 'thermal' +def test_depletion_chain_data_library(run_in_tmpdir): + dep_lib = openmc.data.DataLibrary.from_xml() + prev_len = len(dep_lib.libraries) + chain_path = Path(__file__).parents[1] / "chain_simple.xml" + dep_lib.register_file(chain_path) + assert len(dep_lib.libraries) == prev_len + 1 + # Inspect + dep_dict = dep_lib.libraries[-1] + assert dep_dict['materials'] == [] + assert dep_dict['type'] == 'depletion_chain' + assert dep_dict['path'] == str(chain_path) + + out_path = "cross_section_chain.xml" + dep_lib.export_to_xml(out_path) + + dep_import = openmc.data.DataLibrary.from_xml(out_path) + for lib in reversed(dep_import.libraries): + if lib['type'] == 'depletion_chain': + break + else: + raise IndexError("depletion_chain not found in exported DataLibrary") + assert os.path.exists(lib['path']) + + def test_linearize(): """Test linearization of a continuous function.""" x, y = openmc.data.linearize([-1., 1.], lambda x: 1 - x*x) diff --git a/tests/unit_tests/test_deplete_operator.py b/tests/unit_tests/test_deplete_operator.py new file mode 100644 index 0000000000..757b51d923 --- /dev/null +++ b/tests/unit_tests/test_deplete_operator.py @@ -0,0 +1,70 @@ +"""Basic unit tests for openmc.deplete.Operator instantiation + +Modifies and resets environment variable OPENMC_CROSS_SECTIONS +to a custom file with new depletion_chain node +""" + +from os import environ +from unittest import mock +from pathlib import Path + +import pytest +from openmc.deplete.abc import TransportOperator +from openmc.deplete.chain import Chain + +BARE_XS_FILE = "bare_cross_sections.xml" +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" + + +@pytest.fixture() +def bare_xs(run_in_tmpdir): + """Create a very basic cross_sections file, return simple Chain. + + """ + + bare_xs_contents = """ + + + +""".format(CHAIN_PATH) + + with open(BARE_XS_FILE, "w") as out: + out.write(bare_xs_contents) + + yield + + +class BareDepleteOperator(TransportOperator): + """Very basic class for testing the initialization.""" + + # declare abstract methods so object can be created + def __call__(self, *args, **kwargs): + pass + + def initial_condition(self): + pass + + def get_results_info(self): + pass + + +@mock.patch.dict(environ, {"OPENMC_CROSS_SECTIONS": BARE_XS_FILE}) +def test_operator_init(bare_xs): + """The test will set and unset environment variable OPENMC_CROSS_SECTIONS + to point towards a temporary dummy file. This file will be removed + at the end of the test, and only contains a + depletion_chain node.""" + # force operator to read from OPENMC_CROSS_SECTIONS + bare_op = BareDepleteOperator(chain_file=None) + act_chain = bare_op.chain + ref_chain = Chain.from_xml(CHAIN_PATH) + assert len(act_chain) == len(ref_chain) + for name in ref_chain.nuclide_dict: + # compare openmc.deplete.Nuclide objects + ref_nuc = ref_chain[name] + act_nuc = act_chain[name] + for prop in [ + 'name', 'half_life', 'decay_energy', 'reactions', + 'decay_modes', 'yield_data', 'yield_energies', + ]: + assert getattr(act_nuc, prop) == getattr(ref_nuc, prop), prop