# 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.
"""Module to test geometry converters."""
import math
import os
import unittest
from numpy.testing import assert_allclose
from armi import runLog
from armi.reactor import blocks
from armi.reactor import geometry
from armi.reactor import grids
from armi.reactor.converters import geometryConverters
from armi.reactor.converters import uniformMesh
from armi.reactor.flags import Flags
from armi.reactor.tests.test_reactors import loadTestReactor, reduceTestReactorRings
from armi.tests import TEST_ROOT, mockRunLogs
from armi.utils import directoryChangers
THIS_DIR = os.path.dirname(__file__)
[docs]class TestGeometryConverters(unittest.TestCase):
def setUp(self):
self.o, self.r = loadTestReactor(TEST_ROOT)
self.cs = self.o.cs
[docs] def test_addRing(self):
"""Tests that ``addRing`` adds the correct number of fuel assemblies to the test reactor."""
converter = geometryConverters.FuelAssemNumModifier(self.cs)
converter.numFuelAssems = 7
converter.ringsToAdd = 1 * ["radial shield"]
converter.convert(self.r)
numAssems = len(self.r.core.getAssemblies())
self.assertEqual(
numAssems, 13
) # should end up with 6 reflector assemblies per 1/3rd Core
locator = self.r.core.spatialGrid.getLocatorFromRingAndPos(4, 1)
shieldtype = self.r.core.childrenByLocator[locator].getType()
self.assertEqual(
shieldtype, "radial shield"
) # check that the right thing was added
# one more test with an uneven number of rings
converter.numFuelAssems = 8
converter.convert(self.r)
numAssems = len(self.r.core.getAssemblies())
self.assertEqual(
numAssems, 19
) # should wind up with 11 reflector assemblies per 1/3rd core
[docs] def test_setNumberOfFuelAssems(self):
"""Tests that ``setNumberOfFuelAssems`` properly changes the number of fuel assemblies."""
# tests ability to add fuel assemblies
converter = geometryConverters.FuelAssemNumModifier(self.cs)
converter.numFuelAssems = 60
converter.convert(self.r)
numFuelAssems = 0
for assem in self.r.core.getAssemblies():
if assem.hasFlags(Flags.FUEL):
numFuelAssems += 1
self.assertEqual(numFuelAssems, 60)
# checks that existing fuel assemblies are preserved
locator = self.r.core.spatialGrid.getLocatorFromRingAndPos(1, 1)
fueltype = self.r.core.childrenByLocator[locator].getType()
self.assertEqual(fueltype, "igniter fuel")
# checks that existing control rods are preserved
locator = self.r.core.spatialGrid.getLocatorFromRingAndPos(5, 1)
controltype = self.r.core.childrenByLocator[locator].getType()
self.assertEqual(controltype, "primary control")
# checks that existing reflectors are overwritten with feed fuel
locator = self.r.core.spatialGrid.getLocatorFromRingAndPos(9, 5)
oldshieldtype = self.r.core.childrenByLocator[locator].getType()
self.assertEqual(oldshieldtype, "feed fuel")
# checks that outer assemblies are removed
locator = self.r.core.spatialGrid.getLocatorFromRingAndPos(9, 1)
with self.assertRaises(KeyError):
_ = self.r.core.childrenByLocator[locator]
# tests ability to remove fuel assemblies
converter.numFuelAssems = 20
converter.convert(self.r)
numFuelAssems = 0
for assem in self.r.core.getAssemblies():
if assem.hasFlags(Flags.FUEL):
numFuelAssems += 1
self.assertEqual(numFuelAssems, 20)
[docs] def test_getAssembliesInSector(self):
allAssems = self.r.core.getAssemblies()
fullSector = geometryConverters.HexToRZConverter._getAssembliesInSector(
self.r.core, 0, 360
)
self.assertGreaterEqual(
len(fullSector), len(allAssems)
) # could be > due to edge assems
third = geometryConverters.HexToRZConverter._getAssembliesInSector(
self.r.core, 0, 30
)
# could solve this analytically based on test core size
self.assertAlmostEqual(25, len(third))
oneLine = geometryConverters.HexToRZConverter._getAssembliesInSector(
self.r.core, 0, 0.001
)
self.assertAlmostEqual(5, len(oneLine)) # same here
[docs]class TestHexToRZConverter(unittest.TestCase):
def setUp(self):
self.o, self.r = loadTestReactor(TEST_ROOT)
reduceTestReactorRings(self.r, self.o.cs, 2)
self.cs = self.o.cs
runLog.setVerbosity("extra")
self._expandReactor = False
self._massScaleFactor = 1.0
if not self._expandReactor:
self._massScaleFactor = 3.0
def tearDown(self):
del self.o
del self.cs
del self.r
[docs] def test_convert(self):
"""Test HexToRZConverter.convert().
Notes
-----
Ensure the converted reactor has 1) nuclides and nuclide masses that match the
original reactor, 2) for a given (r,z,theta) location the expected block type exists,
3) the converted reactor has the right (r,z,theta) coordinates, and 4) the converted
reactor blocks all have a single (homogenized) component.
.. test:: Convert a 3D hex reactor core to an RZ-Theta core.
:id: T_ARMI_CONV_3DHEX_TO_2DRZ
:tests: R_ARMI_CONV_3DHEX_TO_2DRZ
"""
# make the reactor smaller, because of a test parallelization edge case
for ring in [9, 8, 7, 6, 5, 4, 3]:
self.r.core.removeAssembliesInRing(ring, self.o.cs)
converterSettings = {
"radialConversionType": "Ring Compositions",
"axialConversionType": "Axial Coordinates",
"uniformThetaMesh": True,
"thetaBins": 1,
"axialMesh": [25, 50, 75, 100, 150, 175],
"thetaMesh": [2 * math.pi],
}
expectedMassDict, expectedNuclideList = self._getExpectedData()
geomConv = geometryConverters.HexToRZConverter(
self.cs, converterSettings, expandReactor=self._expandReactor
)
geomConv.convert(self.r)
newR = geomConv.convReactor
self._checkBlockComponents(newR)
self._checkNuclidesMatch(expectedNuclideList, newR)
self._checkNuclideMasses(expectedMassDict, newR)
self._checkBlockAtMeshPoint(geomConv)
self._checkReactorMeshCoordinates(geomConv)
_figs = geomConv.plotConvertedReactor()
with directoryChangers.TemporaryDirectoryChanger():
geomConv.plotConvertedReactor("fname")
# bonus test: reset() works after converter has filled in values
geomConv.reset()
self.assertIsNone(geomConv.convReactor)
self.assertIsNone(geomConv._radialMeshConversionType)
self.assertIsNone(geomConv._axialMeshConversionType)
self.assertIsNone(geomConv._currentRadialZoneType)
self.assertEqual(geomConv._newBlockNum, 0)
def _checkBlockAtMeshPoint(self, geomConv):
b = geomConv._getBlockAtMeshPoint(0.0, 2.0 * math.pi, 0.0, 12.0, 50.0, 75.0)
self.assertTrue(b.hasFlags(Flags.FUEL))
def _checkReactorMeshCoordinates(self, geomConv):
thetaMesh, radialMesh, axialMesh = geomConv._getReactorMeshCoordinates()
expectedThetaMesh = [math.pi * 2.0]
expectedAxialMesh = [25.0, 50.0, 75.0, 100.0, 150.0, 175.0]
expectedRadialMesh = [
8.794379,
23.26774,
]
assert_allclose(expectedThetaMesh, thetaMesh)
assert_allclose(expectedRadialMesh, radialMesh)
assert_allclose(expectedAxialMesh, axialMesh)
def _getExpectedData(self):
"""Retrieve the mass of all nuclides in the reactor prior to converting."""
expectedMassDict = {}
expectedNuclideList = self.r.blueprints.allNuclidesInProblem
for nuclide in sorted(expectedNuclideList):
expectedMassDict[nuclide] = self.r.core.getMass(nuclide)
return expectedMassDict, expectedNuclideList
def _checkBlockComponents(self, newR):
for b in newR.core.getBlocks():
if len(b) != 1:
raise ValueError(
"Block {} has {} components and should only have 1".format(
b, len(b)
)
)
def _checkNuclidesMatch(self, expectedNuclideList, newR):
"""Check that the nuclide lists match before and after conversion."""
actualNuclideList = newR.blueprints.allNuclidesInProblem
if set(expectedNuclideList) != set(actualNuclideList):
diffList = sorted(set(expectedNuclideList).difference(actualNuclideList))
diffList += sorted(set(actualNuclideList).difference(expectedNuclideList))
runLog.warning(diffList)
raise ValueError(
"{0} nuclides do not match between the original and converted reactor".format(
len(diffList)
)
)
def _checkNuclideMasses(self, expectedMassDict, newR):
"""Check that all nuclide masses in the new reactor are equivalent to before the conversion."""
massMismatchCount = 0
for nuclide in expectedMassDict.keys():
expectedMass = expectedMassDict[nuclide]
actualMass = newR.core.getMass(nuclide) / self._massScaleFactor
if round(abs(expectedMass - actualMass), 7) != 0.0:
print(
"{:6s} {:10.2f} {:10.2f}".format(nuclide, expectedMass, actualMass)
)
massMismatchCount += 1
# Raise error if there are any inconsistent masses
if massMismatchCount > 0:
raise ValueError(
"{0} nuclides have masses that are not consistent after the conversion".format(
massMismatchCount
)
)
[docs] def test_createHomogenizedRZTBlock(self):
newBlock = blocks.ThRZBlock("testBlock", self.cs)
a = self.r.core[0]
converterSettings = {}
geomConv = geometryConverters.HexToRZConverter(
self.cs, converterSettings, expandReactor=self._expandReactor
)
volumeExpected = a.getVolume()
(
_atoms,
_newBlockType,
_newBlockTemp,
newBlockVol,
) = geomConv.createHomogenizedRZTBlock(newBlock, 0, a.getHeight(), [a])
# The volume of the radialZone and the radialThetaZone should be equal for RZ geometry
self.assertAlmostEqual(volumeExpected, newBlockVol)
[docs]class TestEdgeAssemblyChanger(unittest.TestCase):
def setUp(self):
"""Use the related setup in the testFuelHandlers module."""
self.o, self.r = loadTestReactor(TEST_ROOT)
reduceTestReactorRings(self.r, self.o.cs, 3)
def tearDown(self):
del self.o
del self.r
[docs] def test_edgeAssemblies(self):
"""Sanity check on adding edge assemblies.
.. test:: Test adding/removing assemblies from a reactor.
:id: T_ARMI_ADD_EDGE_ASSEMS
:tests: R_ARMI_ADD_EDGE_ASSEMS
"""
def getAssemByRingPos(ringPos: tuple):
for a in self.r.core.getAssemblies():
if a.spatialLocator.getRingPos() == ringPos:
return a
return None
numAssemsOrig = len(self.r.core.getAssemblies())
# assert that there is no assembly in the (3, 4) (ring, position).
self.assertIsNone(getAssemByRingPos((3, 4)))
# add the assembly
converter = geometryConverters.EdgeAssemblyChanger()
converter.addEdgeAssemblies(self.r.core)
numAssemsWithEdgeAssem = len(self.r.core.getAssemblies())
# assert that there is an assembly in the (3, 4) (ring, position).
self.assertIsNotNone(getAssemByRingPos((3, 4)))
self.assertTrue(numAssemsWithEdgeAssem > numAssemsOrig)
# try to add the assembly again (you can't)
with mockRunLogs.BufferLog() as mock:
converter.addEdgeAssemblies(self.r.core)
self.assertIn("Skipping addition of edge assemblies", mock.getStdout())
self.assertTrue(numAssemsWithEdgeAssem, len(self.r.core.getAssemblies()))
# must be added after geom transform
for b in self.o.r.core.getBlocks():
b.p.power = 1.0
converter.scaleParamsRelatedToSymmetry(self.r)
a = self.r.core.getAssembliesOnSymmetryLine(grids.BOUNDARY_0_DEGREES)[0]
self.assertTrue(all(b.p.power == 2.0 for b in a), "Powers were not scaled")
# remove the assembly that was added
converter.removeEdgeAssemblies(self.r.core)
self.assertIsNone(getAssemByRingPos((3, 4)))
self.assertEqual(numAssemsOrig, len(self.r.core.getAssemblies()))
[docs]class TestThirdCoreHexToFullCoreChanger(unittest.TestCase):
def setUp(self):
self.o, self.r = loadTestReactor(TEST_ROOT)
reduceTestReactorRings(self.r, self.o.cs, 3)
# initialize the block powers to a uniform power profile, accounting for
# the loaded reactor being 1/3 core
numBlocksInFullCore = 0
for a in self.r.core:
if a.getLocation() == "001-001":
for b in a:
numBlocksInFullCore += 1
else:
for b in a:
# account for the 1/3 symmetry
numBlocksInFullCore += 3
for a in self.r.core:
if a.getLocation() == "001-001":
for b in a:
b.p["power"] = self.o.cs["power"] / numBlocksInFullCore / 3
else:
for b in a:
b.p["power"] = self.o.cs["power"] / numBlocksInFullCore
def tearDown(self):
del self.o
del self.r
[docs] def test_growToFullCoreFromThirdCore(self):
"""Test that a hex core can be converted from a third core to a full core geometry.
.. test:: Convert a third-core to a full-core geometry and then restore it.
:id: T_ARMI_THIRD_TO_FULL_CORE0
:tests: R_ARMI_THIRD_TO_FULL_CORE
"""
def getLTAAssems():
aList = []
for a in self.r.core.getAssemblies():
if a.getType == "lta fuel":
aList.append(a)
return aList
# Check the initialization of the third core model
self.assertFalse(self.r.core.isFullCore)
self.assertEqual(
self.r.core.symmetry,
geometry.SymmetryType(
geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC
),
)
initialNumBlocks = len(self.r.core.getBlocks())
assems = getLTAAssems()
expectedLoc = [(3, 2)]
for i, a in enumerate(assems):
self.assertEqual(a.spatialLocator.getRingPos(), expectedLoc[i])
self.assertAlmostEqual(
self.r.core.getTotalBlockParam("power"), self.o.cs["power"] / 3, places=5
)
self.assertGreater(
self.r.core.getTotalBlockParam("power", calcBasedOnFullObj=True),
self.o.cs["power"] / 3,
)
# Perform reactor conversion
changer = geometryConverters.ThirdCoreHexToFullCoreChanger(self.o.cs)
changer.convert(self.r)
# Check the full core conversion is successful
self.assertTrue(self.r.core.isFullCore)
self.assertGreater(len(self.r.core.getBlocks()), initialNumBlocks)
self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE)
assems = getLTAAssems()
expectedLoc = [(3, 2), (3, 6), (3, 10)]
for i, a in enumerate(assems):
self.assertEqual(a.spatialLocator.getRingPos(), expectedLoc[i])
# ensure that block power is handled correctly
self.assertAlmostEqual(
self.r.core.getTotalBlockParam("power"), self.o.cs["power"], places=5
)
self.assertAlmostEqual(
self.r.core.getTotalBlockParam("power", calcBasedOnFullObj=True),
self.o.cs["power"],
places=5,
)
# Check that the geometry can be restored to a third core
changer.restorePreviousGeometry(self.r)
self.assertEqual(initialNumBlocks, len(self.r.core.getBlocks()))
self.assertEqual(
self.r.core.symmetry,
geometry.SymmetryType(
geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC
),
)
self.assertFalse(self.r.core.isFullCore)
self.assertAlmostEqual(
self.r.core.getTotalBlockParam("power"), self.o.cs["power"] / 3, places=5
)
assems = getLTAAssems()
expectedLoc = [(3, 2)]
for i, a in enumerate(assems):
self.assertEqual(a.spatialLocator.getRingPos(), expectedLoc[i])
[docs] def test_initNewFullReactor(self):
"""Test that initNewReactor will growToFullCore if necessary."""
# Perform reactor conversion
changer = geometryConverters.ThirdCoreHexToFullCoreChanger(self.o.cs)
changer.convert(self.r)
converter = uniformMesh.NeutronicsUniformMeshConverter(self.o.cs)
newR = converter.initNewReactor(self.r, self.o.cs)
# Check the full core conversion is successful
self.assertTrue(self.r.core.isFullCore)
self.assertTrue(newR.core.isFullCore)
self.assertEqual(newR.core.symmetry.domain, geometry.DomainType.FULL_CORE)
[docs] def test_skipGrowToFullCoreWhenAlreadyFullCore(self):
"""Test that hex core is not modified when third core to full core changer is called on an
already full core geometry.
.. test: Convert a one-third core to full core and restore back to one-third core.
:id: T_ARMI_THIRD_TO_FULL_CORE2
:tests: R_ARMI_THIRD_TO_FULL_CORE
"""
# Check the initialization of the third core model and convert to a full core
self.assertFalse(self.r.core.isFullCore)
self.assertEqual(
self.r.core.symmetry,
geometry.SymmetryType(
geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC
),
)
numBlocksThirdCore = len(self.r.core.getBlocks())
# convert the third core to full core
changer = geometryConverters.ThirdCoreHexToFullCoreChanger(self.o.cs)
with mockRunLogs.BufferLog() as mock:
changer.convert(self.r)
self.assertIn("Expanding to full core geometry", mock.getStdout())
numBlocksFullCore = len(self.r.core.getBlocks())
self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE)
# try to convert to full core again (it shouldn't do anything)
with mockRunLogs.BufferLog() as mock:
changer.convert(self.r)
self.assertIn(
"Detected that full core reactor already exists. Cannot expand.",
mock.getStdout(),
)
self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE)
self.assertEqual(numBlocksFullCore, len(self.r.core.getBlocks()))
# restore back to 1/3 core
with mockRunLogs.BufferLog() as mock:
changer.restorePreviousGeometry(self.r)
self.assertIn("revert from full to 1/3 core", mock.getStdout())
self.assertEqual(numBlocksThirdCore, len(self.r.core.getBlocks()))
self.assertEqual(
self.r.core.symmetry,
geometry.SymmetryType(
geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC
),
)