Skip to content

Commit

Permalink
Merge branch 'main' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
HunterPSmith committed Aug 19, 2022
2 parents 622205c + 44f287d commit 6e34506
Show file tree
Hide file tree
Showing 35 changed files with 778 additions and 355 deletions.
23 changes: 0 additions & 23 deletions armi/__init__.py
Expand Up @@ -113,29 +113,6 @@ def isStableReleaseVersion(version=None):
return "-" not in version


def _registerUserPlugin(plugManager, userPluginName):
"""Register one individual user plugin by name."""
try:
pluginMod = importlib.import_module(userPluginName)
except ImportError:
runLog.error(
f"The plugin `{userPluginName}` could not be imported. Verify it is installed "
"in your current environment or adjust the active user plugins."
)
raise

# Each plugin must have a constant called PLUGIN pointing to the plugin class.
# This allows discoverability without being overly restrictive in class names
try:
plugManager.register(pluginMod.PLUGIN)
except AttributeError:
runLog.error(
f"The plugin `{userPluginName}` does not have a PLUGIN constant defined. "
"This constant is required in user plugins. Please adjust plugin."
)
raise


def init(choice=None, fName=None, cs=None):
"""
Scan a directory for armi inputs and load one to interact with.
Expand Down
2 changes: 0 additions & 2 deletions armi/apps.py
Expand Up @@ -259,8 +259,6 @@ def registerUserPlugins(self, pluginPaths):
because they are defined during run time, not import time. As such, we
restrict their flexibility and power as compared to the usual ArmiPlugins.
"""
self.__initNewPlugins()

for pluginPath in pluginPaths:
if ".py:" in pluginPath:
# The path is of the form: /path/to/why.py:MyPlugin
Expand Down
12 changes: 10 additions & 2 deletions armi/bookkeeping/db/compareDB3.py
Expand Up @@ -47,6 +47,7 @@
import collections
import os
import re
import traceback

from tabulate import tabulate
import h5py
Expand Down Expand Up @@ -360,8 +361,15 @@ def _diffSpecialData(
if not attrsMatch:
return

src = database3.unpackSpecialData(srcData[()], srcData.attrs, paramName)
ref = database3.unpackSpecialData(refData[()], refData.attrs, paramName)
try:
src = database3.unpackSpecialData(srcData[()], srcData.attrs, paramName)
ref = database3.unpackSpecialData(refData[()], refData.attrs, paramName)
except Exception:
runLog.error(
f"Unable to unpack special data for paramName {paramName}. "
f"{traceback.format_exc()}",
)
return

diff = []
for dSrc, dRef in zip(src.tolist(), ref.tolist()):
Expand Down
3 changes: 1 addition & 2 deletions armi/bookkeeping/db/database3.py
Expand Up @@ -1307,8 +1307,7 @@ def _writeParams(self, h5group, comps):
# flatten, store the data offsets and array shapes, and None locations
# as attrs
# - If not jagged, all top-level ndarrays are the same shape, so it is
# probably easier to replace Nones with ndarrays filled with special
# values.
# easier to replace Nones with ndarrays filled with special values.
if parameters.NoDefault in data:
data = None
else:
Expand Down
15 changes: 15 additions & 0 deletions armi/bookkeeping/db/tests/test_comparedb3.py
Expand Up @@ -204,6 +204,21 @@ def test_diffSpecialData(self):
self.assertEqual(dr.nDiffs(), 0)
self.assertIn("Special formatting parameters for", mock._outputStream)

# make an H5 datasets that will cause unpackSpecialData to fail
f4 = h5py.File("test_diffSpecialData4.hdf5", "w")
refData4 = f4.create_dataset("numberDensities", data=a2)
refData4.attrs["shapes"] = "2"
refData4.attrs["numDens"] = a2
f5 = h5py.File("test_diffSpecialData5.hdf5", "w")
srcData5 = f5.create_dataset("numberDensities", data=a2)
srcData5.attrs["shapes"] = "2"
srcData5.attrs["numDens"] = a2

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

def test_diffSimpleData(self):
dr = DiffResults(0.01)

Expand Down
43 changes: 42 additions & 1 deletion armi/bookkeeping/db/tests/test_database3.py
Expand Up @@ -14,14 +14,16 @@

r""" Tests for the Database3 class
"""
# pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access,no-member,disallowed-name,invalid-name
import subprocess
import unittest

import h5py
import numpy

from armi.bookkeeping.db import database3
from armi.bookkeeping.db import _getH5File, database3
from armi.reactor import grids
from armi.reactor import parameters
from armi.reactor.tests import test_reactors
from armi.tests import TEST_ROOT
from armi.utils import getPreviousTimeNode
Expand Down Expand Up @@ -50,6 +52,45 @@ def tearDown(self):
self.stateRetainer.__exit__()
self.td.__exit__(None, None, None)

def test_writeToDB(self):
self.r.p.cycle = 0
self.r.p.timeNode = 0
self.r.p.cycleLength = 0

# Adding some nonsense in, to test NoDefault params
self.r.p.availabilityFactor = parameters.NoDefault

# validate that the H5 file gets bigger after the write
self.assertEqual(list(self.db.h5db.keys()), ["inputs"])
self.db.writeToDB(self.r)
self.assertEqual(sorted(self.db.h5db.keys()), ["c00n00", "inputs"])

keys = [
"Circle",
"Core",
"DerivedShape",
"Helix",
"HexAssembly",
"HexBlock",
"Hexagon",
"Reactor",
"layout",
]
self.assertEqual(sorted(self.db.h5db["c00n00"].keys()), sorted(keys))

# validate availabilityFactor did not make it into the H5 file
rKeys = ["cycle", "cycleLength", "flags", "serialNum", "timeNode"]
self.assertEqual(
sorted(self.db.h5db["c00n00"]["Reactor"].keys()), sorted(rKeys)
)

def test_getH5File(self):
with self.assertRaises(TypeError):
_getH5File(None)

h5 = _getH5File(self.db)
self.assertEqual(type(h5), h5py.File)

def makeHistory(self):
"""Walk the reactor through a few time steps and write them to the db."""
for cycle, node in ((cycle, node) for cycle in range(3) for node in range(3)):
Expand Down
133 changes: 73 additions & 60 deletions armi/cases/case.py
Expand Up @@ -715,32 +715,45 @@ def writeInputs(self, sourceDir: Optional[str] = None):
self.cs.writeToYamlFile(self.title + ".yaml")


def copyInputsHelper(
fileDescription: str, fileFullPath: pathlib.Path, destPath: pathlib.Path
def _copyInputsHelper(
fileDescription: str,
sourcePath: str,
destPath: str,
origFile: str,
) -> str:
"""
Helper function for copyInterfaceInputs: Creates an absolute file path, and
copies the file to that location.
copies the file to that location. If that file path does not exist, returns
the file path from the original settings file.
Parameters
----------
fileDescription : str
A file description for the copyOrWarn method
fileFullPath : pathlib.Path object
sourcePath : str
The absolute file path of the file to copy
destPath : pathlib.Path object
destPath : str
The target directory to copy input files to
origFile : str
File path as defined in the original settings file
Returns
-------
destFilePath : str
destFilePath (or origFile) : str
"""
sourceName = os.path.basename(fileFullPath.name)
destFilePath = os.path.abspath(destPath / sourceName)
pathTools.copyOrWarn(fileDescription, fileFullPath, destFilePath)
return destFilePath
sourceName = os.path.basename(sourcePath)
destFilePath = os.path.join(destPath, sourceName)
try:
pathTools.copyOrWarn(fileDescription, sourcePath, destFilePath)
if pathlib.Path(destFilePath).exists():
return destFilePath
else:
return origFile
except Exception:
return origFile


def copyInterfaceInputs(
Expand Down Expand Up @@ -777,8 +790,9 @@ def copyInterfaceInputs(
Returns
-------
newSettings : dict
A new settings object that contains settings for the keys and either an
absolute file path or a list of absolute file paths for the values
A new settings object that contains settings for the keys and values that are
either an absolute file path, a list of absolute file paths, or the original
file path if absolute paths could not be resolved
Notes
-----
Expand All @@ -791,9 +805,8 @@ def copyInterfaceInputs(
activeInterfaces = interfaces.getActiveInterfaceInfo(cs)
sourceDir = sourceDir or cs.inputDirectory
sourceDirPath = pathlib.Path(sourceDir)
destPath = pathlib.Path(destination)

assert destPath.is_dir()
assert pathlib.Path(destination).is_dir()

newSettings = {}

Expand All @@ -810,55 +823,55 @@ def copyInterfaceInputs(
)
label = key.name

newSettings[label] = []
newFiles = []
for f in files:
globFilePaths = None
path = pathlib.Path(f)
if path.is_absolute() and path.exists() and path.is_file():
# Path is absolute, no settings modification or filecopy needed
pass
else:
# Path is either relative or includes a wildcard
if not (path.exists() and path.is_file()):
runLog.extra(
f"Input file for `{label}` setting could not be resolved "
f"with the following file path: `{path}`. Checking for "
f"file at path `{sourceDirPath}`."
)

# Attempt to find relative path file
sourceFullString = os.path.join(sourceDirPath, f)
sourceFullPath = pathlib.Path(sourceFullString)
if not os.path.exists(sourceFullPath):
runLog.extra(
f"Input file for `{label}` setting could not be resolved "
f"with the following file path: `{sourceFullPath}`. Checking "
f"for wildcards."
)
# Attempt to capture file paths from wildcards
globFilePaths = [
pathlib.Path(os.path.join(sourceDirPath, g))
for g in glob.glob(sourceFullString)
]
if len(globFilePaths) == 0:
runLog.warning(
f"No input files for `{label}` setting could be resolved "
f"with the following file path: `{sourceFullPath}`."
)
WILDCARD = False
RELATIVE = False
if "*" in f:
WILDCARD = True
if ".." in f:
RELATIVE = True

# Finally, copy + update settings according to file path type
if not globFilePaths:
destFilePath = copyInputsHelper(label, sourceFullPath, destPath)
# Some settings are a single filename. Others are lists of files.
# Either overwrite the empty list at the top of the loop, or
# append to it.
if len(files) == 1:
newSettings[label] = str(destFilePath)
else:
newSettings[label].append(str(destFilePath))
path = pathlib.Path(f)
if not WILDCARD and not RELATIVE:
try:
if path.is_absolute() and path.exists() and path.is_file():
# Path is absolute, no settings modification or filecopy needed
pass
except OSError:
pass
# Attempt to construct an absolute file path
sourceFullPath = os.path.join(sourceDirPath, f)
if WILDCARD:
globFilePaths = [
pathlib.Path(os.path.join(sourceDirPath, g))
for g in glob.glob(sourceFullPath)
]
if len(globFilePaths) == 0:
destFilePath = f
newFiles.append(str(destFilePath))
else:
for gFile in globFilePaths:
destFilePath = copyInputsHelper(label, gFile, destPath)
newSettings[label].append(str(destFilePath))
destFilePath = _copyInputsHelper(
label, gFile, destination, f
)
newFiles.append(str(destFilePath))
else:
destFilePath = _copyInputsHelper(
label, sourceFullPath, destination, f
)
newFiles.append(str(destFilePath))
if destFilePath == f:
runLog.info(
f"No input files for `{label}` setting could be resolved with "
f"the following path: `{sourceFullPath}`. Will not update "
f"`{label}`."
)

# Some settings are a single filename. Others are lists of files. Make
# sure we are returning what the setting expects
if len(files) == 1 and not WILDCARD:
newSettings[label] = newFiles[0]
else:
newSettings[label] = newFiles
return newSettings

0 comments on commit 6e34506

Please sign in to comment.