From badaaacca447ead3a978481dde3de71e5ee17d2f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 11 Jan 2022 17:50:50 -0600 Subject: [PATCH] Consistent use of cross sections / chain file in Operator class --- openmc/deplete/abc.py | 27 +------- openmc/deplete/chain.py | 22 ++++++- openmc/deplete/operator.py | 75 ++++++++++------------- tests/unit_tests/test_deplete_operator.py | 14 ++--- 4 files changed, 60 insertions(+), 78 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index b9198696d..3c7a13687 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -19,7 +19,6 @@ from warnings import warn from numpy import nonzero, empty, asarray from uncertainties import ufloat -from openmc.data import DataLibrary from openmc.lib import MaterialFilter, Tally from openmc.checkvalue import check_type, check_greater_than from openmc.mpi import comm @@ -70,10 +69,8 @@ class TransportOperator(ABC): Parameters ---------- - chain_file : str, optional - Path to the depletion chain XML file. Defaults to the file - listed under ``depletion_chain`` in - :envvar:`OPENMC_CROSS_SECTIONS` environment variable. + chain_file : str + Path to the depletion chain XML file fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. @@ -95,30 +92,12 @@ class TransportOperator(ABC): Results from a previous depletion calculation. ``None`` if no results are to be used. """ - def __init__(self, chain_file=None, fission_q=None, dilute_initial=1.0e3, + def __init__(self, chain_file, fission_q=None, dilute_initial=1.0e3, prev_results=None): self.dilute_initial = dilute_initial self.output_dir = '.' # Read depletion chain - if chain_file is None: - chain_file = os.environ.get("OPENMC_DEPLETE_CHAIN", None) - if chain_file is None: - 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, fission_q) if prev_results is None: self.prev_res = None diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 0c39a92e3..0c1c34cf7 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -7,6 +7,7 @@ loaded from an .xml file and all the nuclides are linked together. from io import StringIO from itertools import chain import math +import os import re from collections import OrderedDict, defaultdict, namedtuple from collections.abc import Mapping, Iterable @@ -14,7 +15,8 @@ from numbers import Real, Integral from warnings import warn from openmc.checkvalue import check_type, check_greater_than -from openmc.data import gnd_name, zam +from openmc.data import gnd_name, zam, DataLibrary +from openmc.exceptions import DataError from .nuclide import FissionYieldDistribution # Try to use lxml if it is available. It preserves the order of attributes and @@ -237,6 +239,24 @@ def replace_missing_fpy(actinide, fpy_data, decay_data): return 'U235' +def _find_chain_file(cross_sections=None): + # First check deprecated OPENMC_DEPLETE_CHAIN environment variable + chain_file = os.environ.get("OPENMC_DEPLETE_CHAIN") + if chain_file is not None: + warn("Use of OPENMC_DEPLETE_CHAIN is deprecated in favor of adding " + "depletion_chain to OPENMC_CROSS_SECTIONS", FutureWarning) + return chain_file + + # Check for depletion chain in cross_sections.xml + data = DataLibrary.from_xml(cross_sections) + for lib in reversed(data.libraries): + if lib['type'] == 'depletion_chain': + return lib['path'] + + raise DataError("No depletion chain specified and could not find depletion " + f"chain in {cross_sections}") + + class Chain: """Full representation of a depletion chain. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 7e3b34d0c..b5e377bed 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -10,20 +10,20 @@ densities is all done in-memory instead of through the filesystem. import copy from collections import OrderedDict import os -from typing import Type -import xml.etree.ElementTree as ET from warnings import warn -from pathlib import Path import numpy as np from uncertainties import ufloat import openmc from openmc.checkvalue import check_value +from openmc.data import DataLibrary +from openmc.exceptions import DataError import openmc.lib from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber +from .chain import _find_chain_file from .reaction_rates import ReactionRates from .results_list import ResultsList from .helpers import ( @@ -58,6 +58,22 @@ def _distribute(items): j += chunk_size +def _find_cross_sections(model): + """Determine cross sections to use for depletion""" + if model.materials and model.materials.cross_sections is not None: + # Prefer info from Model class if available + return model.materials.cross_sections + + # otherwise fallback to environment variable + cross_sections = os.environ.get("OPENMC_CROSS_SECTIONS") + if cross_sections is None: + raise DataError( + "Cross sections were not specified in Model.materials and " + "the OPENMC_CROSS_SECTIONS environment variable is not set." + ) + return cross_sections + + class Operator(TransportOperator): """OpenMC transport operator for depletion. @@ -206,6 +222,11 @@ class Operator(TransportOperator): "to generate the depletion Operator." raise TypeError(msg) + # Determine cross sections / depletion chain + cross_sections = _find_cross_sections(model) + if chain_file is None: + chain_file = _find_chain_file(cross_sections) + check_value('fission yield mode', fission_yield_mode, self._fission_helpers.keys()) check_value('normalization mode', normalization_mode, @@ -270,7 +291,7 @@ class Operator(TransportOperator): self.prev_res.append(new_res) # Determine which nuclides have incident neutron data - self.nuclides_with_data = self._get_nuclides_with_data() + self.nuclides_with_data = self._get_nuclides_with_data(cross_sections) # Select nuclides with data that are also in the chain self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides @@ -637,14 +658,6 @@ class Operator(TransportOperator): for mat in self.materials: mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) - # Grab the cross sections tag from the existing file - mfile = Path("materials.xml") - if mfile.exists(): - tree = ET.parse(str(mfile)) - xs = tree.find('cross_sections') - if xs is not None and self.materials.cross_sections is None: - self.materials.cross_sections = xs.text - self.materials.export_to_xml() def _get_tally_nuclides(self): @@ -768,40 +781,14 @@ class Operator(TransportOperator): return OperatorResult(k_combined, rates) - def _get_nuclides_with_data(self): - """Loads a cross_sections.xml file to find participating nuclides. - - This allows for nuclides that are important in the decay chain but not - important neutronically, or have no cross section data. - """ - - # Reads cross_sections.xml to create a dictionary containing - # participating (burning and not just decaying) nuclides. - - try: - filename = os.environ["OPENMC_CROSS_SECTIONS"] - except KeyError: - filename = None - + def _get_nuclides_with_data(self, cross_sections): + """Loads cross_sections.xml file to find nuclides with neutron data""" nuclides = set() - - try: - tree = ET.parse(filename) - except Exception: - if filename is None: - msg = "No cross_sections.xml specified in materials." - else: - msg = 'Cross section file "{}" is invalid.'.format(filename) - raise IOError(msg) - - root = tree.getroot() - for nuclide_node in root.findall('library'): - mats = nuclide_node.get('materials') - if not mats: + data_lib = DataLibrary.from_xml(cross_sections) + for library in data_lib.libraries: + if library['type'] != 'neutron': continue - for name in mats.split(): - # Make a burn list of the union of nuclides in cross_sections.xml - # and nuclides in depletion chain. + for name in library['materials']: if name not in nuclides: nuclides.add(name) diff --git a/tests/unit_tests/test_deplete_operator.py b/tests/unit_tests/test_deplete_operator.py index 3574d293d..7d24f1428 100644 --- a/tests/unit_tests/test_deplete_operator.py +++ b/tests/unit_tests/test_deplete_operator.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest from openmc.deplete.abc import TransportOperator -from openmc.deplete.chain import Chain +from openmc.deplete.chain import Chain, _find_chain_file BARE_XS_FILE = "bare_cross_sections.xml" CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" @@ -31,7 +31,7 @@ def bare_xs(run_in_tmpdir): with open(BARE_XS_FILE, "w") as out: out.write(bare_xs_contents) - yield + yield BARE_XS_FILE class BareDepleteOperator(TransportOperator): @@ -54,14 +54,10 @@ class BareDepleteOperator(TransportOperator): 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) + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + bare_op = BareDepleteOperator(_find_chain_file(bare_xs)) act_chain = bare_op.chain ref_chain = Chain.from_xml(CHAIN_PATH) assert len(act_chain) == len(ref_chain)