# Copyright 2019 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.
"""Tests the blueprints (loading input) file."""
import io
import os
import pathlib
import unittest
import yamlize
from armi import settings
from armi.nucDirectory.elements import bySymbol
from armi.physics.neutronics.settings import CONF_XS_KERNEL
from armi.reactor import blueprints
from armi.reactor import parameters
from armi.reactor.blueprints.componentBlueprint import ComponentBlueprint
from armi.reactor.blueprints.isotopicOptions import NuclideFlags, CustomIsotopics
from armi.reactor.flags import Flags
from armi.tests import TEST_ROOT
from armi.utils import directoryChangers
from armi.utils import textProcessors
from armi.reactor.blueprints.gridBlueprint import saveToStream
[docs]class TestBlueprints(unittest.TestCase):
"""Test that the basic functionality of faithfully receiving user input to construct
ARMI data model objects works as expected.
Values are hopefully not hardcoded in here, just sanity checks that nothing messed
up as this is code has VERY high incidental coverage from other tests.
NOTE: as it stands it seems a little hard to test more granularity with the
blueprints file as each initialization is intended to be a complete load from the
input file, and each load also
makes calls out to the reactor for some assembly initialization steps.
TODO: see the above note, and try to test blueprints on a wider range of input
files, touching on each failure case.
"""
@classmethod
def setUpClass(cls):
cls.cs = settings.Settings()
cls.directoryChanger = directoryChangers.DirectoryChanger(TEST_ROOT)
cls.directoryChanger.open()
y = textProcessors.resolveMarkupInclusions(
pathlib.Path(os.getcwd()) / "refSmallReactor.yaml"
)
cls.blueprints = blueprints.Blueprints.load(y)
cls.blueprints._prepConstruction(cls.cs)
@classmethod
def tearDownClass(cls):
cls.directoryChanger.close()
@staticmethod
def __stubify(latticeMap):
"""Little helper method to allow lattie maps to be compared free of whitespace."""
return latticeMap.replace(" ", "").replace("-", "").replace("\n", "")
[docs] def test_roundTripCompleteBP(self):
"""Test the round-tip of reading and writing blueprint files.
.. test:: Validates the round trip of reading and writing blueprints.
:id: T_ARMI_BP_TO_DB1
:tests: R_ARMI_BP_TO_DB
"""
# the correct lattice map
latticeMap = """- - SH
- SH SH
- SH OC SH
SH OC OC SH
OC IC OC SH
OC IC IC OC SH
IC IC IC OC SH
IC IC PC OC SH
IC PC IC IC OC SH
LA IC IC IC OC
IC IC IC IC SH
IC LB IC IC OC
IC IC PC IC SH
LA IC IC OC
IC IC IC IC SH
IC IC IC OC
IC IC IC PC SH"""
latticeMap = self.__stubify(latticeMap)
# validate some core elements from the blueprints
self.assertEqual(self.blueprints.gridDesigns["core"].symmetry, "third periodic")
map0 = self.__stubify(self.blueprints.gridDesigns["core"].latticeMap)
self.assertEqual(map0, latticeMap)
# save the blueprint to a stream
stream = io.StringIO()
stream.seek(0)
self.blueprints.dump(self.blueprints)
saveToStream(stream, self.blueprints, True, True)
stream.seek(0)
with directoryChangers.TemporaryDirectoryChanger():
# save the stream to a file
filePath = "test_roundTripCompleteBP.yaml"
with open(filePath, "w") as fout:
fout.write(stream.read())
# load the blueprint from that file again
bp = blueprints.Blueprints.load(open(filePath, "r").read())
# re-validate some core elements from the blueprints
self.assertEqual(bp.gridDesigns["core"].symmetry, "third periodic")
map1 = self.__stubify(bp.gridDesigns["core"].latticeMap)
self.assertEqual(map1, latticeMap)
[docs] def test_nuclides(self):
"""Tests the available sets of nuclides work as expected."""
actives = set(self.blueprints.activeNuclides)
inerts = set(self.blueprints.inertNuclides)
self.assertEqual(
actives.union(inerts), set(self.blueprints.allNuclidesInProblem)
)
self.assertEqual(actives.intersection(inerts), set())
[docs] def test_getAssemblyTypeBySpecifier(self):
aDesign = self.blueprints.assemDesigns.bySpecifier["IC"]
self.assertEqual(aDesign.name, "igniter fuel")
self.assertEqual(aDesign.specifier, "IC")
[docs] def test_specialIsotopicVectors(self):
mox = self.blueprints.customIsotopics["MOX"]
allNucsInProblem = set(self.blueprints.allNuclidesInProblem)
for a in mox.keys():
self.assertIn(a, allNucsInProblem)
self.assertIn("U235", mox)
self.assertAlmostEqual(mox["PU239"], 0.00286038)
[docs] def test_componentDimensions(self):
"""Tests that the user can specify the dimensions of a component with arbitrary fidelity.
.. test:: A component can be correctly created from a blueprint file.
:id: T_ARMI_BP_COMP
:tests: R_ARMI_BP_COMP
"""
fuelAssem = self.blueprints.constructAssem(self.cs, name="igniter fuel")
fuel = fuelAssem.getComponents(Flags.FUEL)[0]
self.assertAlmostEqual(fuel.getDimension("od", cold=True), 0.86602)
self.assertAlmostEqual(fuel.getDimension("id", cold=True), 0.0)
self.assertAlmostEqual(fuel.getDimension("od"), 0.87763665, 4)
self.assertAlmostEqual(fuel.getDimension("id"), 0.0)
self.assertAlmostEqual(fuel.getDimension("mult"), 169)
[docs] def test_traceNuclides(self):
"""Ensure that armi.reactor.blueprints.componentBlueprint.insertDepletableNuclideKeys runs.
.. test:: Users marking components as depletable will affect number densities.
:id: T_ARMI_BP_NUC_FLAGS1
:tests: R_ARMI_BP_NUC_FLAGS
"""
fuel = (
self.blueprints.constructAssem(self.cs, "igniter fuel")
.getFirstBlock(Flags.FUEL)
.getComponent(Flags.FUEL)
)
self.assertIn("AM241", fuel.getNuclides())
self.assertLess(fuel.getNumberDensity("AM241"), 1e-5)
[docs]class TestBlueprintsSchema(unittest.TestCase):
"""Test the blueprint schema checks."""
_yamlString = r"""blocks:
fuel: &block_fuel
fuel: &component_fuel_fuel
shape: Hexagon
material: UZr
Tinput: 25.0
Thot: 600.0
ip: 0.0
mult: 1.0
op: 10.0
fuel2: &block_fuel2
group1:
shape: Group
duct:
shape: Hexagon
material: UZr
Tinput: 25.0
Thot: 600.0
ip: 9.0
mult: 1.0
op: 10.0
matrix:
shape: DerivedShape
material: Graphite
Tinput: 25.0
Thot: 600.0
components:
freefuel:
shape: Sphere
material: UZr
Tinput: 25.0
Thot: 600.0
id: 0.0
mult: 1.0
od: 4.0
freeclad:
shape: Sphere
material: HT9
Tinput: 25.0
Thot: 600.0
id: 4.0
mult: 1.0
od: 4.1
component groups:
group1:
freefuel:
mult: 1.0
freeclad:
mult: 1.0
assemblies:
fuel a: &assembly_a
specifier: IC
blocks: [*block_fuel]
height: [1.0]
axial mesh points: [1]
xs types: [A]
fuel b:
<<: *assembly_a
hotChannelFactors: Reactor
fuel c: &assembly_c
specifier: OC
blocks: [*block_fuel2]
height: [1.0]
axial mesh points: [1]
xs types: [A]
grids:
pins:
geom: cartesian
lattice map: |
2 2 2 2 2
2 1 1 1 2
2 1 3 1 2
2 3 1 1 2
2 2 2 2 2
"""
[docs] def test_noDuplicateKeysInYamlBlueprints(self):
"""
Prove that if you duplicate a section of a YAML blueprint file,
a hard error will be thrown.
"""
# loop through a few different sections, to test blueprints broadly
sections = ["blocks:", "components:", "component groups:"]
for sectionName in sections:
# modify blueprint YAML to duplicate this section
yamlString = str(self._yamlString)
i = yamlString.find(sectionName)
lenSection = yamlString[i:].find("\n\n")
section = yamlString[i : i + lenSection]
yamlString = yamlString[:i] + section + yamlString[i : i + lenSection]
# validate that this is now an invalid YAML blueprint
with self.assertRaises(Exception):
_design = blueprints.Blueprints.load(yamlString)
[docs] def test_assemblyParameters(self):
cs = settings.Settings()
design = blueprints.Blueprints.load(self._yamlString)
fa = design.constructAssem(cs, name="fuel a")
fb = design.constructAssem(cs, name="fuel b")
for paramDef in fa.p.paramDefs.inCategory(
parameters.Category.assignInBlueprints
):
# Semantics of __iter__() and items() is different now in the parameter
# system. Since we aren't using __slots__ anymore, we use the parameter
# definitions (which have a global-ish sense of `assigned`ness), so we can't
# tell, per-object, whether they've been set.
self.assertEqual(paramDef.default, fa.p[paramDef.name])
self.assertIn(paramDef.name, fb.p)
self.assertEqual(fa.p.hotChannelFactors, "Default")
self.assertEqual(fb.p.hotChannelFactors, "Reactor")
[docs] def test_nuclidesMc2v2(self):
"""Tests that ZR is not expanded to its isotopics for this setting."""
cs = settings.Settings()
newSettings = {CONF_XS_KERNEL: "MC2v2"}
cs = cs.modified(newSettings=newSettings)
design = blueprints.Blueprints.load(self._yamlString)
design._prepConstruction(cs)
self.assertTrue(
set({"U238", "U235", "ZR"}).issubset(set(design.allNuclidesInProblem))
)
assem = design.constructAssem(cs, name="fuel a")
self.assertTrue(
set(assem.getNuclides()).issubset(set(design.allNuclidesInProblem))
)
[docs] def test_nuclidesMc2v3(self):
"""Tests that ZR is expanded to its isotopics for MC2v3."""
cs = settings.Settings()
newSettings = {CONF_XS_KERNEL: "MC2v3"}
cs = cs.modified(newSettings=newSettings)
design = blueprints.Blueprints.load(self._yamlString)
design._prepConstruction(cs)
# 93 and 95 are not naturally occurring.
zrNucs = {"ZR" + str(A) for A in range(90, 97)} - {"ZR93", "ZR95"}
self.assertTrue(
set({"U238", "U235"} | zrNucs).issubset(set(design.allNuclidesInProblem))
)
self.assertTrue(zrNucs.issubset(set(design.inertNuclides)))
assem = design.constructAssem(cs, name="fuel a")
# the assembly won't get non-naturally occurring nuclides
unnaturalZr = (n.name for n in bySymbol["ZR"].nuclides if n.abundance == 0.0)
designNucs = set(design.allNuclidesInProblem).difference(unnaturalZr)
self.assertTrue(set(assem.getNuclides()).issubset(designNucs))
[docs] def test_merge(self):
yamlString = r"""
nuclide flags:
B10: {burn: true, xs: true}
B11: {burn: true, xs: true}
DUMP1: {burn: true, xs: true}
FE: {burn: true, xs: true}
NI: {burn: true, xs: true}
C: {burn: true, xs: true}
MO: {burn: true, xs: true}
SI: {burn: true, xs: true}
CR: {burn: true, xs: true}
MN: {burn: true, xs: true}
NA: {burn: true, xs: true}
V: {burn: true, xs: true}
W: {burn: true, xs: true}
blocks:
nomerge block: &unmerged_block
A: &comp_a
shape: Circle
material: B4C
Tinput: 50.0
Thot: 500.0
id: 0.0
mult: 1
od: .5
Gap1: &comp_gap
shape: Circle
material: Void
Tinput: 50.0
Thot: 500.0
id: A.od
mult: 1
od: B.id
B: &gcomp_b
shape: Circle
material: HT9
Tinput: 20.0
Thot: 600.0
id: .5
mult: 1
od: .75
Gap2: &comp_gap2
shape: Circle
material: Void
Tinput: 50.0
Thot: 500.0
id: B.od
mult: 1
od: Clad.id
Clad: &comp_clad
shape: Circle
material: HT9
Tinput: 20.0
Thot: 700.0
id: .75
mult: 1
od: 1.0
coolant: &comp_coolant
shape: DerivedShape
material: Sodium
Tinput: 600.0
Thot: 600.0
duct: &comp_duct
shape: Hexagon
material: HT9
Tinput: 20.0
Thot: 500.0
ip: 1.2
mult: 1
op: 1.4
intercoolant: &comp_intercoolant
shape: Hexagon
material: Sodium
Tinput: 500.0
Thot: 500.0
ip: duct.op
mult: 1
op: 1.6
merge block: &merged_block
A:
<<: *comp_a
mergeWith: Clad
Gap1: *comp_gap
B:
<<: *gcomp_b
mergeWith: Clad
Gap2: *comp_gap2
Clad: *comp_clad
coolant: *comp_coolant
duct: *comp_duct
intercoolant: *comp_intercoolant
assemblies:
a: &assembly_a
specifier: IC
blocks: [*merged_block, *unmerged_block]
height: [1.0, 1.0]
axial mesh points: [1, 1]
xs types: [A, A]
"""
bp = blueprints.Blueprints.load(yamlString)
a = bp.constructAssem(settings.Settings(), name="a")
mergedBlock, unmergedBlock = a
self.assertNotIn("A", mergedBlock.getComponentNames())
self.assertNotIn("B", mergedBlock.getComponentNames())
self.assertEqual(len(mergedBlock) + 4, len(unmergedBlock))
self.assertAlmostEqual(
sum(c.getArea() for c in mergedBlock),
sum(c.getArea() for c in unmergedBlock),
)
mergedNucs, unmergedNucs = (
mergedBlock.getNumberDensities(),
unmergedBlock.getNumberDensities(),
)
errorMessage = ""
for nucName in set(unmergedNucs) | set(mergedNucs):
n1, n2 = unmergedNucs[nucName], mergedNucs[nucName]
try:
self.assertAlmostEqual(n1, n2)
except AssertionError:
errorMessage += "\nnuc {} not equal. unmerged: {} merged: {}".format(
nucName, n1, n2
)
self.assertTrue(not errorMessage, errorMessage)
self.assertAlmostEqual(mergedBlock.getMass(), unmergedBlock.getMass())
[docs] def test_nuclideFlags(self):
with self.assertRaises(yamlize.YamlizingError):
NuclideFlags.load("{potato: {burn: true, xs: true}}")
with self.assertRaises(yamlize.YamlizingError):
NuclideFlags.load("{U238: {burn: 12, xs: 0}}")
[docs] def test_customIsotopics(self):
with self.assertRaises(yamlize.YamlizingError):
CustomIsotopics.load("MOX: {input format: applesauce}")
with self.assertRaises(yamlize.YamlizingError):
CustomIsotopics.load("MOX: {input format: number densities, density: -0.1}")
with self.assertRaises(yamlize.YamlizingError):
CustomIsotopics.load(
"MOX: {input format: number densities, density: 1.5, FAKENUC234: 0.000286}"
)
[docs] def test_components(self):
bads = [
# bad shape
{
"shape": "potato",
"name": "name",
"material": "HT9",
"Tinput": 1.0,
"Thot": 1.0,
},
# bad merge
{
"shape": "circle",
"name": "name",
"material": "HT9",
"Tinput": 1.0,
"Thot": 1.0,
"mergeWith": 6,
},
# bad isotopics
{
"shape": "circle",
"name": "name",
"material": "HT9",
"Tinput": 1.0,
"Thot": 1.0,
"isotopics": 4,
},
# bad key
{
"shape": "circle",
"name": "name",
"material": "HT9",
"Tinput": 1.0,
"Thot": 1.0,
5: "od",
},
# bad linked dimension
{
"shape": "circle",
"name": "name",
"material": "HT9",
"Tinput": 1.0,
"Thot": 1.0,
"mult": "potato,mult",
},
]
for bad in bads:
with self.assertRaises(yamlize.YamlizingError):
ComponentBlueprint.load(repr(bad))
[docs] def test_cladding_invalid(self):
"""Make sure cladding input components are flagged as invalid."""
bad = {
"name": "cladding",
"shape": "Circle",
"material": "HT9",
"Tinput": 1.0,
"Thot": 1.0,
}
with self.assertRaises(yamlize.YamlizingError):
ComponentBlueprint.load(repr(bad))
[docs] def test_withoutBlocks(self):
# Some projects use a script to generate an input that has completely unique blocks,
# so the blocks: section is not needed
yamlWithoutBlocks = """
nuclide flags:
U238: {burn: true, xs: true}
U235: {burn: true, xs: true}
LFP35: {burn: true, xs: true}
U236: {burn: true, xs: true}
PU239: {burn: true, xs: true}
DUMP2: {burn: true, xs: true}
DUMP1: {burn: true, xs: true}
NP237: {burn: true, xs: true}
PU238: {burn: true, xs: true}
PU236: {burn: true, xs: true}
LFP39: {burn: true, xs: true}
PU238: {burn: true, xs: true}
LFP40: {burn: true, xs: true}
PU241: {burn: true, xs: true}
LFP38: {burn: true, xs: true}
U234: {burn: true, xs: true}
AM241: {burn: true, xs: true}
LFP41: {burn: true, xs: true}
PU242: {burn: true, xs: true}
AM243: {burn: true, xs: true}
CM244: {burn: true, xs: true}
CM242: {burn: true, xs: true}
AM242: {burn: true, xs: true}
PU240: {burn: true, xs: true}
CM245: {burn: true, xs: true}
NP238: {burn: true, xs: true}
CM243: {burn: true, xs: true}
CM246: {burn: true, xs: true}
CM247: {burn: true, xs: true}
ZR: {burn: false, xs: true}
assemblies:
fuel a: &assembly_a
specifier: FF
blocks:
- { name: fuel,
fuel: { shape: Hexagon, material: UZr, Tinput: 25.0, Thot: 600.0, ip: 0.0, mult: 1.0, op: 10.0} }
height: [1.0]
axial mesh points: [1]
xs types: [A]
fuel b:
<<: *assembly_a
specifier: IF
"""
cs = settings.Settings()
design = blueprints.Blueprints.load(yamlWithoutBlocks)
design.constructAssem(cs, name="fuel a")
fa = design.constructAssem(cs, name="fuel a")
fb = design.constructAssem(cs, name="fuel b")
for a in (fa, fb):
self.assertEqual(1, len(a))
self.assertEqual(1, len(a[0]))