From cc98d13793a7dd1fac4d42a260bd49403889becc Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 30 May 2019 17:55:09 -0400 Subject: [PATCH 01/10] Read chain file for deplete.Operator from OPENMC_CROSS_SECTIONS If a chain_file is not passed when initializing an openmc.deplete.Operator object, then the chain_file will be read from the depletion_chain node from $OPENMC_CROSS_SECTIONS. A deprecation warning is raised if the environment variable OPENMC_DEPLETE_CHAIN is still set, informing the users that this information can instead be placed in their cross section file. Documentation is updated describing the logic in the chain_file search to reflect this in - TransportOperator - Operator - Chain . Closes #1239 --- openmc/deplete/abc.py | 26 ++++++++++++++++++++++---- openmc/deplete/chain.py | 3 ++- openmc/deplete/operator.py | 5 +++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8502909a21..14dd3638ce 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -8,6 +8,8 @@ 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 .chain import Chain @@ -43,8 +45,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 +65,23 @@ 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.") + xs_file = os.environ.get("OPENMC_CROSS_SECTIONS", None) + if xs_file is None: + raise IOError( + "OPENMC_CROSS_SECTIONS environment variable " + "not set.") + root = ET.parse(xs_file).getroot() + node = root.find("depletion_chain") + if node is None: + raise IOError( + "No chain specified, either manually or " + "under depletion_chain in environment variable " + "OPENMC_CROSS_SECTIONS.") + chain_file = node.get("path") + else: + warn("Use of OPENMC_DEPLETE_CHAIN is deprecated in favor " + "of adding depletion_chain to OPENMC_CROSS_SECTIONS", + DeprecationWarning) 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 From f7008616465267906dd7ca75e0339812549e2988 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 30 May 2019 18:01:33 -0400 Subject: [PATCH 02/10] Add test for the instantiation of deplete.Operator Construct a minimal cross sections xml file that only contains the depletion_chain node required by the Operator. The path to this file is set to the OPENMC_CROSS_SECTIONS file, and reverted after the test. The depletion_chain points towards the chain_simple.xml files, and a reference Chain is produced in the test. The test involves creating a bare Operator instance, that constructs a chain based on the temporary OPENMC_CROSS_SECTIONS file. This chain is compared to that produced by reading the chain_simple.xml file stored in the test directory. --- tests/unit_tests/test_deplete_operator.py | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/unit_tests/test_deplete_operator.py diff --git a/tests/unit_tests/test_deplete_operator.py b/tests/unit_tests/test_deplete_operator.py new file mode 100644 index 0000000000..c188ec94c2 --- /dev/null +++ b/tests/unit_tests/test_deplete_operator.py @@ -0,0 +1,72 @@ +"""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 remove +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().cwd() / "tests" / "chain_simple.xml" + + +@pytest.fixture(scope="module") +def bare_xs(): + """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 + remove(BARE_XS_FILE) + + +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 From 3c223248a80043b12e700b09f1e32ebf54f6ebd3 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 11 Jun 2019 11:51:34 -0400 Subject: [PATCH 03/10] Support Path objects for DataLibrary.register_filename HDF5 File object supports reading from file-like objects, which includes pathlib.Path. Retain the filename of the path in the libraries list --- openmc/data/library.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openmc/data/library.py b/openmc/data/library.py index 905dbbe4a7..e090b2062f 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,20 @@ class DataLibrary(EqualityMixin): Parameters ---------- - filename : str + filename : str or Path Path to the file to be registered. """ - # 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: + with h5py.File(path, 'r') as h5file: filetype = h5file.attrs['filetype'].decode()[5:] materials = list(h5file) - library = {'path': filename, 'type': filetype, 'materials': materials} + library = {'path': path.name, 'type': filetype, 'materials': materials} self.libraries.append(library) def export_to_xml(self, path='cross_sections.xml'): From ac4056e96d267976d48b3bae2b1a6e23e7367a2a Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 11 Jun 2019 12:23:38 -0400 Subject: [PATCH 04/10] Support depletion_chain in DataLibrary to/from xml and register DataLibrary.from_xml looks for depletion_chain node in the cross sections xml file. If found the path to this file is added to the list of libraries. An empty list of materials is used for consistency with other libraries. Type and path attributes are consistent. DataLibrary.export_to_xml does not write the materials attribute if the value stored in that library evaluates to False, e.g. for the empty list stored in the depletion chain. DataLibrary.register_file has a conditional wherein a passed xml file is treated as the depletion_chain and hdf5 files are treated as material/isotopic cross section files. --- openmc/data/library.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/openmc/data/library.py b/openmc/data/library.py index e090b2062f..d1a0d778d9 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -51,6 +51,8 @@ class DataLibrary(EqualityMixin): ---------- filename : str or Path Path to the file to be registered. + If an ``xml`` file, treat as the depletion chain file without + materials. """ if not isinstance(filename, pathlib.Path): @@ -58,9 +60,17 @@ class DataLibrary(EqualityMixin): else: path = filename - with h5py.File(path, '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': path.name, 'type': filetype, 'materials': materials} self.libraries.append(library) @@ -88,7 +98,8 @@ class DataLibrary(EqualityMixin): for library in self.libraries: lib_element = ET.SubElement(root, "library") - lib_element.set('materials', ' '.join(library['materials'])) + if library['materials']: + lib_element.set('materials', ' '.join(library['materials'])) lib_element.set('path', os.path.relpath(library['path'], common_dir)) lib_element.set('type', library['type']) @@ -108,7 +119,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 ------- @@ -148,4 +159,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 From 3429ab21b286a23fe091a73af9dea29a4c228314 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 11 Jun 2019 13:36:03 -0400 Subject: [PATCH 05/10] Retrieve depletion_chain for TransportOperator using DataLibrary --- openmc/deplete/abc.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 14dd3638ce..d25d1d74f2 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -11,6 +11,7 @@ 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']) @@ -65,19 +66,17 @@ class TransportOperator(metaclass=ABCMeta): if chain_file is None: chain_file = os.environ.get("OPENMC_DEPLETE_CHAIN", None) if chain_file is None: - xs_file = os.environ.get("OPENMC_CROSS_SECTIONS", None) - if xs_file is None: - raise IOError( - "OPENMC_CROSS_SECTIONS environment variable " - "not set.") - root = ET.parse(xs_file).getroot() - node = root.find("depletion_chain") - if node is None: + data = DataLibrary.from_xml() + # search for depletion_chain path from end of list + for lib in data.libraries[::-1]: + 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 = node.get("path") + chain_file = lib['path'] else: warn("Use of OPENMC_DEPLETE_CHAIN is deprecated in favor " "of adding depletion_chain to OPENMC_CROSS_SECTIONS", From 0c0e591cb749663e949284b94eae8ad6b3db0146 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 11 Jun 2019 13:54:13 -0400 Subject: [PATCH 06/10] Simple changes to address reviewer comments Re-organize some imports, use run_in_tmpdir fixture, use FutureWarning over DeprecationWarning --- openmc/deplete/abc.py | 2 +- tests/unit_tests/test_deplete_operator.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index d25d1d74f2..226877c149 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -80,7 +80,7 @@ class TransportOperator(metaclass=ABCMeta): else: warn("Use of OPENMC_DEPLETE_CHAIN is deprecated in favor " "of adding depletion_chain to OPENMC_CROSS_SECTIONS", - DeprecationWarning) + FutureWarning) self.chain = Chain.from_xml(chain_file) @abstractmethod diff --git a/tests/unit_tests/test_deplete_operator.py b/tests/unit_tests/test_deplete_operator.py index c188ec94c2..757b51d923 100644 --- a/tests/unit_tests/test_deplete_operator.py +++ b/tests/unit_tests/test_deplete_operator.py @@ -4,21 +4,20 @@ Modifies and resets environment variable OPENMC_CROSS_SECTIONS to a custom file with new depletion_chain node """ -from os import remove from os import environ from unittest import mock from pathlib import Path -import pytest +import pytest from openmc.deplete.abc import TransportOperator from openmc.deplete.chain import Chain BARE_XS_FILE = "bare_cross_sections.xml" -CHAIN_PATH = Path().cwd() / "tests" / "chain_simple.xml" +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" -@pytest.fixture(scope="module") -def bare_xs(): +@pytest.fixture() +def bare_xs(run_in_tmpdir): """Create a very basic cross_sections file, return simple Chain. """ @@ -33,7 +32,6 @@ def bare_xs(): out.write(bare_xs_contents) yield - remove(BARE_XS_FILE) class BareDepleteOperator(TransportOperator): From f1e930b8bab460fa879b63df6a198f3db54fc2bb Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 13 Jun 2019 19:06:07 -0400 Subject: [PATCH 07/10] Remove mention of OPENMC_DEPLETE_CHAIN in model.py --- openmc/deplete/abc.py | 2 +- openmc/model/model.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 226877c149..2e598fb74a 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -68,7 +68,7 @@ class TransportOperator(metaclass=ABCMeta): if chain_file is None: data = DataLibrary.from_xml() # search for depletion_chain path from end of list - for lib in data.libraries[::-1]: + for lib in reversed(data.libraries): if lib['type'] == 'depletion_chain': break else: diff --git a/openmc/model/model.py b/openmc/model/model.py index cf6603638e..bdd35079d4 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_SECITON` environment variable if it exists. method : str Integration method used for depletion (e.g., 'cecm', 'predictor') **kwargs From 52ff408651813f4c82b0ce76c2f32031028cc41e Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 13 Jun 2019 19:32:35 -0400 Subject: [PATCH 08/10] Better exporting/importing of depletion_chain in DataLibrary Use str(path) in the path attribute when registering. This preserves the full path, not just the basename. Give special depletion_chain tag when exporting a library of type depletion_chain, rather than using previous library tag --- openmc/data/library.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openmc/data/library.py b/openmc/data/library.py index d1a0d778d9..d5f18aa170 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -72,7 +72,7 @@ class DataLibrary(EqualityMixin): "File type {} not supported by {}" .format(path.name, self.__class__.__name__)) - library = {'path': path.name, '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'): @@ -97,8 +97,10 @@ class DataLibrary(EqualityMixin): dir_element.text = os.path.realpath(common_dir) for library in self.libraries: - lib_element = ET.SubElement(root, "library") - if 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']) From e702b5186c0aa0303f7422bb3ce56d3faad8f2ab Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 13 Jun 2019 19:35:15 -0400 Subject: [PATCH 09/10] Add test for working with depletion_chains in DataLibrary Load the system default cross section file, register a depletion chain, and inspect the results. Should have one additional library, with an empty list of materials. Export and create a new library from this exported xml. Examine the contents of the new depletion chain library --- tests/unit_tests/test_data_misc.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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) From ed6c78a0e7e0b0b1d7bd8d399bae367f2b85b4b6 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 14 Jun 2019 13:43:22 -0400 Subject: [PATCH 10/10] Fix documentation in openmc/model/model.py --- openmc/model/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index bdd35079d4..f66bd8f16d 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -135,7 +135,7 @@ class Model(object): chain_file : str, optional Path to the depletion chain XML file. Defaults to the chain found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECITON` environment variable if it exists. + :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. method : str Integration method used for depletion (e.g., 'cecm', 'predictor') **kwargs