diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py index 404c29be77..ce052aae5c 100644 --- a/armi/reactor/components/__init__.py +++ b/armi/reactor/components/__init__.py @@ -27,9 +27,14 @@ 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.shapes 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 @@ -70,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 62441df7a1..f1fc855dbb 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -21,7 +21,7 @@ import math -from armi.reactor.components.shapes import ShapedComponent +from armi.reactor.components import ShapedComponent from armi.reactor.components import componentParameters @@ -145,126 +145,6 @@ def getPerimeter(self, Tc=None): return perimeter -class ShieldBlock(Hexagon): - """Solid hexagonal block with n uniform circular holes hollowed out of it.""" - - is3D = False - - THERMAL_EXPANSION_DIMS = {"op", "holeOD"} - - pDefs = componentParameters.getShieldBlockParameterDefinitions() - - def __init__( - self, - name, - material, - Tinput, - Thot, - op, - holeOD, - nHoles, - mult=1.0, - modArea=None, - isotopics=None, - mergeWith=None, - components=None, - ): - ShapedComponent.__init__( - self, - name, - material, - Tinput, - Thot, - isotopics=isotopics, - mergeWith=mergeWith, - components=components, - ) - self._linkAndStoreDimensions( - components, op=op, holeOD=holeOD, nHoles=nHoles, mult=mult, modArea=modArea - ) - - def getComponentArea(self, cold=False): - r"""Computes the area for the hexagon with n number of circular holes in cm^2.""" - op = self.getDimension("op", cold=cold) - holeOD = self.getDimension("holeOD", cold=cold) - nHoles = self.getDimension("nHoles", cold=cold) - mult = self.getDimension("mult") - hexArea = math.sqrt(3.0) / 2.0 * (op ** 2) - circularArea = nHoles * math.pi * ((holeOD / 2.0) ** 2) - area = mult * (hexArea - circularArea) - return area - - -class Helix(ShapedComponent): - """A spiral wire component used to model a pin wire-wrap. - - Notes - ----- - http://mathworld.wolfram.com/Helix.html - In a single rotation with an axial climb of P, the length of the helix will be a factor of - 2*pi*sqrt(r^2+c^2)/2*pi*c longer than vertical length L. P = 2*pi*c. - """ - - is3D = False - - THERMAL_EXPANSION_DIMS = {"od", "id", "axialPitch", "helixDiameter"} - - pDefs = componentParameters.getHelixParameterDefinitions() - - def __init__( - self, - name, - material, - Tinput, - Thot, - od=None, - axialPitch=None, - mult=None, - helixDiameter=None, - id=0.0, - modArea=None, - isotopics=None, - mergeWith=None, - components=None, - ): - ShapedComponent.__init__( - self, - name, - material, - Tinput, - Thot, - isotopics=isotopics, - mergeWith=mergeWith, - components=components, - ) - self._linkAndStoreDimensions( - components, - od=od, - axialPitch=axialPitch, - mult=mult, - helixDiameter=helixDiameter, - id=id, - modArea=modArea, - ) - - def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): - return self.getDimension("od", Tc, cold) + self.getDimension( - "helixDiameter", Tc, cold=cold - ) - - def getComponentArea(self, cold=False): - """Computes the area for the helix in cm^2.""" - ap = self.getDimension("axialPitch", cold=cold) - hd = self.getDimension("helixDiameter", cold=cold) - id = self.getDimension("id", cold=cold) - od = self.getDimension("od", cold=cold) - mult = self.getDimension("mult") - c = ap / (2.0 * math.pi) - helixFactor = math.sqrt((hd / 2.0) ** 2 + c ** 2) / c - area = mult * math.pi * ((od / 2.0) ** 2 - (id / 2.0) ** 2) * helixFactor - return area - - class Rectangle(ShapedComponent): """A rectangle component.""" @@ -434,8 +314,20 @@ 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): + """ + Triangle with defined base and height. + + Notes + ----- + The exact angles of the triangle are undefined. The exact side lenths and angles + are not critical to calculation of component area, so area can still be calculated. + """ is3D = False diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py new file mode 100644 index 0000000000..e08955cb71 --- /dev/null +++ b/armi/reactor/components/complexShapes.py @@ -0,0 +1,240 @@ +# 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. + +""" +Components represented by complex shapes, and typically less widely used. +""" + +import math + +from armi.reactor.components import ShapedComponent +from armi.reactor.components import componentParameters +from armi.reactor.components import basicShapes + + +class HoledHexagon(basicShapes.Hexagon): + """Hexagon with n uniform circular holes hollowed out of it.""" + + THERMAL_EXPANSION_DIMS = {"op", "holeOD"} + + pDefs = componentParameters.getHoledHexagonParameterDefinitions() + + def __init__( + self, + name, + material, + Tinput, + Thot, + op, + holeOD, + nHoles, + mult=1.0, + modArea=None, + isotopics=None, + mergeWith=None, + components=None, + ): + ShapedComponent.__init__( + self, + name, + material, + Tinput, + Thot, + isotopics=isotopics, + mergeWith=mergeWith, + components=components, + ) + self._linkAndStoreDimensions( + components, op=op, holeOD=holeOD, nHoles=nHoles, mult=mult, modArea=modArea + ) + + def getComponentArea(self, cold=False): + r"""Computes the area for the hexagon with n number of circular holes in cm^2.""" + op = self.getDimension("op", cold=cold) + holeOD = self.getDimension("holeOD", cold=cold) + nHoles = self.getDimension("nHoles", cold=cold) + mult = self.getDimension("mult") + hexArea = math.sqrt(3.0) / 2.0 * (op ** 2) + circularArea = nHoles * math.pi * ((holeOD / 2.0) ** 2) + area = mult * (hexArea - circularArea) + return area + + +class HoledRectangle(basicShapes.Rectangle): + """Rectangle with one circular hole in it.""" + + THERMAL_EXPANSION_DIMS = {"lengthOuter", "widthOuter", "holeOD"} + + pDefs = componentParameters.getHoledRectangleParameterDefinitions() + + def __init__( + self, + name, + material, + Tinput, + Thot, + holeOD, + lengthOuter=None, + widthOuter=None, + mult=1.0, + modArea=None, + isotopics=None, + mergeWith=None, + components=None, + ): + ShapedComponent.__init__( + self, + name, + material, + Tinput, + Thot, + isotopics=isotopics, + mergeWith=mergeWith, + components=components, + ) + self._linkAndStoreDimensions( + components, + lengthOuter=lengthOuter, + widthOuter=widthOuter, + holeOD=holeOD, + mult=mult, + modArea=modArea, + ) + + def getComponentArea(self, cold=False): + r"""Computes the area (in cm^2) for the the rectangle with one hole in it.""" + length = self.getDimension("lengthOuter", cold=cold) + width = self.getDimension("widthOuter", cold=cold) + rectangleArea = length * width + holeOD = self.getDimension("holeOD", cold=cold) + circularArea = math.pi * ((holeOD / 2.0) ** 2) + mult = self.getDimension("mult") + area = mult * (rectangleArea - circularArea) + return area + + +class HoledSquare(basicShapes.Square): + """Square with one circular hole in it.""" + + THERMAL_EXPANSION_DIMS = {"widthOuter", "holeOD"} + + pDefs = componentParameters.getHoledRectangleParameterDefinitions() + + def __init__( + self, + name, + material, + Tinput, + Thot, + holeOD, + widthOuter=None, + mult=1.0, + modArea=None, + isotopics=None, + mergeWith=None, + components=None, + ): + ShapedComponent.__init__( + self, + name, + material, + Tinput, + Thot, + isotopics=isotopics, + mergeWith=mergeWith, + components=components, + ) + self._linkAndStoreDimensions( + components, widthOuter=widthOuter, holeOD=holeOD, mult=mult, modArea=modArea + ) + + def getComponentArea(self, cold=False): + r"""Computes the area (in cm^2) for the the square with one hole in it.""" + width = self.getDimension("widthOuter", cold=cold) + rectangleArea = width ** 2 + holeOD = self.getDimension("holeOD", cold=cold) + circularArea = math.pi * ((holeOD / 2.0) ** 2) + mult = self.getDimension("mult") + area = mult * (rectangleArea - circularArea) + return area + + +class Helix(ShapedComponent): + """A spiral wire component used to model a pin wire-wrap. + + Notes + ----- + http://mathworld.wolfram.com/Helix.html + In a single rotation with an axial climb of P, the length of the helix will be a factor of + 2*pi*sqrt(r^2+c^2)/2*pi*c longer than vertical length L. P = 2*pi*c. + """ + + is3D = False + + THERMAL_EXPANSION_DIMS = {"od", "id", "axialPitch", "helixDiameter"} + + pDefs = componentParameters.getHelixParameterDefinitions() + + def __init__( + self, + name, + material, + Tinput, + Thot, + od=None, + axialPitch=None, + mult=None, + helixDiameter=None, + id=0.0, + modArea=None, + isotopics=None, + mergeWith=None, + components=None, + ): + ShapedComponent.__init__( + self, + name, + material, + Tinput, + Thot, + isotopics=isotopics, + mergeWith=mergeWith, + components=components, + ) + self._linkAndStoreDimensions( + components, + od=od, + axialPitch=axialPitch, + mult=mult, + helixDiameter=helixDiameter, + id=id, + modArea=modArea, + ) + + def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): + return self.getDimension("od", Tc, cold) + self.getDimension( + "helixDiameter", Tc, cold=cold + ) + + def getComponentArea(self, cold=False): + """Computes the area for the helix in cm^2.""" + ap = self.getDimension("axialPitch", cold=cold) + hd = self.getDimension("helixDiameter", cold=cold) + id = self.getDimension("id", cold=cold) + od = self.getDimension("od", cold=cold) + mult = self.getDimension("mult") + c = ap / (2.0 * math.pi) + helixFactor = math.sqrt((hd / 2.0) ** 2 + c ** 2) / c + area = mult * math.pi * ((od / 2.0) ** 2 - (id / 2.0) ** 2) * helixFactor + return area diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index c90ac92594..1b4f0fcd4b 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.shapes 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/componentParameters.py b/armi/reactor/components/componentParameters.py index 5197917ef0..2117abd6b7 100644 --- a/armi/reactor/components/componentParameters.py +++ b/armi/reactor/components/componentParameters.py @@ -141,8 +141,8 @@ def getHexagonParameterDefinitions(): return pDefs -def getShieldBlockParameterDefinitions(): - """Return parameters for ShieldBlock.""" +def getHoledHexagonParameterDefinitions(): + """Return parameters for HoledHexagon.""" pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder(location=ParamLocation.AVERAGE, saveToDB=True) as pb: @@ -153,6 +153,16 @@ def getShieldBlockParameterDefinitions(): return pDefs +def getHoledRectangleParameterDefinitions(): + """Return parameters for HoledRectangle.""" + + pDefs = parameters.ParameterDefinitionCollection() + with pDefs.createBuilder(location=ParamLocation.AVERAGE, saveToDB=True) as pb: + pb.defParam("holeOD", units="?", description="?") + + return pDefs + + def getHelixParameterDefinitions(): """Return parameters for Helix.""" diff --git a/armi/reactor/components/shapes.py b/armi/reactor/components/shapes.py deleted file mode 100644 index c843271af0..0000000000 --- a/armi/reactor/components/shapes.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. - -""" -Component subclasses that have shape. -""" -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 f109f57639..b30124fb1d 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.shapes 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 6acbead2ec..2d864fa282 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 shapes +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( - shapes.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 8a2df8e9d7..3cacb00352 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -26,7 +26,9 @@ NullComponent, Circle, Hexagon, - ShieldBlock, + HoledHexagon, + HoledRectangle, + HoledSquare, Helix, Sphere, Cube, @@ -101,7 +103,6 @@ def test_componentInitializationAndDuplication(self): thisAttrs["name"] = "banana{}".format(i) if "modArea" in thisAttrs: thisAttrs["modArea"] = None - component = components.factory(name, [], thisAttrs) duped = copy.deepcopy(component) for key, val in component.p.items(): @@ -667,8 +668,8 @@ def test_dimensionThermallyExpands(self): self.assertEqual(cur, ref[i]) -class TestShieldBlock(TestShapedComponent): - componentCls = ShieldBlock +class TestHoledHexagon(TestShapedComponent): + componentCls = HoledHexagon componentDims = { "Tinput": 25.0, "Thot": 430.0, @@ -705,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 2e1480419f..a7110fa464 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 shapes from armi.reactor.components import basicShapes -from armi.reactor.components.shapes 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 = shapes.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 = shapes.UnshapedVolumetricComponent( + c = UnshapedVolumetricComponent( "batchMassAdditionComponent", custom.Custom(), 0.0, 0.0, volume=1 ) b = blocks.Block("testBlock", location=loc)