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

# 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 math
import numpy as np

from armi.physics.neutronics.isotopicDepletion.isotopicDepletionInterface import (
    isDepletable,
)
from armi.reactor import blocks, components, grids
from armi.reactor.converters import blockConverters
from armi.reactor.flags import Flags
from armi.reactor.tests.test_blocks import loadTestBlock
from armi.reactor.tests.test_reactors import TEST_ROOT, loadTestReactor
from armi.utils import hexagon
from armi.utils.directoryChangers import TemporaryDirectoryChanger


[docs]def buildSimpleFuelBlockNegativeArea(): """ Return a simple block containing fuel, clad, duct, and coolant. The block has a negative-area gap between fuel and cladding for testing. """ b = blocks.HexBlock("fuel", height=10.0) fuelDims = {"Tinput": 25, "Thot": 600, "od": 0.76, "id": 0.00, "mult": 127.0} cladDims = {"Tinput": 25, "Thot": 600, "od": 0.80, "id": 0.76, "mult": 127.0} ductDims = {"Tinput": 25, "Thot": 600, "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} fuel = components.Circle("fuel", "UZr", **fuelDims) clad = components.Circle("clad", "HT9", **cladDims) gapDims = { "Tinput": 25, "Thot": 600, "od": "clad.id", "id": "fuel.od", "mult": 127.0, } gapDims["components"] = {"fuel": fuel, "clad": clad} gap = components.Circle("gap", "Void", **gapDims) duct = components.Hexagon("duct", "HT9", **ductDims) coolant = components.DerivedShape("coolant", "Sodium", **coolDims) intercoolant = components.Hexagon("intercoolant", "Sodium", **intercoolantDims) b.add(fuel) b.add(gap) b.add(clad) b.add(duct) b.add(coolant) b.add(intercoolant) b.getVolumeFractions() return b
[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_dissolveMultiple(self): """Test dissolving multiple components into another.""" self._test_dissolve_multi(loadTestBlock(), ["wire", "clad"], "coolant") self._test_dissolve_multi( loadTestBlock(), ["inner liner", "outer liner"], "clad" )
[docs] def test_dissolveZeroArea(self): """Test dissolving a zero-area component into another.""" self._test_dissolve(loadTestBlock(), "gap2", "outer liner")
[docs] def test_dissolveIntoZeroArea(self): """Test dissolving a component into a zero-area solvent (raises ValueError).""" with self.assertRaises(ValueError): self._test_dissolve(loadTestBlock(), "outer liner", "gap2")
[docs] def test_dissolveNegativeArea(self): """Test dissolving a zero-area component into another.""" with self.assertRaises(ValueError): self._test_dissolve(buildSimpleFuelBlockNegativeArea(), "gap", "clad")
[docs] def test_dissolveIntoNegativeArea(self): """Test dissolving a zero-area component into another.""" with self.assertRaises(ValueError): self._test_dissolve(buildSimpleFuelBlockNegativeArea(), "clad", "gap")
def _test_dissolve_multi(self, block, soluteNames, solventName): converter = blockConverters.MultipleComponentMerger( block, soluteNames, solventName ) convertedBlock = converter.convert() for soluteName in soluteNames: 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_buildInsideDuct(self): """Test building inside the duct.""" block = loadTestBlock(cold=False) block.spatialGrid = grids.HexGrid.fromPitch(1.0) converter = blockConverters.HexComponentsToCylConverter(block) converter._buildInsideDuct() insideBlock = converter.convertedBlock ductIP = block.getComponent(Flags.DUCT).getDimension("ip") bondMass = block.getComponent(Flags.BOND).getMass("NA") coolantMass = block.getComponent(Flags.COOLANT).getMass("NA") self.assertAlmostEqual(insideBlock.getMass("U235"), block.getMass("U235")) self.assertAlmostEqual(insideBlock.getMass("NA"), bondMass + coolantMass) self.assertAlmostEqual(insideBlock.getArea(), ductIP**2 * math.sqrt(3) / 2)
[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(np.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(np.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) return b