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