# Copyright 2026 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Basic tests of the Composition class."""
import os
import unittest
from ruamel.yaml.constructor import DuplicateKeyError
import armi.matProps
from armi.matProps.constituent import Constituent
from armi.matProps.material import MatPropsMaterial
[docs]
class TestComposition(unittest.TestCase):
def setUp(self):
self.testName = self.id().split(".")[-1]
searchStr = "test_"
if self.testName.startswith(searchStr):
self.testName = self.testName[len(searchStr) :]
def _createFunction(self, compMap=None):
compValue = {}
if compMap is not None:
compValue = compMap
materialMap = {
"file format": "TESTS",
"composition": compValue,
"material type": "Metal",
"density": {
"function": {
"T": {"min": 100.0, "max": 200.0},
"type": "symbolic",
"equation": 1.0,
}
},
}
mat = MatPropsMaterial()
mat.loadNode(materialMap)
return mat
[docs]
def test_compositionMissing(self):
materialMap = {
"file format": "TESTS",
"material type": "Metal",
"density": "whatever",
}
mat = MatPropsMaterial()
with self.assertRaisesRegex(KeyError, "Missing YAML node `composition`"):
mat.loadNode(materialMap)
[docs]
def test_compositionVoid(self):
node = {"file format": "TESTS", "material type": "Metal", "density": "whatever", "composition": {"V": "void"}}
mat = MatPropsMaterial()
c = Constituent.parseComposition(mat.getNode(node, "composition"))
self.assertEqual(len(c), 0)
[docs]
def test_compositionInvTuple(self):
# Invalid doesn't have two elements
badCompMap = {"Fe": [1.0]}
with self.assertRaisesRegex(
TypeError,
"Composition values must be either a tuple of min/max values, or `balance`",
):
self._createFunction(badCompMap)
[docs]
def test_compositionInvStr(self):
badCompMap = {"a": [0.5, 0.5], "b": "remainder"}
with self.assertRaisesRegex(
TypeError,
"Composition values must be either a tuple of min/max values, or `balance`",
):
self._createFunction(badCompMap)
[docs]
def test_compositionMissBalance(self):
compMap = {"a": [0.25, 0.26], "b": [0.3, 0.31], "c": [0.45, 0.46]}
with self.assertRaisesRegex(ValueError, "exactly one balance element"):
self._createFunction(compMap)
[docs]
def test_compositionBalanceNum(self):
compMap = {"a": [15.0, 15.1], "b": "balance", "c": "balance"}
with self.assertRaisesRegex(ValueError, "exactly one balance element"):
self._createFunction(compMap)
[docs]
def test_compositionBalance(self):
compMap = {"a": [15.0, 20.0], "b": [30.0, 35.0], "c": "balance"}
mat = self._createFunction(compMap)
mat.name = self.testName
self.assertEqual(str(mat), f"<MatPropsMaterial {self.testName} <MaterialType Metal>>")
c_minValue, c_maxValue = None, None
sumMin, sumMax = 0.0, 0.0
for compElement in mat.composition:
if compElement.name != "c":
self.assertFalse(compElement.isBalance)
compValue = compMap.get(compElement.name)
self.assertIsNotNone(compValue)
self.assertAlmostEqual(compElement.minValue, compValue[0])
self.assertAlmostEqual(compElement.maxValue, compValue[1])
sumMin += compElement.minValue
sumMax += compElement.maxValue
else:
self.assertTrue(compElement.isBalance)
c_minValue = compElement.minValue
c_maxValue = compElement.maxValue
self.assertAlmostEqual(c_minValue, 100.0 - sumMax)
self.assertAlmostEqual(c_maxValue, 100.0 - sumMin)
[docs]
def test_compositionBalance2(self):
compMap = {
"a": [10.0, 15.0],
"b": [20.1, 35.1],
"c": [30.2, 50.2],
"d": "balance",
}
mat = self._createFunction(compMap)
mat.name = self.testName
self.assertEqual(str(mat), f"<MatPropsMaterial {self.testName} <MaterialType Metal>>")
sumMin = 0.0
d_minValue, d_maxValue = None, None
for compElement in mat.composition:
if compElement.name != "d":
self.assertFalse(compElement.isBalance)
compValue = compMap.get(compElement.name)
self.assertIsNotNone(compValue)
self.assertAlmostEqual(compElement.minValue, compValue[0])
self.assertAlmostEqual(compElement.maxValue, compValue[1])
sumMin += compElement.minValue
else:
self.assertTrue(compElement.isBalance)
d_minValue = compElement.minValue
d_maxValue = compElement.maxValue
self.assertAlmostEqual(d_minValue, 0.0)
self.assertAlmostEqual(d_maxValue, 100.0 - sumMin)
[docs]
def test_compositionMinValue(self):
compMap = {"a": [-1.0, 20.0], "b": "balance"}
with self.assertRaisesRegex(ValueError, "negative minimum"):
self._createFunction(compMap)
[docs]
def test_compositionMaxValue(self):
compMap = {"a": [15.0, 14.9], "b": "balance"}
with self.assertRaisesRegex(ValueError, "max < min"):
self._createFunction(compMap)
[docs]
def test_compositionMaxValue2(self):
compMap = {"a": [15.0, 100.1], "b": "balance"}
with self.assertRaisesRegex(ValueError, "max > 100.0"):
self._createFunction(compMap)
[docs]
def test_compositionMinSum(self):
compMap = {
"a": [30.0, 30.1],
"b": [40.1, 40.2],
"c": [50.2, 50.3],
"d": "balance",
}
with self.assertRaisesRegex(ValueError, "minimum composition summation greater than 100.0"):
self._createFunction(compMap)
[docs]
def test_compositionDuplicate(self):
duplicateTestFile = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"invalidTestFiles",
"duplicateComposition.yaml",
)
with self.assertRaises(DuplicateKeyError):
armi.matProps.loadMaterial(duplicateTestFile)