Source code for armi.reactor.converters.tests.test_geometryConverters

# 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, geometry, grids
from armi.reactor.converters import geometryConverters, uniformMesh
from armi.reactor.flags import Flags
from armi.testing 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.core) 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 ), )