diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py index 0fc7f11dc..ce052aae5 100644 --- a/armi/reactor/components/__init__.py +++ b/armi/reactor/components/__init__.py @@ -27,8 +27,12 @@ Class inheritance diagram for :py:mod:`armi.reactor.components`. """ + +import math + +import numpy + from armi.reactor.components.component import * # pylint: disable=wildcard-import -from armi.reactor.components.componentCategories import * # pylint: disable=wildcard-import from armi.reactor.components.basicShapes import * # pylint: disable=wildcard-import from armi.reactor.components.complexShapes import * # pylint: disable=wildcard-import from armi.reactor.components.volumetricShapes import * # pylint: disable=wildcard-import @@ -71,3 +75,255 @@ def _removeDimensionNameSpaces(attrs): if " " in key: clean = key.replace(" ", "_") attrs[clean] = attrs.pop(key) + + +# Below are a few component base classes + + +class NullComponent(Component): + r"""returns zero for all dimensions. is none. """ + + def __cmp__(self, other): + r"""be smaller than everything. """ + return -1 + + def __lt__(self, other): + return True + + def __bool__(self): + r"""handles truth testing. """ + return False + + __nonzero__ = __bool__ # Python2 compatibility + + def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): + return None + + def getDimension(self, key, Tc=None, cold=False): + return 0.0 + + +class UnshapedComponent(Component): + """ + A component with undefined dimensions. + + Useful for situations where you just want to enter the area directly. + """ + + pDefs = componentParameters.getUnshapedParameterDefinitions() + + def __init__( + self, + name, + material, + Tinput, + Thot, + area=numpy.NaN, + op=None, + modArea=None, + isotopics=None, # pylint: disable=too-many-arguments + mergeWith=None, + components=None, + ): + Component.__init__( + self, + name, + material, + Tinput, + Thot, + area=area, + isotopics=isotopics, + mergeWith=mergeWith, + components=components, + ) + self._linkAndStoreDimensions(components, op=op, modArea=modArea) + + def getComponentArea(self, cold=False): + """ + Get the area of this component in cm^2. + + Parameters + ---------- + cold : bool, optional + Compute the area with as-input dimensions instead of thermally-expanded + """ + return self.p.area + + def setArea(self, val): + self.p.area = val + self.clearCache() + + def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): + """ + Approximate it as circular and return the radius. + + This is the smallest it can possibly be. Since this is used to determine + the outer component, it will never be allowed to be the outer one. + """ + return math.sqrt(self.p.area / math.pi) + + +class UnshapedVolumetricComponent(UnshapedComponent): + """ + A component with undefined dimensions. + + Useful for situations where you just want to enter the volume directly. + + See Also + -------- + armi.reactor.batch.Batch + """ + + is3D = True + + def __init__( + self, + name, + material, + Tinput, + Thot, + area=numpy.NaN, + op=None, + isotopics=None, # pylint: disable=too-many-arguments + mergeWith=None, + components=None, + volume=numpy.NaN, + ): + Component.__init__( + self, + name, + material, + Tinput, + Thot, + area=area, + isotopics=isotopics, + mergeWith=mergeWith, + components=components, + ) + self._linkAndStoreDimensions(components, op=op, userDefinedVolume=volume) + + def getComponentArea(self, cold=False): + return self.getVolume() / self.parent.getHeight() + + def getComponentVolume(self): + """Get the volume of the component in cm^3.""" + return self.getDimension("userDefinedVolume") + + def setVolume(self, volume): + self.setDimension("userDefinedVolume", volume) + self.clearCache() + + +class ZeroMassComponent(UnshapedVolumetricComponent): + """ + A component that never has mass -- it always returns zero for getMass and + getNumberDensity + + Useful for situations where you want to give a block integrated flux, but ensure + mass is never added to it + + See Also + -------- + armi.reactor.batch.makeMgFluxBlock + """ + + def getNumberDensity(self, *args, **kwargs): + """ + Always return 0 because this component has not mass + """ + return 0.0 + + def setNumberDensity(self, *args, **kwargs): + """ + Never add mass + """ + pass + + +class PositiveOrNegativeVolumeComponent(UnshapedVolumetricComponent): + """ + A component that may have negative mass for removing mass from batches + + See Also + -------- + armi.reactor.batch.makeMassAdditionComponent + """ + + def _checkNegativeVolume(self, volume): + """ + Allow negative areas. + """ + pass + + +class DerivedShape(UnshapedComponent): + """ + This a component that does have specific dimensions, but they're complicated. + + Notes + ---- + - This component type is "derived" through the addition or + subtraction of other shaped components (e.g. Coolant) + """ + + def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): + """ + The bounding circle for a derived component. + + Notes + ----- + This is used to sort components relative to one another. + + There can only be one derived component per block, this is generally the coolant inside a + duct. Under most circumstances, the volume (or area) of coolant will be greater than any + other (single) component (i.e. a single pin) within the assembly. So, sorting based on the + Dh of the DerivedShape will result in somewhat expected results. + """ + if self.parent is None: + # since this is only used for comparison, and it must be smaller than at + # least one component, make it 0 instead of infinity. + return 0.0 + else: + # area = pi r**2 = pi d**2 / 4 => d = sqrt(4*area/pi) + return math.sqrt(4.0 * self.getComponentArea() / math.pi) + + def computeVolume(self): + """Cannot compute volume until it is derived.""" + return self.parent._deriveUndefinedVolume() # pylint: disable=protected-access + + def getVolume(self): + """ + Get volume of derived shape. + + The DerivedShape must pay attention to all of the companion objects, because if they change, this changes. + However it's inefficient to always recompute the derived volume, so we have to rely on the parent to know + if anything has changed. + + Since each parent is only allowed one DerivedShape, we can reset the update flag here. + + Returns + ------- + float + volume of component in cm^3. + + """ + if self.parent.derivedMustUpdate: + # tell _updateVolume to update it during the below getVolume call + self.p.volume = None + self.parent.derivedMustUpdate = False + vol = UnshapedComponent.getVolume(self) + return vol + + def getComponentArea(self, cold=False): + """ + Get the area of this component in cm^2. + + Parameters + ---------- + cold : bool, optional + Ignored for this component + """ + if self.parent.derivedMustUpdate: + self.computeVolume() + + return self.p.area diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py index 5c6768ee6..f1fc855db 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -21,7 +21,7 @@ import math -from armi.reactor.components.componentCategories import ShapedComponent +from armi.reactor.components import ShapedComponent from armi.reactor.components import componentParameters @@ -314,6 +314,10 @@ def getComponentArea(self, cold=False): area = mult * (widthO * widthO - widthI * widthI) return area + def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): + widthO = self.getDimension("widthOuter", Tc, cold=cold) + return math.sqrt(widthO ** 2 + widthO ** 2) + class Triangle(ShapedComponent): """ diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py index 26c81d1a3..e08955cb7 100644 --- a/armi/reactor/components/complexShapes.py +++ b/armi/reactor/components/complexShapes.py @@ -18,7 +18,7 @@ import math -from armi.reactor.components.componentCategories import ShapedComponent +from armi.reactor.components import ShapedComponent from armi.reactor.components import componentParameters from armi.reactor.components import basicShapes diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index b61b8bae7..1b4f0fcd4 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -65,7 +65,7 @@ def componentTypeIsValid(component, name): `DerivedShape` if the coolant dimensions are not provided. """ - from armi.reactor.components.componentCategories import NullComponent + from armi.reactor.components import NullComponent if name.lower() == "coolant": invalidComponentTypes = [Component, NullComponent] @@ -1240,6 +1240,12 @@ def getMicroSuffix(self): return self.parent.getMicroSuffix() +class ShapedComponent(Component): + """A component with well-defined dimensions.""" + + pass + + def getReactionRateDict(nucName, lib, xsType, mgFlux, nDens): """ Parameters diff --git a/armi/reactor/components/componentCategories.py b/armi/reactor/components/componentCategories.py deleted file mode 100644 index 92dbadf3b..000000000 --- a/armi/reactor/components/componentCategories.py +++ /dev/null @@ -1,276 +0,0 @@ -# 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. - -""" -Basic component categories. -""" -import math - -import numpy - -from armi.reactor.components import Component -from armi.reactor.components import componentParameters - - -class NullComponent(Component): - r"""returns zero for all dimensions. is none. """ - - def __cmp__(self, other): - r"""be smaller than everything. """ - return -1 - - def __lt__(self, other): - return True - - def __bool__(self): - r"""handles truth testing. """ - return False - - __nonzero__ = __bool__ # Python2 compatibility - - def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): - return None - - def getDimension(self, key, Tc=None, cold=False): - return 0.0 - - -class UnshapedComponent(Component): - """ - A component with undefined dimensions. - - Useful for situations where you just want to enter the area directly. - """ - - pDefs = componentParameters.getUnshapedParameterDefinitions() - - def __init__( - self, - name, - material, - Tinput, - Thot, - area=numpy.NaN, - op=None, - modArea=None, - isotopics=None, # pylint: disable=too-many-arguments - mergeWith=None, - components=None, - ): - Component.__init__( - self, - name, - material, - Tinput, - Thot, - area=area, - isotopics=isotopics, - mergeWith=mergeWith, - components=components, - ) - self._linkAndStoreDimensions(components, op=op, modArea=modArea) - - def getComponentArea(self, cold=False): - """ - Get the area of this component in cm^2. - - Parameters - ---------- - cold : bool, optional - Compute the area with as-input dimensions instead of thermally-expanded - """ - return self.p.area - - def setArea(self, val): - self.p.area = val - self.clearCache() - - def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): - """ - Approximate it as circular and return the radius. - - This is the smallest it can possibly be. Since this is used to determine - the outer component, it will never be allowed to be the outer one. - """ - return math.sqrt(self.p.area / math.pi) - - -class UnshapedVolumetricComponent(UnshapedComponent): - """ - A component with undefined dimensions. - - Useful for situations where you just want to enter the volume directly. - - See Also - -------- - armi.reactor.batch.Batch - """ - - is3D = True - - def __init__( - self, - name, - material, - Tinput, - Thot, - area=numpy.NaN, - op=None, - isotopics=None, # pylint: disable=too-many-arguments - mergeWith=None, - components=None, - volume=numpy.NaN, - ): - Component.__init__( - self, - name, - material, - Tinput, - Thot, - area=area, - isotopics=isotopics, - mergeWith=mergeWith, - components=components, - ) - self._linkAndStoreDimensions(components, op=op, userDefinedVolume=volume) - - def getComponentArea(self, cold=False): - return self.getVolume() / self.parent.getHeight() - - def getComponentVolume(self): - """Get the volume of the component in cm^3.""" - return self.getDimension("userDefinedVolume") - - def setVolume(self, volume): - self.setDimension("userDefinedVolume", volume) - self.clearCache() - - -class ZeroMassComponent(UnshapedVolumetricComponent): - """ - A component that never has mass -- it always returns zero for getMass and - getNumberDensity - - Useful for situations where you want to give a block integrated flux, but ensure - mass is never added to it - - See Also - -------- - armi.reactor.batch.makeMgFluxBlock - """ - - def getNumberDensity(self, *args, **kwargs): - """ - Always return 0 because this component has not mass - """ - return 0.0 - - def setNumberDensity(self, *args, **kwargs): - """ - Never add mass - """ - pass - - -class PositiveOrNegativeVolumeComponent(UnshapedVolumetricComponent): - """ - A component that may have negative mass for removing mass from batches - - See Also - -------- - armi.reactor.batch.makeMassAdditionComponent - """ - - def _checkNegativeVolume(self, volume): - """ - Allow negative areas. - """ - pass - - -class ShapedComponent(Component): - """A component with well-defined dimensions.""" - - -class DerivedShape(UnshapedComponent): - """ - This a component that does have specific dimensions, but they're complicated. - - Notes - ---- - - This component type is "derived" through the addition or - subtraction of other shaped components (e.g. Coolant) - """ - - def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): - """ - The bounding circle for a derived component. - - Notes - ----- - This is used to sort components relative to one another. - - There can only be one derived component per block, this is generally the coolant inside a - duct. Under most circumstances, the volume (or area) of coolant will be greater than any - other (single) component (i.e. a single pin) within the assembly. So, sorting based on the - Dh of the DerivedShape will result in somewhat expected results. - """ - if self.parent is None: - # since this is only used for comparison, and it must be smaller than at - # least one component, make it 0 instead of infinity. - return 0.0 - else: - # area = pi r**2 = pi d**2 / 4 => d = sqrt(4*area/pi) - return math.sqrt(4.0 * self.getComponentArea() / math.pi) - - def computeVolume(self): - """Cannot compute volume until it is derived.""" - return self.parent._deriveUndefinedVolume() # pylint: disable=protected-access - - def getVolume(self): - """ - Get volume of derived shape. - - The DerivedShape must pay attention to all of the companion objects, because if they change, this changes. - However it's inefficient to always recompute the derived volume, so we have to rely on the parent to know - if anything has changed. - - Since each parent is only allowed one DerivedShape, we can reset the update flag here. - - Returns - ------- - float - volume of component in cm^3. - - """ - if self.parent.derivedMustUpdate: - # tell _updateVolume to update it during the below getVolume call - self.p.volume = None - self.parent.derivedMustUpdate = False - vol = UnshapedComponent.getVolume(self) - return vol - - def getComponentArea(self, cold=False): - """ - Get the area of this component in cm^2. - - Parameters - ---------- - cold : bool, optional - Ignored for this component - """ - if self.parent.derivedMustUpdate: - self.computeVolume() - - return self.p.area diff --git a/armi/reactor/components/volumetricShapes.py b/armi/reactor/components/volumetricShapes.py index 2f06450e9..b30124fb1 100644 --- a/armi/reactor/components/volumetricShapes.py +++ b/armi/reactor/components/volumetricShapes.py @@ -17,7 +17,7 @@ import math from armi.reactor.components import componentParameters -from armi.reactor.components.componentCategories import ShapedComponent +from armi.reactor.components import ShapedComponent class Sphere(ShapedComponent): diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 6099e9e34..2d864fa28 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -24,7 +24,7 @@ from armi.reactor import components import armi.runLog as runLog import armi.settings as settings -from armi.reactor.components import componentCategories +from armi.reactor.components import UnshapedComponent from armi import materials from armi.nucDirectory import nucDir, nuclideBases from armi.utils.units import MOLES_PER_CC_TO_ATOMS_PER_BARN_CM @@ -1126,7 +1126,7 @@ def test_UnshapedGetPitch(self): block = blocks.HexBlock("TestHexBlock", location=None) outerPitch = 2.0 block.addComponent( - componentCategories.UnshapedComponent( + UnshapedComponent( "TestComponent", "Void", Tinput=25.0, Thot=25.0, op=outerPitch ) ) diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index dc5fc771d..3cacb0035 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -27,6 +27,8 @@ Circle, Hexagon, HoledHexagon, + HoledRectangle, + HoledSquare, Helix, Sphere, Cube, @@ -704,6 +706,75 @@ def test_dimensionThermallyExpands(self): self.assertEqual(cur, ref[i]) +class TestHoledRectangle(TestShapedComponent): + """Tests HoledRectangle, and provides much support for HoledSquare test.""" + + componentCls = HoledRectangle + componentDims = { + "Tinput": 25.0, + "Thot": 430.0, + "lengthOuter": 16.0, + "widthOuter": 10.0, + "holeOD": 3.6, + "mult": 1.0, + } + + dimsToTestExpansion = ["lengthOuter", "widthOuter", "holeOD", "mult"] + + def setUp(self): + TestShapedComponent.setUp(self) + self.setClassDims() + + def setClassDims(self): + # This enables subclassing testing for square + self.length = self.component.getDimension("lengthOuter") + self.width = self.component.getDimension("widthOuter") + + def test_getBoundingCircleOuterDiameter(self): + # hypotenuse + ref = (self.length ** 2 + self.width ** 2) ** 0.5 + cur = self.component.getBoundingCircleOuterDiameter() + self.assertAlmostEqual(ref, cur) + + def test_getArea(self): + rectArea = self.length * self.width + odHole = self.component.getDimension("holeOD") + mult = self.component.getDimension("mult") + holeArea = math.pi * ((odHole / 2.0) ** 2) + ref = mult * (rectArea - holeArea) + cur = self.component.getArea() + self.assertAlmostEqual(cur, ref) + + def test_thermallyExpands(self): + self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) + + def test_dimensionThermallyExpands(self): + ref = [True] * len(self.dimsToTestExpansion) + ref[-1] = False # mult shouldn't expand + for i, d in enumerate(self.dimsToTestExpansion): + cur = d in self.component.THERMAL_EXPANSION_DIMS + self.assertEqual(cur, ref[i]) + + +class TestHoledSquare(TestHoledRectangle): + + componentCls = HoledSquare + + componentDims = { + "Tinput": 25.0, + "Thot": 430.0, + "widthOuter": 16.0, + "holeOD": 3.6, + "mult": 1.0, + } + + dimsToTestExpansion = ["widthOuter", "holeOD", "mult"] + + def setClassDims(self): + # This enables subclassing testing for square + self.width = self.length = self.component.getDimension("widthOuter") + + class TestHelix(TestShapedComponent): componentCls = Helix componentDims = { diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index d766dd336..a7110fa46 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -27,9 +27,8 @@ from armi.reactor import batch from armi.reactor import blocks from armi.reactor import assemblies -from armi.reactor.components import componentCategories from armi.reactor.components import basicShapes -from armi.reactor.components.componentCategories import UnshapedVolumetricComponent +from armi.reactor.components import UnshapedVolumetricComponent from armi.materials import custom from armi.reactor import locations from armi.reactor import grids @@ -438,7 +437,7 @@ def test_addMass(self): mass = 1.0 aB = batch.Batch("testBatch") - c = componentCategories.UnshapedVolumetricComponent( + c = UnshapedVolumetricComponent( "batchMassAdditionComponent", custom.Custom(), 0.0, 0.0, volume=1 ) b = blocks.Block("testBlock", location=loc) @@ -482,7 +481,7 @@ def test_setMass(self): mass = 1.0 aB = batch.Batch("testBatch") - c = componentCategories.UnshapedVolumetricComponent( + c = UnshapedVolumetricComponent( "batchMassAdditionComponent", custom.Custom(), 0.0, 0.0, volume=1 ) b = blocks.Block("testBlock", location=loc)