Skip to content

Commit

Permalink
Merge remote-tracking branch 'zprince/component_flux_up' into drewj/b…
Browse files Browse the repository at this point in the history
…u-rotate-with-pin-dep

* zprince/component_flux_up:
  fix linting
  release notes
  Enabling axial expansion with detailed depletion (#1954)
  Improving error testing (#2004)
  Addressing reviewer comments for component pin mg fluxes
  Removing mystery coverage line from tests (#2003)
  beef up assertions
  resolve fixme
  org imports
  No need to recast strings to strings
  Finishing up the numProcessors -> nTasks conversion (#2002)
  Using one-block reactor for component flux test
  Supporting Python 3.13 (#1996)
  Removing SmartList & adding coverage (#1992)
  Update `copyOrWarn` and `getFileSHA1Hash` to account for directories (#1984)
  Removing broken plot (#1994)
  Adding unit tests for `CylindricalComponentsDuctHetAverageBlockCollection` (#1991)
  Allowing creation of partially heterogeneous assemblies for 1D XS model (#1949)
  • Loading branch information
drewj-tp committed Nov 7, 2024
2 parents bd3c7ea + 9f7168d commit 4ed2f3f
Show file tree
Hide file tree
Showing 39 changed files with 722 additions and 288 deletions.
1 change: 1 addition & 0 deletions .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=pyproject.toml -m pytest --cov=armi --cov-config=pyproject.toml --cov-report=lcov --cov-append --ignore=venv armi/tests/test_mpiParameters.py || true
mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=pyproject.toml -m pytest --cov=armi --cov-config=pyproject.toml --cov-report=lcov --cov-append --ignore=venv armi/tests/test_mpiDirectoryChangers.py || true
coverage combine --rcfile=pyproject.toml --keep -a
coverage report --rcfile=pyproject.toml -i --skip-empty --skip-covered --sort=cover --fail-under=90
- name: Publish to coveralls.io
uses: coverallsapp/github-action@v2
with:
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/unittests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
python: [3.9, '3.10', '3.11', '3.12']
python: [3.9, '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Update package index
run: sudo apt-get update
- name: Install mpi libs
Expand All @@ -37,6 +38,6 @@ jobs:
run: |
pip install -e .[memprof,mpi,test]
pytest -n 4 armi
mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=pyproject.toml -m pytest --cov=armi --cov-config=pyproject.toml --ignore=venv armi/tests/test_mpiFeatures.py || true
mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=pyproject.toml -m pytest --cov=armi --cov-config=pyproject.toml --ignore=venv armi/tests/test_mpiParameters.py || true
mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=pyproject.toml -m pytest --cov=armi --cov-config=pyproject.toml --ignore=venv armi/utils/tests/test_directoryChangersMpi.py || true
mpiexec -n 2 --use-hwthread-cpus pytest armi/tests/test_mpiFeatures.py
mpiexec -n 2 --use-hwthread-cpus pytest armi/tests/test_mpiParameters.py
mpiexec -n 2 --use-hwthread-cpus pytest armi/utils/tests/test_directoryChangersMpi.py
8 changes: 5 additions & 3 deletions armi/bookkeeping/db/tests/test_comparedb3.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,17 @@ def test_diffSpecialData(self):
refData4 = f4.create_dataset("numberDensities", data=a2)
refData4.attrs["shapes"] = "2"
refData4.attrs["numDens"] = a2
refData4.attrs["specialFormatting"] = True
f5 = h5py.File("test_diffSpecialData5.hdf5", "w")
srcData5 = f5.create_dataset("numberDensities", data=a2)
srcData5.attrs["shapes"] = "2"
srcData5.attrs["numDens"] = a2
srcData5.attrs["specialFormatting"] = True

# there should an exception
with self.assertRaises(Exception) as e:
# there should a log message
with mockRunLogs.BufferLog() as mock:
_diffSpecialData(refData4, srcData5, out, dr)
self.assertIn("Unable to unpack special data for paramName", e)
self.assertIn("Unable to unpack special data for", mock.getStdout())

# make an H5 datasets that will add a np.inf diff because keys don't match
f6 = h5py.File("test_diffSpecialData6.hdf5", "w")
Expand Down
7 changes: 4 additions & 3 deletions armi/cases/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ def _copyInputsHelper(
-------
destFilePath (or origFile) : str
"""
sourceName = os.path.basename(sourcePath)
sourceName = pathlib.Path(sourcePath).name
destFilePath = os.path.join(destPath, sourceName)
try:
pathTools.copyOrWarn(fileDescription, sourcePath, destFilePath)
Expand All @@ -880,9 +880,10 @@ def copyInterfaceInputs(
This function should now be able to handle the updating of:
- a single file (relative or absolute)
- a list of files (relative or absolute), and
- a list of files (relative or absolute)
- a file entry that has a wildcard processing into multiple files.
Glob is used to offer support for wildcards.
- a directory and its contents
If the file paths are absolute, do nothing. The case will be able to find the file.
Expand Down Expand Up @@ -950,7 +951,7 @@ def copyInterfaceInputs(
path = pathlib.Path(f)
if not WILDCARD and not RELATIVE:
try:
if path.is_absolute() and path.exists() and path.is_file():
if path.is_absolute() and path.exists():
# Path is absolute, no settings modification or filecopy needed
newFiles.append(path)
continue
Expand Down
2 changes: 1 addition & 1 deletion armi/nuclearDataIO/nuclearFileMetadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,4 @@ def _getSkippedKeys(self, other, selfContainer, otherContainer, mergedData):


class NuclideMetadata(_Metadata):
"""Simple dictionary for providing metadata about how to read/write a nuclde to/from a file."""
"""Simple dictionary for providing metadata about how to read/write a nuclide to/from a file."""
2 changes: 1 addition & 1 deletion armi/nuclearDataIO/tests/test_xsLibraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def test_mergeFailsWithNonIsotxsFiles(self):
os.remove(dummyFileName)

with TemporaryDirectoryChanger():
dummyFileName = "ISOtopics.txt"
dummyFileName = "ISO[]"
with open(dummyFileName, "w") as file:
file.write(
"This is a file that starts with the letters 'ISO' but will"
Expand Down
4 changes: 2 additions & 2 deletions armi/nuclearDataIO/xsLibraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def getISOTXSLibrariesToMerge(xsLibrarySuffix, xsLibFileNames):
isosWithSuffix = [
iso
for iso in isosToMerge
if re.match(f".*ISO[A-Z]{{2}}F?{xsLibrarySuffix}$", iso)
if re.match(f".*ISO[A-Za-z]{{2}}F?{xsLibrarySuffix}$", iso)
]
isosToMerge = [
iso
Expand Down Expand Up @@ -193,7 +193,7 @@ def mergeXSLibrariesInWorkingDirectory(
for xsLibFilePath in sorted(xsLibFiles):
try:
# get XS ID from the cross section library name
xsID = re.search("ISO([A-Z0-9]{2})", xsLibFilePath).group(1)
xsID = re.search("ISO([A-Z0-9a-z]{2})", xsLibFilePath).group(1)
except AttributeError:
# if glob has matched something that is not actually an ISOXX file,
# the .group() call will fail
Expand Down
79 changes: 77 additions & 2 deletions armi/physics/neutronics/crossSectionGroupManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
from armi.physics.neutronics.const import CONF_CROSS_SECTION
from armi.reactor import flags
from armi.reactor.components import basicShapes
from armi.reactor.converters.blockConverters import stripComponents
from armi.reactor.flags import Flags
from armi.utils.units import TRACE_NUMBER_DENSITY

Expand Down Expand Up @@ -623,7 +624,11 @@ def _getAverageComponentNucs(self, components, bWeights):
weight = bWeight * c.getArea()
totalWeight += weight
densities += weight * np.array(c.getNuclideNumberDensities(allNucNames))
return allNucNames, densities / totalWeight
if totalWeight > 0.0:
weightedDensities = densities / totalWeight
else:
weightedDensities = np.zeros_like(densities)
return allNucNames, weightedDensities

def _orderComponentsInGroup(self, repBlock):
"""Order the components based on dimension and material type within the representative
Expand All @@ -648,6 +653,64 @@ def _getNucTempHelper(self):
return nvt, nv


class CylindricalComponentsDuctHetAverageBlockCollection(
CylindricalComponentsAverageBlockCollection
):
"""
Creates a representative block for the purpose of cross section generation with a one-
dimensional cylindrical model where all material inside the duct is homogenized.
.. impl:: Create partially heterogeneous representative blocks.
:id: I_ARMI_XSGM_CREATE_REPR_BLOCKS2
:implements: R_ARMI_XSGM_CREATE_REPR_BLOCKS
This class constructs representative blocks based on a volume-weighted average using
cylindrical blocks from an existing block list. Inheriting functionality from the abstract
:py:class:`Reactor <armi.physics.neutronics.crossSectionGroupManager.BlockCollection>`
object, this class will construct representative blocks using averaged parameters of all
blocks in the given collection. Number density averages are computed at a component level.
Nuclide temperatures from a median block-average temperature are used and the average burnup
is evaluated across all blocks in the block list.
The average nuclide temperatures are calculated only for the homogenized region inside of
the duct. For the non-homogenized regions, the MC2 writer uses the component temperatures.
Notes
-----
The representative block for this collection is the same as the parent. The only difference between
the two collection types is that this collection calculates average nuclide temperatures based only
on the components that are inside of the duct.
"""

def _getNewBlock(self):
newBlock = copy.deepcopy(self._selectCandidateBlock())
newBlock.name = "1D_CYL_DUCT_HET_AVG_" + newBlock.getMicroSuffix()
return newBlock

def _makeRepresentativeBlock(self):
"""Build a representative fuel block based on component number densities."""
self.calcAvgNuclideTemperatures()
return CylindricalComponentsAverageBlockCollection._makeRepresentativeBlock(
self
)

def _getNucTempHelper(self):
"""All candidate blocks are used in the average."""
nvt = np.zeros(len(self.allNuclidesInProblem))
nv = np.zeros(len(self.allNuclidesInProblem))
for block in self.getCandidateBlocks():
wt = self.getWeight(block)
# remove the duct and intercoolant from the block before
# calculating average nuclide temps
newBlock, _mixtureFlags = stripComponents(block, Flags.DUCT)
nvtBlock, nvBlock = getBlockNuclideTemperatureAvgTerms(
newBlock, self.allNuclidesInProblem
)
nvt += nvtBlock * wt
nv += nvBlock * wt
return nvt, nv


class SlabComponentsAverageBlockCollection(BlockCollection):
"""
Creates a representative 1D slab block.
Expand Down Expand Up @@ -789,7 +852,11 @@ def _getAverageComponentNucs(self, components, bWeights):
weight = bWeight * c.getArea()
totalWeight += weight
densities += weight * np.array(c.getNuclideNumberDensities(allNucNames))
return allNucNames, densities / totalWeight
if totalWeight > 0.0:
weightedDensities = densities / totalWeight
else:
weightedDensities = np.zeros_like(densities)
return allNucNames, weightedDensities

def _orderComponentsInGroup(self, repBlock):
"""Order the components based on dimension and material type within the representative block."""
Expand Down Expand Up @@ -1520,6 +1587,9 @@ def updateNuclideTemperatures(self, blockCollectionByXsGroup=None):
FLUX_WEIGHTED_AVERAGE_BLOCK_COLLECTION = "FluxWeightedAverage"
SLAB_COMPONENTS_BLOCK_COLLECTION = "ComponentAverage1DSlab"
CYLINDRICAL_COMPONENTS_BLOCK_COLLECTION = "ComponentAverage1DCylinder"
CYLINDRICAL_COMPONENTS_DUCT_HET_BLOCK_COLLECTION = (
"ComponentAverage1DCylinderDuctHeterogeneous"
)

# Mapping between block collection string constants and their
# respective block collection classes.
Expand All @@ -1529,12 +1599,17 @@ def updateNuclideTemperatures(self, blockCollectionByXsGroup=None):
FLUX_WEIGHTED_AVERAGE_BLOCK_COLLECTION: FluxWeightedAverageBlockCollection,
SLAB_COMPONENTS_BLOCK_COLLECTION: SlabComponentsAverageBlockCollection,
CYLINDRICAL_COMPONENTS_BLOCK_COLLECTION: CylindricalComponentsAverageBlockCollection,
CYLINDRICAL_COMPONENTS_DUCT_HET_BLOCK_COLLECTION: CylindricalComponentsDuctHetAverageBlockCollection,
}


def blockCollectionFactory(xsSettings, allNuclidesInProblem):
"""Build a block collection based on user settings and input."""
blockRepresentation = xsSettings.blockRepresentation
if (
blockRepresentation == CYLINDRICAL_COMPONENTS_BLOCK_COLLECTION
) and xsSettings.ductHeterogeneous:
blockRepresentation = CYLINDRICAL_COMPONENTS_DUCT_HET_BLOCK_COLLECTION
validBlockTypes = xsSettings.validBlockTypes
averageByComponent = xsSettings.averageByComponent
return BLOCK_COLLECTIONS[blockRepresentation](
Expand Down
47 changes: 46 additions & 1 deletion armi/physics/neutronics/crossSectionSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
CONF_HOMOGBLOCK = "useHomogenizedBlockComposition"
CONF_INTERNAL_RINGS = "numInternalRings"
CONF_MERGE_INTO_CLAD = "mergeIntoClad"
CONF_MERGE_INTO_FUEL = "mergeIntoFuel"
CONF_MESH_PER_CM = "meshSubdivisionsPerCm"
CONF_REACTION_DRIVER = "nuclideReactionDriver"
CONF_XSID = "xsID"
Expand All @@ -57,6 +58,8 @@
CONF_COMPONENT_AVERAGING = "averageByComponent"
CONF_XS_MAX_ATOM_NUMBER = "xsMaxAtomNumber"
CONF_MIN_DRIVER_DENSITY = "minDriverDensity"
CONF_DUCT_HETEROGENEOUS = "ductHeterogeneous"
CONF_TRACE_ISOTOPE_THRESHOLD = "traceIsotopeThreshold"


class XSGeometryTypes(Enum):
Expand Down Expand Up @@ -136,6 +139,7 @@ def getStr(cls, typeSpec: Enum):
CONF_XSID,
CONF_GEOM,
CONF_MERGE_INTO_CLAD,
CONF_MERGE_INTO_FUEL,
CONF_DRIVER,
CONF_HOMOGBLOCK,
CONF_INTERNAL_RINGS,
Expand All @@ -149,6 +153,8 @@ def getStr(cls, typeSpec: Enum):
CONF_XS_PRIORITY,
CONF_XS_MAX_ATOM_NUMBER,
CONF_MIN_DRIVER_DENSITY,
CONF_DUCT_HETEROGENEOUS,
CONF_TRACE_ISOTOPE_THRESHOLD,
},
XSGeometryTypes.getStr(XSGeometryTypes.TWO_DIMENSIONAL_HEX): {
CONF_XSID,
Expand Down Expand Up @@ -186,6 +192,7 @@ def getStr(cls, typeSpec: Enum):
vol.Optional(CONF_INTERNAL_RINGS): vol.Coerce(int),
vol.Optional(CONF_EXTERNAL_RINGS): vol.Coerce(int),
vol.Optional(CONF_MERGE_INTO_CLAD): [str],
vol.Optional(CONF_MERGE_INTO_FUEL): [str],
vol.Optional(CONF_XS_FILE_LOCATION): [str],
vol.Optional(CONF_EXTERNAL_FLUX_FILE_LOCATION): str,
vol.Optional(CONF_MESH_PER_CM): vol.Coerce(float),
Expand All @@ -194,6 +201,8 @@ def getStr(cls, typeSpec: Enum):
vol.Optional(CONF_XS_MAX_ATOM_NUMBER): vol.Coerce(int),
vol.Optional(CONF_MIN_DRIVER_DENSITY): vol.Coerce(float),
vol.Optional(CONF_COMPONENT_AVERAGING): bool,
vol.Optional(CONF_DUCT_HETEROGENEOUS): bool,
vol.Optional(CONF_TRACE_ISOTOPE_THRESHOLD): vol.Coerce(float),
}
)

Expand Down Expand Up @@ -410,6 +419,12 @@ class XSModelingOptions:
and is sometimes used to merge a "gap" or low-density region into
a "clad" region to avoid numerical issues.
mergeIntoFuel : list of str
This is a lattice physics configuration option that is a list of component
names to merge into a "fuel" component. This is highly-design specific
and is sometimes used to merge a "gap" or low-density region into
a "fuel" region to avoid numerical issues.
meshSubdivisionsPerCm : float
This is a lattice physics configuration option that can be used to control
subregion meshing of the representative block in 1D problems.
Expand All @@ -419,7 +434,7 @@ class XSModelingOptions:
no others will allocate to it. This is useful for time balancing when you
have one task that takes much longer than the others.
xsPriority:
xsPriority: int
The priority of the mpi tasks that results from this xsID. Lower priority
will execute first. starting longer jobs first is generally more efficient.
Expand All @@ -429,10 +444,31 @@ class XSModelingOptions:
(e.g., fission products) as a depletion product of an isotope with a much
smaller atomic number.
averageByComponent: bool
Controls whether the representative block averaging is performed on a
component-by-component basis or on the block as a whole. If True, the
resulting representative block will have component compositions that
largely reflect those of the underlying blocks in the collection. If
False, the number densities of some nuclides in the individual
components may not be reflective of those of the underlying components
due to the block number density "dehomogenization".
minDriverDensity: float
The minimum number density for nuclides included in driver material for a 1D
lattice physics model.
ductHeterogeneous : bool
This is a lattice physics configuration option used to enable a partially
heterogeneous approximation for a 1D cylindrical model. Everything inside of the
duct will be treated as homogeneous.
traceIsotopeThreshold : float
This is a lattice physics configuration option used to enable a separate 0D fuel
cross section calculation for trace fission products when using a 1D cross section
model. This can significantly reduce the memory and run time required for the 1D
model. The setting takes a float value that represents the number density cutoff
for isotopes to be considered "trace". If no value is provided, the default is 0.0.
Notes
-----
Not all default attributes may be useful for your specific application and you may
Expand All @@ -458,12 +494,15 @@ def __init__(
numInternalRings=None,
numExternalRings=None,
mergeIntoClad=None,
mergeIntoFuel=None,
meshSubdivisionsPerCm=None,
xsExecuteExclusive=None,
xsPriority=None,
xsMaxAtomNumber=None,
averageByComponent=False,
minDriverDensity=0.0,
ductHeterogeneous=False,
traceIsotopeThreshold=0.0,
):
self.xsID = xsID
self.geometry = geometry
Expand All @@ -482,10 +521,13 @@ def __init__(
self.numInternalRings = numInternalRings
self.numExternalRings = numExternalRings
self.mergeIntoClad = mergeIntoClad
self.mergeIntoFuel = mergeIntoFuel
self.meshSubdivisionsPerCm = meshSubdivisionsPerCm
self.xsMaxAtomNumber = xsMaxAtomNumber
self.minDriverDensity = minDriverDensity
self.averageByComponent = averageByComponent
self.ductHeterogeneous = ductHeterogeneous
self.traceIsotopeThreshold = traceIsotopeThreshold
# these are related to execution
self.xsExecuteExclusive = xsExecuteExclusive
self.xsPriority = xsPriority
Expand Down Expand Up @@ -668,12 +710,15 @@ def setDefaults(self, blockRepresentation, validBlockTypes):
CONF_GEOM: self.geometry,
CONF_DRIVER: "",
CONF_MERGE_INTO_CLAD: ["gap"],
CONF_MERGE_INTO_FUEL: [],
CONF_MESH_PER_CM: 1.0,
CONF_INTERNAL_RINGS: 0,
CONF_EXTERNAL_RINGS: 1,
CONF_HOMOGBLOCK: False,
CONF_BLOCK_REPRESENTATION: crossSectionGroupManager.CYLINDRICAL_COMPONENTS_BLOCK_COLLECTION,
CONF_BLOCKTYPES: validBlockTypes,
CONF_DUCT_HETEROGENEOUS: False,
CONF_TRACE_ISOTOPE_THRESHOLD: 0.0,
}
elif self.geometry == XSGeometryTypes.getStr(
XSGeometryTypes.TWO_DIMENSIONAL_HEX
Expand Down
Loading

0 comments on commit 4ed2f3f

Please sign in to comment.