# 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.
"""Test block conversions."""
import os
import unittest
import numpy
from armi.reactor.converters import blockConverters
from armi.reactor import blocks
from armi.reactor import components
from armi.reactor.flags import Flags
from armi.reactor.tests.test_blocks import loadTestBlock
from armi.reactor.tests.test_reactors import loadTestReactor, TEST_ROOT
from armi.utils import hexagon
from armi.reactor import grids
from armi.utils.directoryChangers import TemporaryDirectoryChanger
from armi.physics.neutronics.isotopicDepletion.isotopicDepletionInterface import (
isDepletable,
)
[docs]class TestBlockConverter(unittest.TestCase):
def setUp(self):
self.td = TemporaryDirectoryChanger()
self.td.__enter__()
def tearDown(self):
self.td.__exit__(None, None, None)
[docs] def test_dissolveWireIntoCoolant(self):
"""
Test dissolving wire into coolant.
.. test:: Homogenize one component into another.
:id: T_ARMI_BLOCKCONV0
:tests: R_ARMI_BLOCKCONV
"""
self._test_dissolve(loadTestBlock(), "wire", "coolant")
hotBlock = loadTestBlock(cold=False)
self._test_dissolve(hotBlock, "wire", "coolant")
hotBlock = self._perturbTemps(hotBlock, "wire", 127, 800)
self._test_dissolve(hotBlock, "wire", "coolant")
[docs] def test_dissolveLinerIntoClad(self):
"""
Test dissolving liner into clad.
.. test:: Homogenize one component into another.
:id: T_ARMI_BLOCKCONV1
:tests: R_ARMI_BLOCKCONV
"""
self._test_dissolve(loadTestBlock(), "outer liner", "clad")
hotBlock = loadTestBlock(cold=False)
self._test_dissolve(hotBlock, "outer liner", "clad")
hotBlock = self._perturbTemps(hotBlock, "outer liner", 127, 800)
self._test_dissolve(hotBlock, "outer liner", "clad")
def _perturbTemps(self, block, cName, tCold, tHot):
"""Give the component different ref and hot temperatures than in test_Blocks."""
c = block.getComponent(Flags.fromString(cName))
c.refTemp, c.refHot = tCold, tHot
c.setTemperature(tHot)
return block
def _test_dissolve(self, block, soluteName, solventName):
converter = blockConverters.ComponentMerger(block, soluteName, solventName)
convertedBlock = converter.convert()
self.assertNotIn(soluteName, convertedBlock.getComponentNames())
self._checkAreaAndComposition(block, convertedBlock)
[docs] def test_build_NthRing(self):
"""Test building of one ring."""
RING = 6
block = loadTestBlock(cold=False)
block.spatialGrid = grids.HexGrid.fromPitch(1.0)
numPinsInRing = 30
converter = blockConverters.HexComponentsToCylConverter(block)
fuel, clad = _buildJoyoFuel()
pinComponents = [fuel, clad]
converter._buildFirstRing(pinComponents)
converter.pinPitch = 0.76
converter._buildNthRing(pinComponents, RING)
components = converter.convertedBlock
self.assertEqual(components[3].name.split()[0], components[-1].name.split()[0])
self.assertAlmostEqual(
clad.getNumberDensity("FE56"), components[1].getNumberDensity("FE56")
)
self.assertAlmostEqual(
components[3].getArea() + components[-1].getArea(),
clad.getArea() * numPinsInRing / clad.getDimension("mult"),
)
[docs] def test_convert(self):
"""Test conversion with no fuel driver.
.. test:: Convert hex blocks to cylindrical blocks.
:id: T_ARMI_BLOCKCONV_HEX_TO_CYL1
:tests: R_ARMI_BLOCKCONV_HEX_TO_CYL
"""
block = (
loadTestReactor(TEST_ROOT)[1]
.core.getAssemblies(Flags.FUEL)[2]
.getFirstBlock(Flags.FUEL)
)
block.spatialGrid = grids.HexGrid.fromPitch(1.0)
converter = blockConverters.HexComponentsToCylConverter(block)
converter.convert()
for compType in [Flags.FUEL, Flags.CLAD, Flags.DUCT]:
self.assertAlmostEqual(
block.getComponent(compType).getArea(),
sum(
[
component.getArea()
for component in converter.convertedBlock
if component.hasFlags(compType)
]
),
)
for c in converter.convertedBlock.getComponents(compType):
self.assertEqual(
block.getComponent(compType).temperatureInC, c.temperatureInC
)
self.assertEqual(block.getHeight(), converter.convertedBlock.getHeight())
self._checkAreaAndComposition(block, converter.convertedBlock)
self._checkCiclesAreInContact(converter.convertedBlock)
[docs] def test_convertHexWithFuelDriver(self):
"""Test conversion with fuel driver.
.. test:: Convert hex blocks to cylindrical blocks.
:id: T_ARMI_BLOCKCONV_HEX_TO_CYL0
:tests: R_ARMI_BLOCKCONV_HEX_TO_CYL
"""
driverBlock = (
loadTestReactor(TEST_ROOT)[1]
.core.getAssemblies(Flags.FUEL)[2]
.getFirstBlock(Flags.FUEL)
)
block = loadTestReactor(TEST_ROOT)[1].core.getFirstBlock(Flags.CONTROL)
control = block.getComponent(Flags.CONTROL)
# add depletable flag to see if it is carried
control.p.flags |= Flags.DEPLETABLE
driverBlock.spatialGrid = None
block.spatialGrid = grids.HexGrid.fromPitch(1.0)
convertedWithoutDriver = self._testConvertWithDriverRings(
block,
driverBlock,
blockConverters.HexComponentsToCylConverter,
hexagon.numPositionsInRing,
)
self.assertEqual(5, len([c for c in convertedWithoutDriver if isDepletable(c)]))
self.assertEqual(
5, len([c for c in convertedWithoutDriver if c.hasFlags(Flags.CONTROL)])
)
self.assertEqual(
9, len([c for c in convertedWithoutDriver if c.hasFlags(Flags.CLAD)])
)
# This should fail because a spatial grid is required
# on the block.
driverBlock.spatialGrid = None
block.spatialGrid = None
with self.assertRaises(ValueError):
self._testConvertWithDriverRings(
block,
driverBlock,
blockConverters.HexComponentsToCylConverter,
hexagon.numPositionsInRing,
)
# The ``BlockAvgToCylConverter`` should work
# without any spatial grid defined because it
# assumes the grid based on the block type.
driverBlock.spatialGrid = None
block.spatialGrid = None
convertedWithoutDriver = self._testConvertWithDriverRings(
block,
driverBlock,
blockConverters.BlockAvgToCylConverter,
hexagon.numPositionsInRing,
)
# block went to 1 component
self.assertEqual(1, len([c for c in convertedWithoutDriver]))
[docs] def test_convertHexWithFuelDriverOnNegativeComponentAreaBlock(self):
"""
Tests the conversion of a control block with linked components, where
a component contains a negative area due to thermal expansion.
"""
driverBlock = (
loadTestReactor(TEST_ROOT)[1]
.core.getAssemblies(Flags.FUEL)[2]
.getFirstBlock(Flags.FUEL)
)
block = buildControlBlockWithLinkedNegativeAreaComponent()
areas = [c.getArea() for c in block]
# Check that a negative area component exists.
self.assertLess(min(areas), 0.0)
driverBlock.spatialGrid = None
block.spatialGrid = grids.HexGrid.fromPitch(1.0)
converter = blockConverters.HexComponentsToCylConverter(
block, driverFuelBlock=driverBlock, numExternalRings=2
)
convertedBlock = converter.convert()
# The area is increased because the negative area components are
# removed.
self.assertGreater(convertedBlock.getArea(), block.getArea())
[docs] def test_convertCartesianLatticeWithFuelDriver(self):
"""Test conversion with fuel driver."""
r = loadTestReactor(TEST_ROOT, inputFileName="zpprTest.yaml")[1]
driverBlock = r.core.getAssemblies(Flags.FUEL)[2].getFirstBlock(Flags.FUEL)
block = r.core.getAssemblies(Flags.FUEL)[2].getFirstBlock(Flags.BLANKET)
driverBlock.spatialGrid = grids.CartesianGrid.fromRectangle(1.0, 1.0)
block.spatialGrid = grids.CartesianGrid.fromRectangle(1.0, 1.0)
converter = blockConverters.BlockAvgToCylConverter
self._testConvertWithDriverRings(
block, driverBlock, converter, lambda n: (n - 1) * 8
)
def _testConvertWithDriverRings(
self, block, driverBlock, converterToTest, getNumInRing
):
area = block.getArea()
numExternalFuelRings = [1, 2, 3, 4]
numBlocks = 1
for externalRings in numExternalFuelRings:
numBlocks += getNumInRing(externalRings + 1)
converter = converterToTest(
block, driverFuelBlock=driverBlock, numExternalRings=externalRings
)
convertedBlock = converter.convert()
self.assertAlmostEqual(area * numBlocks, convertedBlock.getArea())
self._checkCiclesAreInContact(convertedBlock)
plotFile = "convertedBlock_{0}.svg".format(externalRings)
converter.plotConvertedBlock(fName=plotFile)
os.remove(plotFile)
for c in list(reversed(convertedBlock))[:externalRings]:
self.assertTrue(c.isFuel(), "c was {}".format(c.name))
# remove external driver rings in preperation to check composition
convertedBlock.remove(c)
convBlockWithoutDriver = convertedBlock
self._checkAreaAndComposition(block, convBlockWithoutDriver)
return convBlockWithoutDriver
def _checkAreaAndComposition(self, block, convertedBlock):
self.assertAlmostEqual(block.getArea(), convertedBlock.getArea())
unmergedNucs = block.getNumberDensities()
convDens = convertedBlock.getNumberDensities()
errorMessage = ""
nucs = set(unmergedNucs) | set(convDens)
for nucName in nucs:
n1, n2 = unmergedNucs[nucName], convDens[nucName]
try:
self.assertAlmostEqual(n1, n2)
except AssertionError:
errorMessage += "\nnuc {} not equal. unmerged: {} merged: {}".format(
nucName, n1, n2
)
self.assertTrue(not errorMessage, errorMessage)
bMass = block.getMass()
self.assertAlmostEqual(bMass, convertedBlock.getMass())
self.assertGreater(bMass, 0.0) # verify it isn't empty
def _checkCiclesAreInContact(self, convertedCircleBlock):
numComponents = len(convertedCircleBlock)
self.assertGreater(numComponents, 1)
self.assertTrue(
all(isinstance(c, components.Circle) for c in convertedCircleBlock)
)
lastCompOD = None
lastComp = None
for c in sorted(convertedCircleBlock):
thisID = c.getDimension("id")
thisOD = c.getDimension("od")
if lastCompOD is None:
self.assertTrue(
thisID == 0,
"The inner component {} should have an ID of zero".format(c),
)
else:
self.assertTrue(
thisID == lastCompOD,
"The component {} with id {} was not in contact with the "
"previous component ({}) that had od {}".format(
c, thisID, lastComp, lastCompOD
),
)
lastCompOD = thisOD
lastComp = c
[docs]class TestToCircles(unittest.TestCase):
[docs] def test_fromHex(self):
actualRadii = blockConverters.radiiFromHexPitches([7.47, 7.85, 8.15])
expected = [3.92203, 4.12154, 4.27906]
self.assertTrue(numpy.allclose(expected, actualRadii, rtol=1e-5))
[docs] def test_fromRingOfRods(self):
# JOYO-LMFR-RESR-001, rev 1, Table A.2, 5th layer (ring 6)
actualRadii = blockConverters.radiiFromRingOfRods(
0.76 * 5, 6 * 5, [0.28, 0.315]
)
expected = [3.24034, 3.28553, 3.62584, 3.67104]
self.assertTrue(numpy.allclose(expected, actualRadii, rtol=1e-5))
def _buildJoyoFuel():
"""Build some JOYO components."""
fuel = components.Circle(
name="fuel",
material="UO2",
Tinput=20.0,
Thot=20.0,
od=0.28 * 2,
id=0.0,
mult=91,
)
clad = components.Circle(
name="clad",
material="HT9",
Tinput=20.0,
Thot=20.0,
od=0.315 * 2,
id=0.28 * 2,
mult=91,
)
return fuel, clad
[docs]def buildControlBlockWithLinkedNegativeAreaComponent():
"""
Return a block that contains a bond component that resolves to a negative area
once the fuel and clad thermal expansion have occurred.
"""
b = blocks.HexBlock("control", height=10.0)
controlDims = {"Tinput": 25.0, "Thot": 600, "od": 0.77, "id": 0.00, "mult": 127.0}
bondDims = {
"Tinput": 600,
"Thot": 600,
"od": "clad.id",
"id": "control.od",
"mult": 127.0,
}
cladDims = {"Tinput": 25.0, "Thot": 450, "od": 0.80, "id": 0.77, "mult": 127.0}
wireDims = {
"Tinput": 25.0,
"Thot": 450,
"od": 0.1,
"id": 0.0,
"mult": 127.0,
"axialPitch": 30.0,
"helixDiameter": 0.9,
}
ductDims = {"Tinput": 25.0, "Thot": 400, "op": 16, "ip": 15.3, "mult": 1.0}
intercoolantDims = {
"Tinput": 400,
"Thot": 400,
"op": 17.0,
"ip": ductDims["op"],
"mult": 1.0,
}
coolDims = {"Tinput": 25.0, "Thot": 400}
control = components.Circle("control", "UZr", **controlDims)
clad = components.Circle("clad", "HT9", **cladDims)
# This sets up the linking of the bond to the fuel and the clad components.
bond = components.Circle(
"bond", "Sodium", components={"control": control, "clad": clad}, **bondDims
)
wire = components.Helix("wire", "HT9", **wireDims)
duct = components.Hexagon("duct", "HT9", **ductDims)
coolant = components.DerivedShape("coolant", "Sodium", **coolDims)
intercoolant = components.Hexagon("intercoolant", "Sodium", **intercoolantDims)
b.add(control)
b.add(bond)
b.add(clad)
b.add(wire)
b.add(duct)
b.add(coolant)
b.add(intercoolant)
b.getVolumeFractions() # TODO: remove, should be no-op when removed self.cached
return b