Skip to content

Commit

Permalink
Merge branch 'main' into drewj/improve-assem-axial-linkage
Browse files Browse the repository at this point in the history
* main:
  Fixing HexBlock docstrings (#1981)
  Removing a duplicate hex rotation impl tag (#1979)
  Avoiding closing plots that are meant to be interactive (#1978)
  Ensuring HexBlock.rotate updates child spatial locators (#1943)
  Add new memory runLog info to memoryProfiler.py  (#1970)
  Fixing various doc build issues (#1974)
  Hiding sphinx-needs warnings during doc build (#1973)
  Fixing warnings in CLI startup (#1972)
  • Loading branch information
drewj-tp committed Oct 29, 2024
2 parents 71c7497 + 7ab064b commit 038b75e
Show file tree
Hide file tree
Showing 35 changed files with 1,113 additions and 225 deletions.
2 changes: 1 addition & 1 deletion armi/bookkeeping/db/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
hierarchical Composite Reactor Model. Furthermore, this database format is intended to
be more dynamic, permitting as-yet undeveloped levels and classes in the Composite
Reactor Model to be supported as they are added. More high-level discussion is
contained in :doc:`/user/outputs/database`.
contained in :ref:`database-file`.
The :py:class:`Database` class contains most of the functionality for interacting
with the underlying data. This includes things like dumping a Reactor state to the
Expand Down
49 changes: 48 additions & 1 deletion armi/bookkeeping/memoryProfiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
https://pythonhosted.org/psutil/
https://docs.python.org/3/library/gc.html#gc.garbage
"""
from math import floor
from os import cpu_count
from typing import Optional
import gc
import sys
Expand All @@ -46,6 +48,7 @@
from armi import runLog
from armi.reactor.composites import ArmiObject
from armi.utils import tabulate
from armi.utils.customExceptions import NonexistentSetting

try:
# psutil is an optional requirement, since it doesnt support MacOS very well
Expand All @@ -68,6 +71,28 @@ def describeInterfaces(cs):
return (MemoryProfiler, {})


def getTotalJobMemory(nTasksPerNode):
"""Function to calculate the total memory of a job. This is a constant during a simulation."""
cpuPerNode = cpu_count()
ramPerCpuGB = psutil.virtual_memory().total / (1024**3) / cpuPerNode
if nTasksPerNode == 0:
nTasksPerNode = cpuPerNode
cpusPerTask = floor(cpuPerNode / nTasksPerNode)
jobMem = nTasksPerNode * cpusPerTask * ramPerCpuGB
return jobMem


def getCurrentMemoryUsage():
"""This scavenges the memory profiler in ARMI to get the current memory usage."""
memUsageAction = PrintSystemMemoryUsageAction()
memUsageAction.broadcast()
smpu = SystemAndProcessMemoryUsage()
memUsages = memUsageAction.gather(smpu)
# Grab virtual memory instead of physical. There is a large discrepancy, we will be conservative
memoryUsageInMB = sum([mu.processVirtualMemoryInMB for mu in memUsages])
return memoryUsageInMB


class MemoryProfiler(interfaces.Interface):

name = "memoryProfiler"
Expand All @@ -78,6 +103,7 @@ def __init__(self, r, cs):

def interactBOL(self):
interfaces.Interface.interactBOL(self)
self.printCurrentMemoryState()
mpiAction = PrintSystemMemoryUsageAction()
mpiAction.broadcast().invoke(self.o, self.r, self.cs)
mpiAction.printUsage("BOL SYS_MEM")
Expand All @@ -88,6 +114,8 @@ def interactBOL(self):
mpiAction.broadcast().invoke(self.o, self.r, self.cs)

def interactEveryNode(self, cycle, node):
self.printCurrentMemoryState()

mp = PrintSystemMemoryUsageAction()
mp.broadcast()
mp.invoke(self.o, self.r, self.cs)
Expand All @@ -101,11 +129,30 @@ def interactEveryNode(self, cycle, node):
mpiAction.broadcast().invoke(self.o, self.r, self.cs)

def interactEOL(self):
r"""End of life hook. Good place to wrap up or print out summary outputs."""
"""End of life hook. Good place to wrap up or print out summary outputs."""
if self.cs["debugMem"]:
mpiAction = ProfileMemoryUsageAction("EOL")
mpiAction.broadcast().invoke(self.o, self.r, self.cs)

def printCurrentMemoryState(self):
"""Print the current memory footprint and available memory."""
try:
nTasksPerNode = self.cs["nTasksPerNode"]
except NonexistentSetting:
runLog.extra(
"To view memory consumed, remaining available, and total allocated for a case, "
"add the setting 'nTasksPerNode' to your application."
)
return
totalMemoryInGB = getTotalJobMemory(nTasksPerNode)
currentMemoryUsageInGB = getCurrentMemoryUsage() / 1024
availableMemoryInGB = totalMemoryInGB - currentMemoryUsageInGB
runLog.info(
f"Currently using {currentMemoryUsageInGB} GB of memory. "
f"There is {availableMemoryInGB} GB of memory left. "
f"There is a total allocation of {totalMemoryInGB} GB."
)

def displayMemoryUsage(self, timeDescription):
r"""
Print out some information to stdout about the memory usage of ARMI.
Expand Down
76 changes: 75 additions & 1 deletion armi/bookkeeping/tests/test_memoryProfiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@
# limitations under the License.

"""Tests for memoryProfiler."""
from unittest.mock import MagicMock, patch
import logging
import unittest

from armi import runLog
from armi.bookkeeping import memoryProfiler
from armi.bookkeeping.memoryProfiler import (
getCurrentMemoryUsage,
getTotalJobMemory,
)
from armi.reactor.tests import test_reactors
from armi.tests import mockRunLogs, TEST_ROOT

Expand All @@ -29,7 +34,9 @@ def setUp(self):
{"debugMem": True},
inputFileName="smallestTestReactor/armiRunSmallest.yaml",
)
self.memPro = self.o.getInterface("memoryProfiler")
self.memPro: memoryProfiler.MemoryProfiler = self.o.getInterface(
"memoryProfiler"
)

def tearDown(self):
self.o.removeInterface(self.memPro)
Expand Down Expand Up @@ -123,6 +130,73 @@ def test_profileMemoryUsageAction(self):
pmua = memoryProfiler.ProfileMemoryUsageAction("timeDesc")
self.assertEqual(pmua.timeDescription, "timeDesc")

@patch("psutil.virtual_memory")
@patch("armi.bookkeeping.memoryProfiler.cpu_count")
def test_getTotalJobMemory(self, mockCpuCount, mockVMem):
"""Use an example node with 50 GB of total physical memory and 10 CPUs."""
mockCpuCount.return_value = 10
vMem = MagicMock()
vMem.total = (1024**3) * 50
mockVMem.return_value = vMem

expectedArrangement = {0: 50, 1: 50, 2: 50, 3: 45, 4: 40, 5: 50}
for nTasksPerNode, jobMemory in expectedArrangement.items():
self.assertEqual(getTotalJobMemory(nTasksPerNode), jobMemory)

@patch("armi.bookkeeping.memoryProfiler.PrintSystemMemoryUsageAction")
@patch("armi.bookkeeping.memoryProfiler.SystemAndProcessMemoryUsage")
def test_getCurrentMemoryUsage(
self, mockSysAndProcMemUse, mockPrintSysMemUseAction
):
"""Mock the memory usage across 3 different processes and that the total usage is as expected (6 MB)."""
self._setMemUseMock(mockPrintSysMemUseAction)
self.assertAlmostEqual(getCurrentMemoryUsage(), 6 * 1024)

@patch("armi.bookkeeping.memoryProfiler.PrintSystemMemoryUsageAction")
@patch("armi.bookkeeping.memoryProfiler.SystemAndProcessMemoryUsage")
@patch("psutil.virtual_memory")
@patch("armi.bookkeeping.memoryProfiler.cpu_count")
def test_printCurrentMemoryState(
self, mockCpuCount, mockVMem, mock1, mockPrintSysMemUseAction
):
"""Use an example node with 50 GB of total physical memory and 10 CPUs while using 6 GB."""
mockCpuCount.return_value = 10
vMem = MagicMock()
vMem.total = (1024**3) * 50
mockVMem.return_value = vMem
self._setMemUseMock(mockPrintSysMemUseAction)
with mockRunLogs.BufferLog() as mockLogs:
csMock = MagicMock()
csMock.__getitem__.return_value = 2
self.memPro.cs = csMock
self.memPro.printCurrentMemoryState()
stdOut = mockLogs.getStdout()
self.assertIn("Currently using 6.0 GB of memory.", stdOut)
self.assertIn("There is 44.0 GB of memory left.", stdOut)
self.assertIn("There is a total allocation of 50.0 GB", stdOut)

def test_printCurrentMemoryState_noSetting(self):
"""Test that the try/except works as it should."""
expectedStr = (
"To view memory consumed, remaining available, and total allocated for a case, "
"add the setting 'nTasksPerNode' to your application."
)
with mockRunLogs.BufferLog() as mockLogs:
self.memPro.printCurrentMemoryState()
self.assertIn(expectedStr, mockLogs.getStdout())

def _setMemUseMock(self, mockPrintSysMemUseAction):
class mockMemUse:
def __init__(self, mem: float):
self.processVirtualMemoryInMB = mem

instance = mockPrintSysMemUseAction.return_value
instance.gather.return_value = [
mockMemUse(1 * 1024),
mockMemUse(2 * 1024),
mockMemUse(3 * 1024),
]


class KlassCounterTests(unittest.TestCase):
def get_containers(self):
Expand Down
2 changes: 1 addition & 1 deletion armi/materials/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Custom materials are ones that you can specify all the number densities yourself.
Useful for benchmarking when you have a particular specified material density.
Use the isotopic input described in :doc:`/user/inputs/blueprints`.
Use the isotopic input described in :ref:`bp-input-file`.
The density function gets applied from custom isotopics by
:py:meth:`armi.reactor.blueprints.isotopicOptions.CustomIsotopic.apply`.
Expand Down
4 changes: 1 addition & 3 deletions armi/materials/thU.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
"""
Thorium Uranium metal.
Data is from [IAEA-TECDOCT-1450]_.
Data is from [IAEA-TECDOC-1450]_.
.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf
"""

from armi import runLog
Expand Down
4 changes: 1 addition & 3 deletions armi/materials/thorium.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
"""
Thorium Metal.
Data is from [IAEA-TECDOCT-1450]_.
Data is from [IAEA-TECDOC-1450]_.
.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf
"""
from armi.materials.material import FuelMaterial
from armi.utils.units import getTk
Expand Down
4 changes: 2 additions & 2 deletions armi/materials/thoriumOxide.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"""
Thorium Oxide solid ceramic.
Data is from [IAEA-TECDOCT-1450]_.
Data is from [IAEA-TECDOC-1450]_.
.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
.. [IAEA-TECDOC-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf
"""
from armi import runLog
Expand Down
6 changes: 2 additions & 4 deletions armi/nuclearDataIO/xsNuclides.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,10 @@ def plotScatterMatrix(scatterMatrix, scatterTypeLabel="", fName=None):
pyplot.colorbar()
if fName:
pyplot.savefig(fName)
pyplot.close()
else:
pyplot.show()

pyplot.close()


def plotCompareScatterMatrix(scatterMatrix1, scatterMatrix2, fName=None):
"""Compares scatter matrices graphically between libraries."""
Expand All @@ -260,7 +259,6 @@ def plotCompareScatterMatrix(scatterMatrix1, scatterMatrix2, fName=None):
pyplot.colorbar()
if fName:
pyplot.savefig(fName)
pyplot.close()
else:
pyplot.show()

pyplot.close()
4 changes: 2 additions & 2 deletions armi/physics/fuelCycle/fuelHandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
The :py:class:`FuelHandlerInterface` instantiates a ``FuelHandler``, which is typically a user-defined
subclass the :py:class:`FuelHandler` object in custom shuffle-logic input files.
Users point to the code modules with their custom fuel handlers using the
``shuffleLogic`` and ``fuelHandlerName`` settings, as described in :doc:`/user/inputs/fuel_management`.
``shuffleLogic`` and ``fuelHandlerName`` settings, as described in :ref:`fuel-management-input`.
These subclasses override ``chooseSwaps`` that determine
the particular shuffling of a case.
Expand Down Expand Up @@ -177,7 +177,7 @@ def getFactorList(cycle, cs=None, fallBack=False):
This is the default shuffle control function. Usually you would override this
with your own in a custom shuffleLogic.py file. For more details about how this
works, refer to :doc:`/user/inputs/fuel_management`.
works, refer to :ref:`fuel-management-input`.
This will get bound to the default FuelHandler as a static method below. This is
done to allow a user to mix and match FuelHandler class implementations and
Expand Down
Loading

0 comments on commit 038b75e

Please sign in to comment.