diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index ac5c02aa501..f63e4577a9e 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -17,7 +17,8 @@ import lxml.etree as ET import scipy.sparse as sp - +import xmlschema + from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.data import gnds_name, zam from openmc.exceptions import DataError @@ -26,6 +27,8 @@ import openmc.data +# Name of the XML schema file +XML_SCHEMA = "openmc_chain_schemas.xsd" # tuple of (possible MT values, secondaries) ReactionInfo = namedtuple('ReactionInfo', ('mts', 'secondaries')) @@ -544,7 +547,7 @@ def from_xml(cls, filename, fission_q=None): """ chain = cls() - + Chain.validate(filename) if fission_q is not None: check_type("fission_q", fission_q, Mapping) else: @@ -1043,19 +1046,23 @@ def fission_yields(self, yields): yields = [yields] check_type("fission_yields", yields, Iterable, Mapping) self._fission_yields = yields - - def validate(self, strict=True, quiet=False, tolerance=1e-4): + + @staticmethod + def validate(filename, strict=True, quiet=False, tolerance = None): """Search for possible inconsistencies The following checks are performed for all nuclides present: - - 1) For all non-fission reactions, does the sum of branching + + 1) The XML file structure is validated against an XMLSchema + 2) For all non-fission reactions, does the sum of branching ratios equal about one? - 2) For fission reactions, does the sum of fission yield + 3) For fission reactions, does the sum of fission yield fractions equal about two? Parameters ---------- + filename : str + Path of the XML file that needs to be validated strict : bool, optional Raise exceptions at the first inconsistency if true. Otherwise mark a warning @@ -1084,16 +1091,40 @@ def validate(self, strict=True, quiet=False, tolerance=1e-4): -------- openmc.deplete.Nuclide.validate """ - check_type("tolerance", tolerance, Real) - check_greater_than("tolerance", tolerance, 0.0, True) + msg_func = ("Nuclide {name} caused the following error: {e}").format + xml_tree = ET.parse(filename) + schema_info = Path(__file__).parent / XML_SCHEMA valid = True - # Sort through nuclides by name - for name in sorted(self.nuclide_dict): - stat = self[name].validate(strict, quiet, tolerance) - if quiet and not stat: - return stat - valid = valid and stat - return valid + if tolerance: + check_type("tolerance", tolerance, Real) + check_greater_than("tolerance", tolerance, 0.0, True) + xsd_text = schema_info.read_text() + schema_info = re.sub(r"0\.0001", str(tolerance), xsd_text) + + schema = xmlschema.XMLSchema11(schema_info) + # Get all the validation errors + errors = list(schema.iter_errors(xml_tree)) + if errors: + for e in errors: + # Get the element that caused the error + elem = e.elem + parent = elem + # Find parent Nuclide tag + while parent is not None and parent.tag != "nuclide": + parent = parent.getparent() + # Get Nuclide name + name = parent.get("name") + msg = msg_func(name=name, e=e.message) + if strict: + raise ValueError(msg) + elif quiet: + valid = False + continue + else: + warn(msg) + valid = False + + return valid def reduce(self, initial_isotopes, level=None): """Reduce the size of the chain by following transmutation paths diff --git a/openmc/deplete/openmc_chain_schemas.xsd b/openmc/deplete/openmc_chain_schemas.xsd new file mode 100644 index 00000000000..0684b173d53 --- /dev/null +++ b/openmc/deplete/openmc_chain_schemas.xsd @@ -0,0 +1,142 @@ + + + + + Author: Lorenzo Canzian, newcleo Spa, Italy + Date: 2025-09-25 + + + + + + + + Inside the depletion chain (root tag) one can have unlimited nuclide tags + + + + + + + + + + + + + + + + Each nuclide can have multiple possibile interactions, so there are unlimited choices between different kind of tags + + + + Decay tag contains informations about the decay channel of the nuclide. We have three attributes: + type of decay, target produced, branching ratio. Type of decays include (name, (A_change, Z_change)) + 0: ('gamma', (0, 0)), + 1: ('beta-', (0, 1)), + 2: ('ec/beta+', (0, -1)), + 3: ('IT', (0, 0)), + 4: ('alpha', (-4, -2)), + 5: ('n', (-1, 0)), + 6: ('sf', None), + 7: ('p', (-1, -1)), + 8: ('e-', (0, 0)), + 9: ('xray', (0, 0)), + 10: ('unknown', None) + + + + + + + + + + + + Reaction tag contains informations about the transmutation channel of the nuclide. There are four possible attributes: + type of reaction, target produced, Q value, branching ratio. Reactions are written as (n,2n), (n,fission),(n,gamma),(n,p), etc... + + + + + + + + + + + + + Source tag contains informations about other particles produced during decays of the nuclide. There are three attributes: + type, particle produced, interpolation. We also have another nested tag named parameter which contain the parameters linked to the particle emission + + + + + + + First half are photon energies (in eV), second half are intensities (probabilities of emission) + + + + + + + + + + + + + + + Fission_yields tag contains informations about the products of a fission and the relative fission yields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2d67e834012..fe71f78a12e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "uncertainties", "setuptools", "endf", + "xmlschema" ] [project.optional-dependencies] diff --git a/tests/chain_simple.xml b/tests/chain_simple.xml index 4e08995a8dc..54cf02d9883 100644 --- a/tests/chain_simple.xml +++ b/tests/chain_simple.xml @@ -28,7 +28,7 @@ 2.53000e-02 Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 + 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 1.9411915633 @@ -41,7 +41,7 @@ 2.53000e-02 Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 + 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 1.9460259097 @@ -53,8 +53,8 @@ 2.53000e-02 - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 + Gd157 Gd156 I135 Xe135 Xe136 Cs135 Am241 + 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 1.982567371 diff --git a/tests/chain_simple_decay.xml b/tests/chain_simple_decay.xml index 1c8c5e4eecc..1d78129b781 100644 --- a/tests/chain_simple_decay.xml +++ b/tests/chain_simple_decay.xml @@ -33,7 +33,7 @@ 2.53000e-02 Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 + 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 1.94119120899 @@ -46,7 +46,7 @@ 2.53000e-02 Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 + 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 1.9458259979 @@ -59,7 +59,7 @@ 2.53000e-02 Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 + 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 1.982567812 diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index e90b6102241..7964d89f0cb 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -7,6 +7,7 @@ from pathlib import Path import warnings +import lxml.etree as ET import numpy as np from openmc.mpi import comm from openmc.deplete import Chain, reaction_rates, nuclide, cram, pool @@ -37,7 +38,7 @@ 0.0253 A B - 0.0292737 0.002566345 + 0.4 1.6 @@ -155,7 +156,7 @@ def test_from_xml(simple_chain): assert nuc.yield_energies == (0.0253,) assert list(nuc.yield_data) == [0.0253] assert nuc.yield_data[0.0253].products == ("A", "B") - assert (nuc.yield_data[0.0253].yields == [0.0292737, 0.002566345]).all() + assert (nuc.yield_data[0.0253].yields == [0.4, 1.6]).all() def test_export_to_xml(run_in_tmpdir): @@ -192,7 +193,7 @@ def test_export_to_xml(run_in_tmpdir): nuclide.ReactionTuple("(n,gamma)", "B", 0.0, 0.3) ] C.yield_data = nuclide.FissionYieldDistribution({ - 0.0253: {"A": 0.0292737, "B": 0.002566345}}) + 0.0253: {"A": 0.4, "B": 1.6}}) chain = Chain() chain.nuclides = [H1, A, B, C] @@ -231,8 +232,8 @@ def test_form_matrix(simple_chain): mat[2, 2] = -decay_constant - 3.2 # Loss B, decay, (n,gamma), (n,d) mat[3, 2] = 3 # B -> C, (n,gamma) - mat[1, 3] = 0.0292737 * 1.0 + 4.0 * 0.7 # C -> A fission, (n,gamma) - mat[2, 3] = 0.002566345 * 1.0 + 4.0 * 0.3 # C -> B fission, (n,gamma) + mat[1, 3] = 0.4 * 1.0 + 4.0 * 0.7 # C -> A fission, (n,gamma) + mat[2, 3] = 1.6 * 1.0 + 4.0 * 0.3 # C -> B fission, (n,gamma) mat[3, 3] = -1.0 - 4.0 # Loss C, fission, (n,gamma) sp_matrix = chain.form_matrix(react[0]) @@ -403,7 +404,7 @@ def test_simple_fission_yields(simple_chain): """Check the default fission yields that can be used to form the matrix """ fission_yields = simple_chain.get_default_fission_yields() - assert fission_yields == {"C": {"A": 0.0292737, "B": 0.002566345}} + assert fission_yields == {"C": {"A":0.4, "B": 1.6}} def test_fission_yield_attribute(simple_chain): @@ -426,56 +427,66 @@ def test_fission_yield_attribute(simple_chain): pool.deplete(cram.CRAM48, empty_chain, dummy_conc, None, 0.5) -def test_validate(simple_chain): +def test_validate(): """Test the validate method""" - + filename = 'chain_test.xml' # current chain is invalid # fission yields do not sum to 2.0 - with pytest.raises(ValueError, match="Nuclide C.*fission yields"): - simple_chain.validate(strict=True, tolerance=0.0) + assert Chain.validate(filename, strict=True, quiet=False) + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert Chain.validate(filename, strict=False, quiet=False) + + tree = ET.parse(filename) + root = tree.getroot() + for nuclide in root.findall('nuclide'): + if nuclide.attrib.get("name") == "C": + data_elem = nuclide.find('.//data') + if data_elem is not None: + data_elem.text = "0.0292737 0.002566345" + break + tree.write(filename, encoding="utf-8", xml_declaration=True) + + with pytest.raises(ValueError, match="Nuclide C .*error:"): + Chain.validate(filename, strict=True) with pytest.warns(UserWarning) as record: - assert not simple_chain.validate(strict=False, quiet=False, tolerance=0.0) - assert not simple_chain.validate(strict=False, quiet=True, tolerance=0.0) + assert not Chain.validate(filename, strict=False, quiet=False) + assert not Chain.validate(filename, strict=False, quiet=True) assert len(record) == 1 assert "Nuclide C" in record[0].message.args[0] - # Fix fission yields but keep to restore later - old_yields = simple_chain["C"].yield_data - simple_chain["C"].yield_data = {0.0253: {"A": 1.4, "B": 0.6}} - - assert simple_chain.validate(strict=True, tolerance=0.0) - with warnings.catch_warnings(): - warnings.simplefilter("error") - assert simple_chain.validate(strict=False, quiet=False, tolerance=0.0) - # Mess up "earlier" nuclide's reactions - decay_mode = simple_chain["A"].decay_modes.pop() - - with pytest.raises(ValueError, match="Nuclide A.*decay mode"): - simple_chain.validate(strict=True, tolerance=0.0) - - # restore old fission yields - simple_chain["C"].yield_data = old_yields + for nuclide in root.findall('nuclide'): + if nuclide.attrib.get("name") == "A": + inner_tag = nuclide.find('decay') + if inner_tag is not None: + nuclide.remove(inner_tag) + break + tree.write(filename, encoding="utf-8", xml_declaration=True) + + with pytest.raises(ValueError, match="Nuclide A .*error"): + Chain.validate(filename, strict=True) with pytest.warns(UserWarning) as record: - assert not simple_chain.validate(strict=False, quiet=False, tolerance=0.0) + assert not Chain.validate(filename, strict=False, quiet=False) assert len(record) == 2 assert "Nuclide A" in record[0].message.args[0] assert "Nuclide C" in record[1].message.args[0] - # restore decay modes - simple_chain["A"].decay_modes.append(decay_mode) + # restore chain + chain_file = Path("chain_test.xml") + chain_file.write_text(_TEST_CHAIN, encoding="utf-8") def test_validate_inputs(): - c = Chain() - + filename = 'chain_test.xml' + with pytest.raises(TypeError, match="tolerance"): - c.validate(tolerance=None) + Chain.validate(filename, tolerance = "Unknown") with pytest.raises(ValueError, match="tolerance"): - c.validate(tolerance=-1) + Chain.validate(filename, tolerance = -1) @pytest.fixture diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 60f8b1a25a3..603f6b40d29 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -91,13 +91,14 @@ def pin_model_attributes(): chain_file_xml = """ + 2.53000e-02 - Xe136 - 1.0 + Xe135 Xe136 + 1.0 1.0