# 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 assemblies.py."""
import math
import pathlib
import random
import unittest
from unittest.mock import patch
import numpy as np
from numpy.testing import assert_allclose
from armi import settings, tests
from armi.physics.neutronics.settings import (
CONF_LOADING_FILE,
CONF_XS_KERNEL,
)
from armi.reactor import (
assemblies,
blocks,
blueprints,
components,
geometry,
parameters,
reactors,
)
from armi.reactor.assemblies import (
Flags,
HexAssembly,
copy,
grids,
runLog,
)
from armi.reactor.parameters import ParamLocation
from armi.reactor.tests import test_reactors
from armi.tests import TEST_ROOT, mockRunLogs
from armi.utils import directoryChangers, textProcessors
NUM_BLOCKS = 3
[docs]
def buildTestAssemblies():
"""
Build some assembly objects that will be used in testing.
This builds 2 HexBlocks:
* One with half UZr pins and half UTh pins
* One with all UThZr pins
"""
settings.Settings()
temperature = 273.0
fuelID = 0.0
fuelOD = 1.0
cladOD = 1.1
# generate a reactor with assemblies
# generate components with materials
nPins = 100
fuelDims = {
"Tinput": temperature,
"Thot": temperature,
"od": fuelOD,
"id": fuelID,
"mult": nPins,
}
fuelUZr = components.Circle("fuel", "UZr", **fuelDims)
fuelUTh = components.Circle("fuel UTh", "ThU", **fuelDims)
fuelDims2nPins = {
"Tinput": temperature,
"Thot": temperature,
"od": fuelOD,
"id": fuelID,
"mult": 2 * nPins,
}
fuelUThZr = components.Circle("fuel B", "UThZr", **fuelDims2nPins)
cladDims = {
"Tinput": temperature,
"Thot": temperature,
"od": cladOD,
"id": fuelOD,
"mult": 2 * nPins,
}
clad = components.Circle("clad", "HT9", **cladDims)
interDims = {
"Tinput": temperature,
"Thot": temperature,
"op": 16.8,
"ip": 16.0,
"mult": 1.0,
}
interSodium = components.Hexagon("interCoolant", "Sodium", **interDims)
block = blocks.HexBlock("fuel")
block2 = blocks.HexBlock("fuel")
block.setType("fuel")
block.setHeight(10.0)
block.add(fuelUZr)
block.add(fuelUTh)
block.add(clad)
block.add(interSodium)
block.p.axMesh = 1
block.p.molesHmBOL = 1.0
block.p.molesHmNow = 1.0
block2.setType("fuel")
block2.setHeight(10.0)
block2.add(fuelUThZr)
block2.add(clad)
block2.add(interSodium)
block2.p.axMesh = 1
block2.p.molesHmBOL = 2
block2.p.molesHmNow = 1.0
assemblieObjs = []
for numBlocks, blockTemplate in zip([1, 1, 5, 4], [block, block2, block, block]):
assembly = assemblies.HexAssembly("testAssemblyType")
assembly.spatialGrid = grids.AxialGrid.fromNCells(numBlocks)
assembly.spatialGrid.armiObject = assembly
for _i in range(numBlocks):
newBlock = copy.deepcopy(blockTemplate)
assembly.add(newBlock)
assembly.calculateZCoords()
assembly.reestablishBlockOrder()
assemblieObjs.append(assembly)
return assemblieObjs
[docs]
class MaterialInAssembly_TestCase(unittest.TestCase):
def setUp(self):
(
self.assembly,
self.assembly2,
self.assembly3,
self.assembly4,
) = buildTestAssemblies()
[docs]
def test_sortNoLocator(self):
self.assembly.spatialLocator = None
self.assembly2.spatialLocator = None
self.assertFalse(self.assembly < self.assembly2)
self.assertFalse(self.assembly2 < self.assembly)
grid = grids.HexGrid()
self.assembly.spatialLocator = grid[0, 0, 0]
self.assembly2.spatialLocator = grid[0, 1, 0]
self.assertTrue(self.assembly < self.assembly2)
self.assertFalse(self.assembly2 < self.assembly)
[docs]
def test_UThZrMaterial(self):
"""Test the ternary UThZr material."""
b2 = self.assembly2[0]
uThZrFuel = b2.getComponent(Flags.FUEL | Flags.B)
mat = uThZrFuel.getProperties()
mat.applyInputParams(0.1, 0.0)
self.assertAlmostEqual(
uThZrFuel.getMass("U235") / (uThZrFuel.getMass("U238") + uThZrFuel.getMass("U235")),
0.1111111111111111,
)
[docs]
def makeTestAssembly(numBlocks, assemNum, spatialGrid=grids.HexGrid.fromPitch(1.0), r=None):
coreGrid = r.core.spatialGrid if r is not None else spatialGrid
a = HexAssembly("TestAssem", assemNum=assemNum)
a.spatialGrid = grids.AxialGrid.fromNCells(numBlocks)
a.spatialGrid.armiObject = a
a.spatialLocator = coreGrid[2, 2, 0]
return a
[docs]
class Assembly_TestCase(unittest.TestCase):
def setUp(self):
self.name = "A0015"
self.assemNum = 15
self.height = 10
self.cs = settings.Settings()
# Print nothing to the screen that would normally go to the log.
runLog.setVerbosity("error")
self.r = tests.getEmptyHexReactor()
self.r.core.symmetry = geometry.SymmetryType(geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC)
self.assembly = makeTestAssembly(NUM_BLOCKS, self.assemNum, r=self.r)
# Use these if they are needed
self.blockParams = {
"height": self.height,
"bondRemoved": 0.0,
"envGroupNum": 0,
"buLimit": 35,
"buRate": 0.0,
"eqRegion": -1,
"id": 212.0,
"pdens": 10.0,
"percentBu": 25.3,
"power": 100000.0,
"residence": 4.0,
"smearDensity": 0.6996721711791459,
"timeToLimit": 2.7e5,
"xsTypeNum": 65,
"zbottom": 97.3521,
"ztop": 111.80279999999999,
}
# add some blocks with a component
self.blockList = []
for i in range(NUM_BLOCKS):
b = blocks.HexBlock("TestHexBlock")
b.setHeight(self.height)
self.hexDims = {
"Tinput": 273.0,
"Thot": 273.0,
"op": 0.76,
"ip": 0.0,
"mult": 1.0,
}
h = components.Hexagon("fuel", "UZr", **self.hexDims)
# non-flaggy name important for testing
b.setType("igniter fuel unitst")
b.add(h)
b.parent = self.assembly
b.setName(b.makeName(self.assembly.getNum(), i))
self.assembly.add(b)
self.blockList.append(b)
self.r.core.add(self.assembly)
self.assembly.calculateZCoords()
[docs]
def test_isOnWhichSymmetryLine(self):
line = self.assembly.isOnWhichSymmetryLine()
self.assertEqual(line, 2)
[docs]
def test_notesParameter(self):
self.assertEqual(self.assembly.p.notes, "")
with self.assertRaises(ValueError):
# try to assign a non-string
self.assembly.p.notes = 1
note = "This is a short, acceptable not about the assembly"
self.assembly.p.notes = note
self.assertEqual(self.assembly.p.notes, note)
tooLongNote = "a" * 1001
self.assembly.p.notes = tooLongNote
self.assertEqual(self.assembly.p.notes, tooLongNote[0:1000])
[docs]
def test_iter(self):
cur = []
for block in self.assembly:
cur.append(block)
ref = self.blockList
self.assertEqual(cur, ref)
[docs]
def test_len(self):
cur = len(self.assembly)
ref = len(self.blockList)
self.assertEqual(cur, ref)
[docs]
def test_append(self):
b = blocks.HexBlock("TestBlock")
self.blockList.append(b)
self.assembly.append(b)
cur = self.assembly.getBlocks()
ref = self.blockList
self.assertEqual(cur, ref)
[docs]
def test_extend(self):
blockList = []
for _ in range(2):
b = blocks.HexBlock("TestBlock")
self.blockList.append(b)
blockList.append(b)
self.assembly.extend(blockList)
cur = self.assembly.getBlocks()
ref = self.blockList
self.assertEqual(cur, ref)
for c in self.assembly:
self.assertIs(c.parent, self.assembly)
[docs]
def test_add(self):
a = makeTestAssembly(1, 1)
# successfully add some Blocks to an Assembly
for n in range(3):
self.assertEqual(len(a), n)
b = blocks.HexBlock("TestBlock")
a.add(b)
self.assertIn(b, a)
self.assertEqual(b.parent, a)
self.assertEqual(len(a), n + 1)
with self.assertRaises(TypeError):
a.add(blocks.CartesianBlock("Test Cart Block"))
[docs]
def test_moveTo(self):
ref = self.r.core.spatialGrid.getLocatorFromRingAndPos(3, 10)
i, j = grids.HexGrid.getIndicesFromRingAndPos(3, 10)
locator = self.r.core.spatialGrid[i, j, 0]
self.assembly.moveTo(locator)
cur = self.assembly.spatialLocator
self.assertEqual(cur, ref)
[docs]
def test_scaleParamsWhenMoved(self):
"""Volume integrated parameters must be scaled when an assembly is placed on a core boundary."""
with patch.object(self.assembly.p.paramDefs["chargeFis"], "location", ParamLocation.VOLUME_INTEGRATED):
# patch makes chargeFis look volume integrated
assemblyParams = {"chargeFis": 6.0, "chargeTime": 2}
blockParams = {
# volume integrated parameters
"massHmBOL": 9.0,
"molesHmBOL": np.array([[1, 2, 3], [4, 5, 6]]), # ndarray for testing
"adjMgFlux": [1, 2, 3], # Should normally be an ndarray, list for testing
"lastMgFlux": "foo", # Should normally be an ndarray, str for testing
}
self.assembly.p.update(assemblyParams)
for b in self.assembly.iterBlocks(Flags.FUEL):
b.p.update(blockParams)
i, j = grids.HexGrid.getIndicesFromRingAndPos(1, 1)
locator = self.r.core.spatialGrid[i, j, 0]
self.assertEqual(self.assembly.getSymmetryFactor(), 1)
self.assembly.moveTo(locator)
self.assertEqual(self.assembly.getSymmetryFactor(), 3)
for b in self.assembly.iterBlocks(Flags.FUEL):
# float
assert_allclose(b.p["massHmBOL"] / blockParams["massHmBOL"], 1 / 3)
# np.ndarray
assert_allclose(b.p["molesHmBOL"] / blockParams["molesHmBOL"], 1 / 3)
# list
assert_allclose(np.array(b.p["adjMgFlux"]) / np.array(blockParams["adjMgFlux"]), 1 / 3)
# string
self.assertEqual(b.p["lastMgFlux"], blockParams["lastMgFlux"])
self.assertEqual(self.assembly.p["chargeFis"] / assemblyParams["chargeFis"], 1 / 3)
self.assertEqual(self.assembly.p["chargeTime"] / assemblyParams["chargeTime"], 1)
[docs]
def test_getName(self):
cur = self.assembly.getName()
ref = self.name
self.assertEqual(cur, ref)
[docs]
def test_getNum(self):
cur = self.assembly.getNum()
ref = self.assemNum
self.assertEqual(cur, ref)
[docs]
def test_getLocation(self):
"""
Test for getting string location of assembly.
.. test:: Assembly location is retrievable.
:id: T_ARMI_ASSEM_POSI0
:tests: R_ARMI_ASSEM_POSI
"""
cur = self.assembly.getLocation()
ref = str("005-003")
self.assertEqual(cur, ref)
[docs]
def test_getArea(self):
"""Tests area calculation for hex assembly."""
# Default case: for assemblies with no blocks
a = HexAssembly("TestAssem", assemNum=10)
self.assertIsNone(a.getArea())
# more realistic case: a hex block/assembly
cur = self.assembly.getArea()
ref = math.sqrt(3) / 2.0 * self.hexDims["op"] ** 2
self.assertAlmostEqual(cur, ref, places=6)
[docs]
def test_getVolume(self):
"""Tests volume calculation for hex assembly."""
cur = self.assembly.getVolume()
ref = math.sqrt(3) / 2.0 * self.hexDims["op"] ** 2 * self.height * NUM_BLOCKS
places = 6
self.assertAlmostEqual(cur, ref, places=places)
[docs]
def test_adjustResolution(self):
# Make a second assembly with 4 times the resolution
assemNum2 = self.assemNum * 4
height2 = self.height / 4.0
assembly2 = makeTestAssembly(assemNum2, assemNum2)
# add some blocks with a component
for _ in range(assemNum2):
b = blocks.HexBlock("TestBlock")
b.setHeight(height2)
assembly2.add(b)
self.assembly.adjustResolution(assembly2)
cur = len(self.assembly.getBlocks())
ref = 4.0 * len(self.blockList)
self.assertEqual(cur, ref)
cur = self.assembly.getBlocks()[0].getHeight()
ref = self.height / 4.0
places = 6
self.assertAlmostEqual(cur, ref, places=places)
[docs]
def test_getAxialMesh(self):
cur = self.assembly.getAxialMesh()
ref = [i * self.height + self.height for i in range(NUM_BLOCKS)]
self.assertEqual(cur, ref)
[docs]
def test_calculateZCoords(self):
self.assembly.calculateZCoords()
places = 6
bottom = 0.0
for b in self.assembly:
top = bottom + self.height
cur = b.p.z
ref = bottom + (top - bottom) / 2.0
self.assertAlmostEqual(cur, ref, places=places)
cur = b.p.zbottom
ref = bottom
self.assertAlmostEqual(cur, ref, places=places)
cur = b.p.ztop
ref = top
self.assertAlmostEqual(cur, ref, places=places)
bottom = top
[docs]
def test_getTotalHeight(self):
cur = self.assembly.getTotalHeight()
ref = self.height * NUM_BLOCKS
places = 6
self.assertAlmostEqual(cur, ref, places=places)
[docs]
def test_getHeight(self):
"""Test height of assembly calculation."""
cur = self.assembly.getHeight()
ref = self.height * NUM_BLOCKS
places = 6
self.assertAlmostEqual(cur, ref, places=places)
[docs]
def test_getReactiveHeight(self):
self.assembly[2].getComponent(Flags.FUEL).adjustMassEnrichment(0.01)
self.assembly[2].setNumberDensity("PU239", 0.0)
bottomElevation, reactiveHeight = self.assembly.getReactiveHeight(enrichThresh=0.02)
self.assertEqual(bottomElevation, 0.0)
self.assertEqual(reactiveHeight, 20.0)
[docs]
def test_getFissileMass(self):
for b in self.assembly:
b.p.massHmBOL = b.getHMMass()
b.p.enrichmentBOL = b.getFissileMassEnrich()
cur = self.assembly.getFissileMass()
ref = sum(bi.getMass(["U235", "PU239"]) for bi in self.assembly)
self.assertAlmostEqual(cur, ref)
[docs]
def test_getMass(self):
mass0 = self.assembly.getMass("U235")
mass1 = sum(bi.getMass("U235") for bi in self.assembly)
self.assertAlmostEqual(mass0, mass1)
fuelBlock = next(self.assembly.iterBlocks(Flags.FUEL))
blockU35Mass = fuelBlock.getMass("U235")
fuelBlock.setMass("U235", 2 * blockU35Mass)
self.assertAlmostEqual(fuelBlock.getMass("U235"), blockU35Mass * 2)
self.assertAlmostEqual(self.assembly.getMass("U235"), mass0 + blockU35Mass)
fuelBlock.setMass("U238", 0.0)
self.assertAlmostEqual(blockU35Mass * 2, fuelBlock.getMass("U235"))
[docs]
def test_getAge(self):
res = 5.0
for b in self.assembly:
b.p.residence = res
cur = self.assembly.getAge()
ref = res
places = 6
self.assertAlmostEqual(cur, ref, places=places)
[docs]
def test_makeAxialSnapList(self):
# Make a second assembly with 4 times the resolution
assemNum2 = self.assemNum * 4
height2 = self.height / 4.0
assembly2 = makeTestAssembly(assemNum2, assemNum2)
# add some blocks with a component
for _i in range(assemNum2):
self.hexDims = {
"Tinput": 273.0,
"Thot": 273.0,
"op": 0.76,
"ip": 0.0,
"mult": 1.0,
}
h = components.Hexagon("fuel", "UZr", **self.hexDims)
b = blocks.HexBlock("fuel")
b.setType("igniter fuel")
b.add(h)
b.setHeight(height2)
assembly2.add(b)
self.assembly.makeAxialSnapList(assembly2)
cur = []
for b in self.assembly:
cur.append(b.p.topIndex)
ref = [3, 7, 11]
self.assertEqual(cur, ref)
[docs]
def test_snapAxialMeshToReference(self):
ref = [11, 22, 33]
for b, i in zip(self.assembly, range(self.assemNum)):
b.p.topIndex = i
self.assembly.setBlockMesh(ref)
cur = []
for b in self.assembly:
cur.append(b.p.ztop)
self.assertEqual(cur, ref)
[docs]
def test_updateFromAssembly(self):
assembly2 = makeTestAssembly(self.assemNum, self.assemNum)
params = {}
params["maxPercentBu"] = 30.0
params["numMoves"] = 5.0
params["maxPercentBu"] = 0
params["timeToLimit"] = 2.7e5
params["arealPd"] = 110.0
params["maxDpaPeak"] = 14.0
params["kInf"] = 60.0
for key, param in params.items():
assembly2.p[key] = param
self.assembly.updateParamsFrom(assembly2)
for key, param in params.items():
cur = self.assembly.p[key]
ref = param
self.assertEqual(cur, ref)
def _setup_blueprints(self, filename="refSmallReactor.yaml"):
# need this for the getAllNuclides call
with directoryChangers.DirectoryChanger(TEST_ROOT):
newSettings = {CONF_LOADING_FILE: filename}
self.cs = self.cs.modified(newSettings=newSettings)
with open(self.cs[CONF_LOADING_FILE], "r") as y:
y = textProcessors.resolveMarkupInclusions(y, pathlib.Path(self.cs.inputDirectory))
self.r.blueprints = blueprints.Blueprints.load(y)
self.r.blueprints._prepConstruction(self.cs)
[docs]
def test_duplicate(self):
self._setup_blueprints()
# Perform the copy
assembly2 = copy.deepcopy(self.assembly)
for refBlock, curBlock in zip(self.assembly, assembly2):
numNucs = 0
for nuc in self.assembly.getAncestor(
lambda c: isinstance(c, reactors.Reactor)
).blueprints.allNuclidesInProblem:
numNucs += 1
# Block level density
ref = refBlock.getNumberDensity(nuc)
cur = curBlock.getNumberDensity(nuc)
self.assertEqual(cur, ref)
self.assertGreater(numNucs, 5)
refFracs = refBlock.getVolumeFractions()
curFracs = curBlock.getVolumeFractions()
# Block level area fractions
for ref, cur in zip(refFracs, curFracs):
ref = ref[1]
cur = cur[1]
places = 6
self.assertAlmostEqual(cur, ref, places=places)
# Block level params
for refParam in refBlock.p:
if refParam == "serialNum":
continue
ref = refBlock.p[refParam]
cur = curBlock.p[refParam]
if isinstance(cur, np.ndarray):
self.assertTrue((cur == ref).all())
else:
if refParam == "location":
ref = str(ref)
cur = str(cur)
self.assertEqual(
cur,
ref,
msg="The {} param differs: {} vs. {}".format(refParam, cur, ref),
)
# Block level height
for b, b2 in zip(self.assembly, assembly2):
ref = b.getHeight()
cur = b2.getHeight()
self.assertEqual(cur, ref)
assert_allclose(b.spatialLocator.indices, b2.spatialLocator.indices)
# Assembly level params
for param in self.assembly.p:
if param == "serialNum":
continue
ref = self.assembly.p[param]
cur = assembly2.p[param]
if isinstance(cur, np.ndarray):
assert_allclose(cur, ref)
else:
self.assertEqual(cur, ref)
# Block level core and parent
for b in assembly2:
self.assertEqual(b.core, None)
self.assertEqual(b.parent, assembly2)
[docs]
def test_hasFlags(self):
self.assembly.setType("fuel")
cur = self.assembly.hasFlags(Flags.FUEL)
self.assertTrue(cur)
[docs]
def test_renameBlocksAccordingToAssemblyNum(self):
self.assembly.p.assemNum = 55
self.assembly.renameBlocksAccordingToAssemblyNum()
self.assertIn("{0:04d}".format(self.assembly.getNum()), self.assembly[1].getName())
[docs]
def test_getBlocks(self):
cur = self.assembly.getBlocks()
ref = self.blockList
self.assertEqual(cur, ref)
[docs]
def test_getFirstBlock(self):
cur = self.assembly.getFirstBlock()
ref = self.blockList[0]
self.assertAlmostEqual(cur, ref)
[docs]
def test_getFirstBlockByType(self):
b = self.assembly.getFirstBlockByType("igniter fuel unitst")
self.assertEqual(b.getType(), "igniter fuel unitst")
b = self.assembly.getFirstBlockByType("i do not exist")
self.assertIsNone(b)
[docs]
def test_getBlockData(self):
paramDict = {
"timeToLimit": 40.0,
"power": 10000.0,
"envGroup": 4,
"residence": 3.145,
"eqRegion": -1,
"id": 299.0,
"bondRemoved": 0.337,
"buRate": 42.0,
}
# Set some params
for b in self.assembly:
for param, paramVal in paramDict.items():
b.p[param] = paramVal
for param in paramDict:
cur = list(self.assembly.getChildParamValues(param))
ref = []
for i, b in enumerate(self.blockList):
ref.append(self.blockList[i].p[param])
self.assertAlmostEqual(cur, ref, places=6)
[docs]
def test_getMaxParam(self):
for bi, b in enumerate(self.assembly):
b.p.power = bi
self.assertAlmostEqual(self.assembly.getMaxParam("power"), len(self.assembly) - 1)
[docs]
def test_getElevationsMatchingParamValue(self):
self.assembly[0].p.power = 0.0
self.assembly[1].p.power = 20.0
self.assembly[2].p.power = 10.0
heights = self.assembly.getElevationsMatchingParamValue("power", 15.0)
self.assertListEqual(heights, [12.5, 20.0])
[docs]
def test_calcAvgParam(self):
nums = []
for b in self.assembly:
nums.append(random.random())
b.p.power = nums[-1]
self.assertGreater(len(nums), 2)
self.assertAlmostEqual(self.assembly.calcAvgParam("power"), sum(nums) / len(nums))
[docs]
def test_calcTotalParam(self):
# Remake original assembly
self.assembly = makeTestAssembly(self.assemNum, self.assemNum)
# add some blocks with a component
for i in range(self.assemNum):
b = blocks.HexBlock("TestBlock")
# Set the 1st block to have higher params than the rest.
self.blockParamsTemp = {}
for key, val in self.blockParams.items():
# Iterate with i in self.assemNum, so higher assemNums get the high values.
if key != "xsTypeNum": # must keep valid
b.p[key] = self.blockParamsTemp[key] = val * i
b.setHeight(self.height)
b.setType("fuel")
self.hexDims = {
"Tinput": 273.0,
"Thot": 273.0,
"op": 0.76,
"ip": 0.0,
"mult": 1.0,
}
h = components.Hexagon("intercoolant", "Sodium", **self.hexDims)
b.add(h)
self.assembly.add(b)
for param in self.blockParamsTemp:
tot = 0.0
for b in self.assembly:
try:
tot += b.p[param]
except TypeError:
pass
ref = tot
try:
cur = self.assembly.calcTotalParam(param)
places = 6
self.assertAlmostEqual(cur, ref, places=places)
except TypeError:
pass
[docs]
def test_reattach(self):
# Remake original assembly
self.assembly = makeTestAssembly(self.assemNum, self.assemNum)
self.assertEqual(0, len(self.assembly))
# add some blocks with a component
for i in range(self.assemNum):
b = blocks.HexBlock("TestBlock")
# Set the 1st block to have higher params than the rest.
self.blockParamsTemp = {}
for key, val in self.blockParams.items():
# Iterate with i in self.assemNum, so higher assemNums get the high values.
b.p[key] = self.blockParamsTemp[key] = val * (i + 1)
b.setHeight(self.height)
b.setType("fuel")
self.hexDims = {
"Tinput": 273.0,
"Thot": 273.0,
"op": 0.76,
"ip": 0.0,
"mult": 1.0,
}
h = components.Hexagon("intercoolant", "Sodium", **self.hexDims)
b.add(h)
self.assembly.add(b)
self.assertEqual(self.assemNum, len(self.assembly))
for b in self.assembly:
self.assertEqual("fuel", b.getType())
[docs]
def test_reestablishBlockOrder(self):
self.assertEqual(self.assembly.spatialLocator.indices[0], 2)
self.assertEqual(self.assembly[0].spatialLocator.getRingPos(), (5, 3))
self.assertEqual(self.assembly[0].spatialLocator.indices[2], 0)
axialIndices = [2, 1, 0]
for ai, b in zip(axialIndices, self.assembly):
b.spatialLocator = self.assembly.spatialGrid[0, 0, ai]
self.assembly.reestablishBlockOrder()
cur = []
for b in self.assembly:
cur.append(b.getLocation())
ref = ["005-003-000", "005-003-001", "005-003-002"]
self.assertEqual(cur, ref)
[docs]
def test_countBlocksOfType(self):
cur = self.assembly.countBlocksWithFlags(Flags.IGNITER | Flags.FUEL)
self.assertEqual(cur, 3)
[docs]
def test_getDim(self):
"""Tests dimensions are retrievable."""
# quick test, if there are no blocks
a = HexAssembly("TestAssem", assemNum=10)
self.assertIsNone(a.getDim(Flags.FUEL, "op"))
# more interesting test, with blocks
cur = self.assembly.getDim(Flags.FUEL, "op")
ref = self.hexDims["op"]
places = 6
self.assertAlmostEqual(cur, ref, places=places)
[docs]
def test_getDominantMaterial(self):
cur = self.assembly.getDominantMaterial(Flags.FUEL).getName()
ref = "UZr"
self.assertEqual(cur, ref)
self.assertEqual(self.assembly.getDominantMaterial().getName(), ref)
[docs]
def test_iteration(self):
"""Tests the ability to doubly-loop over assemblies (under development)."""
a = self.assembly
for bi, b in enumerate(a):
if bi == 2:
h = 0.0
for bi2, b2 in enumerate(a):
if bi2 == 0:
self.assertEqual(
b2,
a[0],
msg="First block in new iteration is not the first block of assembly",
)
h += b2.getHeight()
# make sure the loop continues with the right counter
self.assertEqual(
b,
a[bi],
msg="The {0}th block in the loop ({1}) is not equal to the {0}th block in the assembly {2}".format(
bi, b, "dummy"
),
)
[docs]
def test_getBlocksAndZ(self):
blocksAndCenters = self.assembly.getBlocksAndZ()
lastZ = -1.0
for b, c in blocksAndCenters:
self.assertIn(b, self.assembly.getBlocks())
self.assertGreater(c, lastZ)
lastZ = c
self.assertRaises(TypeError, self.assembly.getBlocksAndZ, 1.0)
[docs]
def test_getBlocksBetweenElevations(self):
# assembly should have 3 blocks of 10 cm in it
blocksAndHeights = self.assembly.getBlocksBetweenElevations(0, 10)
self.assertEqual(blocksAndHeights[0], (self.assembly[0], 10.0))
blocksAndHeights = self.assembly.getBlocksBetweenElevations(0, 5.0)
self.assertEqual(blocksAndHeights[0], (self.assembly[0], 5.0))
blocksAndHeights = self.assembly.getBlocksBetweenElevations(1.0, 5.0)
self.assertEqual(blocksAndHeights[0], (self.assembly[0], 4.0))
blocksAndHeights = self.assembly.getBlocksBetweenElevations(9.0, 21.0)
self.assertEqual(blocksAndHeights[0], (self.assembly[0], 1.0))
self.assertEqual(blocksAndHeights[1], (self.assembly[1], 10.0))
self.assertEqual(blocksAndHeights[2], (self.assembly[2], 1.0))
blocksAndHeights = self.assembly.getBlocksBetweenElevations(-10, 1000.0)
self.assertEqual(len(blocksAndHeights), len(self.assembly))
self.assertAlmostEqual(sum([height for _b, height in blocksAndHeights]), self.assembly.getHeight())
[docs]
def test_getParamValuesAtZ(self):
# single value param
for b, temp in zip(self.assembly, [80, 85, 90]):
b.p.percentBu = temp
percentBuDef = b.p.paramDefs["percentBu"]
originalLoc = percentBuDef.location
try:
self.assertAlmostEqual(87.5, self.assembly.getParamValuesAtZ("percentBu", 20.0))
percentBuDef.location = parameters.ParamLocation.BOTTOM
self.assertAlmostEqual(
82.5,
self.assembly.getParamValuesAtZ("percentBu", 5.0, fillValue="extend"),
)
percentBuDef.location = parameters.ParamLocation.TOP
self.assertAlmostEqual(82.5, self.assembly.getParamValuesAtZ("percentBu", 15.0))
for b in self.assembly:
b.p.percentBu = None
self.assertTrue(np.isnan(self.assembly.getParamValuesAtZ("percentBu", 25.0)))
# multiDimensional param
for b, flux in zip(self.assembly, [[1, 10], [2, 8], [3, 6]]):
b.p.mgFlux = flux
self.assertTrue(np.allclose([2.5, 7.0], self.assembly.getParamValuesAtZ("mgFlux", 20.0)))
self.assertTrue(np.allclose([1.5, 9.0], self.assembly.getParamValuesAtZ("mgFlux", 10.0)))
for b in self.assembly:
b.p.mgFlux = [0.0] * 2
self.assertTrue(np.allclose([0.0, 0.0], self.assembly.getParamValuesAtZ("mgFlux", 10.0)))
# single value param at corner
for b, temp in zip(self.assembly, [100, 200, 300]):
b.p.THcornTemp = [temp + iCorner for iCorner in range(6)]
value = self.assembly.getParamValuesAtZ("THcornTemp", 20.0)
self.assertTrue(np.allclose([300, 301, 302, 303, 304, 305], value))
finally:
percentBuDef.location = originalLoc
[docs]
def test_hasContinuousCoolantChannel(self):
self.assertFalse(self.assembly.hasContinuousCoolantChannel())
modifiedAssem = self.assembly
coolantDims = {"Tinput": 273.0, "Thot": 273.0}
h = components.DerivedShape("coolant", "Sodium", **coolantDims)
for b in modifiedAssem:
b.add(h)
self.assertTrue(modifiedAssem.hasContinuousCoolantChannel())
[docs]
def test_carestianCoordinates(self):
"""Check the coordinates of the assembly within the core with a CarestianGrid.
.. test:: Cartesian coordinates are retrievable.
:id: T_ARMI_ASSEM_POSI1
:tests: R_ARMI_ASSEM_POSI
"""
a = makeTestAssembly(
numBlocks=1,
assemNum=1,
spatialGrid=grids.CartesianGrid.fromRectangle(1.0, 1.0),
)
self.assertEqual(a.coords(), (2.0, 2.0))
[docs]
def test_pinPlenumVolume(self):
"""Test the volume of a pin in the assembly's plenum."""
pinPlenumVolume = 5.951978000285659e-05
self._setup_blueprints("refSmallReactorBase.yaml")
assembly = self.r.blueprints.assemblies.get("igniter fuel")
self.assertEqual(pinPlenumVolume, assembly.getPinPlenumVolumeInCubicMeters())
[docs]
def test_averagePlenumTemperature(self):
"""Test an assembly's average plenum temperature with a single block outlet."""
averagePlenumTemp = 42.0
plenumBlock = makeTestAssembly(1, 2, grids.CartesianGrid.fromRectangle(1.0, 1.0))
plenumBlock.setType("plenum", Flags.PLENUM)
plenumBlock.p.THcoolantOutletT = averagePlenumTemp
self.assembly.setBlockMesh([10.0, 20.0, 30.0], conserveMassFlag="auto")
self.assembly.append(plenumBlock)
self.assertEqual(averagePlenumTemp, self.assembly.getAveragePlenumTemperature())
[docs]
def test_rotate(self):
"""Test rotation of an assembly spatial objects.
.. test:: An assembly can be rotated about its z-axis.
:id: T_ARMI_ROTATE_HEX_ASSEM
:tests: R_ARMI_ROTATE_HEX
"""
a = makeTestAssembly(1, 1)
b = blocks.HexBlock("TestBlock")
b.p.THcornTemp = [400, 450, 500, 550, 600, 650]
rotTemp = [600, 650, 400, 450, 500, 550]
b.p.displacementX = 0
b.p.displacementY = 1
rotX = -math.sqrt(3) / 2
rotY = -0.5
a.add(b)
a.rotate(math.radians(120))
# test list rotation
b = a[0]
self.assertEqual(b.p.THcornTemp, rotTemp)
self.assertAlmostEqual(b.p.displacementX, rotX)
self.assertAlmostEqual(b.p.displacementY, rotY)
b.p.THcornTemp = np.array([400, 450, 500, 550, 600, 650])
rotTemp = np.array([600, 650, 400, 450, 500, 550])
a.rotate(math.radians(120))
# test np.ndarray rotation
for i in range(len(b.p.THcornTemp)):
self.assertEqual(b.p.THcornTemp[i], rotTemp[i])
# test that floats and ints are left alone
b.p.THcornTemp = 3
a.rotate(math.radians(120))
self.assertEqual(b.p.THcornTemp, 3)
b.p.THcornTemp = 4.0
a.rotate(math.radians(120))
self.assertEqual(b.p.THcornTemp, 4.0)
# check that TypeError is raised for unexpected data type
b.p.THcornTemp = "bad data"
with self.assertRaises(TypeError):
a.rotate(math.radians(120))
# check that list of len != 6 ends up in runlog warning
# list len=5
b.p.THcornTemp = [400, 450, 500, 550, 600]
with mockRunLogs.BufferLog() as mock:
self.assertEqual("", mock.getStdout())
a.rotate(math.radians(120))
self.assertIn("No rotation method defined", mock.getStdout())
# np.ndarray len=5
b.p.THcornTemp = np.array([400, 450, 500, 550, 600])
with mockRunLogs.BufferLog() as mock:
self.assertEqual("", mock.getStdout())
a.rotate(math.radians(120))
self.assertIn("No rotation method defined", mock.getStdout())
# list len=7
b.p.THcornTemp = [400, 450, 500, 550, 600, 650, 700]
with mockRunLogs.BufferLog() as mock:
self.assertEqual("", mock.getStdout())
a.rotate(math.radians(120))
self.assertIn("No rotation method defined", mock.getStdout())
# np.ndarray len=7
b.p.THcornTemp = np.array([400, 450, 500, 550, 600, 650, 700])
with mockRunLogs.BufferLog() as mock:
self.assertEqual("", mock.getStdout())
a.rotate(math.radians(120))
self.assertIn("No rotation method defined", mock.getStdout())
with self.assertRaisesRegex(ValueError, expected_regex="60 degree"):
a.rotate(math.radians(40))
[docs]
def test_assem_block_types(self):
"""Test that all children of an assembly are blocks, ordered from top to bottom.
.. test:: Validate child types of assembly are blocks, ordered from top to bottom.
:id: T_ARMI_ASSEM_BLOCKS
:tests: R_ARMI_ASSEM_BLOCKS
"""
coords = []
for b in self.assembly.iterBlocks():
# Confirm children are blocks
self.assertIsInstance(b, blocks.Block)
# get coords from the child blocks
coords.append(b.getLocation())
# get the Z-coords for each block
zCoords = [int(c.split("-")[-1]) for c in coords]
# verify the blocks are ordered top-to-bottom, vertically
for i in range(1, len(zCoords)):
self.assertGreater(zCoords[i], zCoords[i - 1])
[docs]
def test_assem_hex_type(self):
"""Test that all children of a hex assembly are hexagons."""
for b in self.assembly:
# For a hex assem, confirm they are of type "Hexagon"
pitch_comp_type = b.PITCH_COMPONENT_TYPE[0]
self.assertEqual(pitch_comp_type.__name__, "Hexagon")
[docs]
def test_getBIndexFromZIndex(self):
# make sure the axMesh parameters are set in our test block
for b in self.assembly:
b.p.axMesh = 1
for zIndex in range(6):
bIndex = self.assembly.getBIndexFromZIndex(zIndex * 0.5)
self.assertEqual(bIndex, math.ceil(zIndex / 2) if zIndex < 5 else -1)
[docs]
def test_getElevationBoundariesByBlockType(self):
elevations = self.assembly.getElevationBoundariesByBlockType()
self.assertEqual(elevations, [0.0, 10.0, 10.0, 20.0, 20.0, 30.0])
[docs]
class AssemblyInReactor_TestCase(unittest.TestCase):
def setUp(self):
self.o, self.r = test_reactors.loadTestReactor(TEST_ROOT)
[docs]
def test_snapAxialMesViaBlockIgn(self):
"""Snap axial mesh to a reference mesh should conserve mass based on Block igniter fuel."""
originalMesh = [25.0, 50.0, 75.0, 100.0, 175.0]
refMesh = [26.0, 52.0, 79.0, 108.0, 175.0]
grid = self.r.core.spatialGrid
# 1. examine mass change in igniterFuel
igniterFuel = self.r.core.childrenByLocator[grid[0, 0, 0]]
# gridplate, fuel, fuel, fuel, plenum
for b in igniterFuel.iterBlocks(Flags.FUEL):
fuelComp = b.getComponent(Flags.FUEL)
# add isotopes from clad and coolant to fuel component to test mass conservation
# mass should only be conserved within fuel component, not over the whole block
fuelComp.setNumberDensity("FE56", 1e-10)
fuelComp.setNumberDensity("NA23", 1e-10)
b = igniterFuel[0]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
igniterMassGrid = b.getMass() - coolMass
igniterMassGridTotal = b.getMass()
b = igniterFuel[1]
igniterHMMass1 = b.getHMMass()
igniterZircMass1 = b.getMass("ZR")
igniterFuelBlockMass = b.getMass()
igniterDuctMass = b.getComponent(Flags.DUCT).getMass()
igniterCoolMass = b.getComponent(Flags.COOLANT).getMass()
coolMass = 0
b = igniterFuel[4]
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
igniterPlenumMass = b.getMass() - coolMass
# expand the core to the new reference mesh
for a in self.r.core:
a.setBlockMesh(refMesh, conserveMassFlag="auto")
# 2. check igniter mass after expansion
# gridplate, fuel, fuel, fuel, plenum
b = igniterFuel[0]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
igniterMassGridAfterExpand = b.getMass() - coolMass
b = igniterFuel[1]
igniterHMMass1AfterExpand = b.getHMMass()
igniterZircMass1AfterExpand = b.getMass("ZR")
igniterDuctMassAfterExpand = b.getComponent(Flags.DUCT).getMass()
igniterCoolMassAfterExpand = b.getComponent(Flags.COOLANT).getMass()
coolMass = 0
b = igniterFuel[4]
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
igniterPlenumMassAfterExpand = b.getMass() - coolMass
self.assertAlmostEqual(igniterMassGrid, igniterMassGridAfterExpand, 7)
self.assertAlmostEqual(igniterHMMass1, igniterHMMass1AfterExpand, 7)
self.assertAlmostEqual(igniterZircMass1, igniterZircMass1AfterExpand, 7)
# demonstrate that the duct and coolant mass are not conserved.
# number density stays constant, mass is scaled by ratio of new to old height
self.assertAlmostEqual(igniterDuctMass, igniterDuctMassAfterExpand * 25.0 / 26.0, 7)
self.assertAlmostEqual(igniterCoolMass, igniterCoolMassAfterExpand * 25.0 / 26.0, 7)
# Note the masses are linearly different by the amount that the plenum shrunk
self.assertAlmostEqual(igniterPlenumMass, igniterPlenumMassAfterExpand * 75 / 67.0, 7)
# Shrink the core back to the original mesh size to see if mass is conserved
for a in self.r.core:
a.setBlockMesh(originalMesh, conserveMassFlag="auto")
# 3. check igniter mass after shrink to original
# gridplate, fuel, fuel, fuel, plenum
b = igniterFuel[0]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
igniterMassGridAfterShrink = b.getMass() - coolMass
igniterMassGridTotalAfterShrink = b.getMass()
b = igniterFuel[1]
igniterHMMass1AfterShrink = b.getHMMass()
igniterZircMass1AfterShrink = b.getMass("ZR")
igniterFuelBlockMassAfterShrink = b.getMass()
igniterDuctMassAfterShrink = b.getComponent(Flags.DUCT).getMass()
igniterCoolMassAfterShrink = b.getComponent(Flags.COOLANT).getMass()
coolMass = 0
b = igniterFuel[4]
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
igniterPlenumMassAfterShrink = b.getMass() - coolMass
self.assertAlmostEqual(igniterMassGrid, igniterMassGridAfterShrink, 7)
self.assertAlmostEqual(igniterMassGridTotal, igniterMassGridTotalAfterShrink, 7)
self.assertAlmostEqual(igniterHMMass1, igniterHMMass1AfterShrink, 7)
self.assertAlmostEqual(igniterZircMass1, igniterZircMass1AfterShrink, 7)
self.assertAlmostEqual(igniterFuelBlockMass, igniterFuelBlockMassAfterShrink, 7)
self.assertAlmostEqual(igniterDuctMass, igniterDuctMassAfterShrink, 7)
self.assertAlmostEqual(igniterCoolMass, igniterCoolMassAfterShrink, 7)
self.assertAlmostEqual(igniterPlenumMass, igniterPlenumMassAfterShrink, 7)
[docs]
def test_snapAxialMeshViaBlockShield(self):
"""Snap axial mesh to a reference mesh should conserve mass based on Block shield."""
originalMesh = [25.0, 50.0, 75.0, 100.0, 175.0]
refMesh = [26.0, 52.0, 79.0, 108.0, 175.0]
# access the shield in ring 9, pos 2
grid = self.r.core.spatialGrid
i, j = grid.getIndicesFromRingAndPos(9, 2)
# 1. examine mass change in radial shield
a = self.r.core.childrenByLocator[grid[i, j, 0]]
# gridplate, axial shield, axial shield, axial shield, plenum
b = a[0]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldMassGrid = b.getMass() - coolMass
b = a[1]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldShieldMass = b.getMass() - coolMass
b = a[4]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldPlenumMass = b.getMass() - coolMass
# expand the core to the new reference mesh
for a in self.r.core:
a.setBlockMesh(refMesh, conserveMassFlag="auto")
# 2. examine mass change in radial shield after expansion
# gridplate, axial shield, axial shield, axial shield, plenum
b = a[0]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldMassGridAfterExpand = b.getMass() - coolMass
b = a[1]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldShieldMassAfterExpand = b.getMass() - coolMass
b = a[4]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldPlenumMassAfterExpand = b.getMass() - coolMass
# non mass conserving expansions
self.assertAlmostEqual(shieldMassGrid * 26.0 / 25.0, shieldMassGridAfterExpand, 7)
self.assertAlmostEqual(shieldShieldMass * 26.0 / 25.0, shieldShieldMassAfterExpand, 7)
self.assertAlmostEqual(shieldPlenumMass, shieldPlenumMassAfterExpand * 75.0 / 67.0, 7)
# Shrink the core back to the original mesh size to see if mass is conserved
for a in self.r.core:
a.setBlockMesh(originalMesh, conserveMassFlag="auto")
# 3. examine mass change in radial shield after shrink to original
# gridplate, axial shield, axial shield, axial shield, plenum
b = a[0]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldMassGridAfterShrink = b.getMass() - coolMass
b = a[1]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldShieldMassAfterShrink = b.getMass() - coolMass
b = a[4]
coolantNucs = b.getComponent(Flags.COOLANT).getNuclides()
coolMass = 0
for nuc in coolantNucs:
coolMass += b.getMass(nuc)
shieldPlenumMassAfterShrink = b.getMass() - coolMass
# non mass conserving expansions
self.assertAlmostEqual(shieldMassGrid, shieldMassGridAfterShrink, 7)
self.assertAlmostEqual(shieldShieldMass, shieldShieldMassAfterShrink, 7)
self.assertAlmostEqual(shieldPlenumMass, shieldPlenumMassAfterShrink, 7)
[docs]
class AnnularFuelTestCase(unittest.TestCase):
"""Test fuel with a whole in the center."""
def setUp(self):
self.cs = settings.Settings()
newSettings = {CONF_XS_KERNEL: "MC2v2"} # don't try to expand elementals
self.cs = self.cs.modified(newSettings=newSettings)
bp = blueprints.Blueprints()
self.r = reactors.Reactor("test", bp)
self.r.add(reactors.Core("Core"))
inputStr = """blocks:
ann fuel: &block_ann_fuel
gap:
shape: Circle
material: Void
Tinput: 20.0
Thot: 435.0
id: 0.0
mult: fuel.mult
od: fuel.id
fuel:
shape: Circle
material: UZr
Tinput: 20.0
Thot: 600.0
id: 0.1
mult: 127
od: 0.8
gap1:
shape: Circle
material: Void
Tinput: 20.0
Thot: 435.0
id: fuel.od
mult: fuel.mult
od: clad.id
clad:
shape: Circle
material: HT9
Tinput: 20.0
Thot: 435.0
id: .85
mult: fuel.mult
od: .95
duct: &component_type2_fuel_duct
shape: Hexagon
material: HT9
Tinput: 20.0
Thot: 435.0
ip: 13.00
op: 13.9
mult: 1
intercoolant: &component_type2_fuel_intercoolant
shape: Hexagon
material: Sodium
Tinput: 435.0
Thot: 435.0
ip: duct.op
mult: 1
op: 16
coolant: &component_type2_fuel_coolant
shape: DerivedShape
material: Sodium
Tinput: 435.0
Thot: 435.0
assemblies:
heights: &standard_heights [30.0]
axial mesh points: &standard_axial_mesh_points [2]
ann fuel:
specifier: FA
blocks: &inner_igniter_fuel_blocks [*block_ann_fuel]
height: *standard_heights
axial mesh points: *standard_axial_mesh_points
hotChannelFactors: TWRPclad
xs types: &inner_igniter_fuel_xs_types [D]
"""
self.blueprints = blueprints.Blueprints.load(inputStr)
self.blueprints._prepConstruction(self.cs)
[docs]
def test_areaCheck(self):
assembly = list(self.blueprints.assemblies.values())[0]
fuelBlock = assembly.getFirstBlock(Flags.FUEL)
intercoolant = fuelBlock.getComponent(Flags.INTERCOOLANT)
bpAssemblyArea = assembly.getArea()
actualAssemblyArea = math.sqrt(3) / 2.0 * intercoolant.p.op**2
self.assertAlmostEqual(bpAssemblyArea, actualAssemblyArea)