Skip to content

Commit

Permalink
Merge 8e4d3ca into c3872e8
Browse files Browse the repository at this point in the history
  • Loading branch information
opotowsky committed Aug 2, 2022
2 parents c3872e8 + 8e4d3ca commit 86c38e4
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 62 deletions.
150 changes: 93 additions & 57 deletions armi/cases/case.py
Expand Up @@ -33,7 +33,7 @@
import time
import textwrap
import ast
from typing import Dict, Optional, Sequence, Set
from typing import Dict, Optional, Sequence, Set, Union
import glob
import tabulate
import six
Expand Down Expand Up @@ -715,39 +715,75 @@ def writeInputs(self, sourceDir: Optional[str] = None):
self.cs.writeToYamlFile(self.title + ".yaml")


def copyInputsHelper(
fileDescription: str, fileFullPath: pathlib.Path, destPath: pathlib.Path
) -> str:
"""
Helper function for copyInterfaceInputs: Creates an absolute file path, and
copies the file to that location.
Parameters
----------
fileDescription : str
A file description for the copyOrWarn method
fileFullPath : pathlib.Path object
The absolute file path of the file to copy
destPath : pathlib.Path object
The target directory to copy input files to
Returns
-------
destFilePath : str
"""

sourceName = os.path.basename(fileFullPath.name)
destFilePath = os.path.abspath(destPath / sourceName)
pathTools.copyOrWarn(fileDescription, fileFullPath, destFilePath)
return destFilePath


def copyInterfaceInputs(
cs, destination: str, sourceDir: Optional[str] = None
) -> Dict[str, str]:
) -> Dict[str, Union[str, list]]:
"""
Copy sets of files that are considered "input" from each active interface.
Ping active interfaces to determine which files are considered "input". This
enables developers to add new inputs in a plugin-dependent/ modular way.
This enables developers to add new inputs in a plugin-dependent/ modular way.
If the file paths are absolute, do nothing. The case will be able to find the file.
In parameter sweeps, these often have a sourceDir associated with them that is
different from the cs.inputDirectory.
In case suites or parameter sweeps, these files often have a sourceDir associated
with them that is different from the cs.inputDirectory. So, if relative, update
the file paths to be absolute in the case settings and copy the file to the
destination directory.
Parameters
----------
cs : CaseSettings
The source case settings to find input files
destination: str
destination : str
The target directory to copy input files to
sourceDir: str, optional
sourceDir : str, optional
The directory from which to copy files. Defaults to cs.inputDirectory
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
Notes
-----
Glob is used to offer support for wildcards.
This may seem a bit overly complex, but a lot of the behavior is important. Relative
paths are copied into the target directory, which in some cases requires updating
the setting that pointed to the file in the first place. This is necessary to avoid
case dependencies in relavive locations above the input directory, which can lead to
issues when cloneing case suites. In the future this could be simplified by adding a
concept for a suite root directory, below which it is safe to copy files without
needing to update settings that point with a relative path to files that are below
it.
Regarding the handling of relative file paths: In the future this could be
simplified by adding a concept for a suite root directory, below which it is safe
to copy files without needing to update settings that point with a relative path
to files that are below it.
"""
activeInterfaces = interfaces.getActiveInterfaceInfo(cs)
Expand All @@ -756,72 +792,72 @@ def copyInterfaceInputs(
destPath = pathlib.Path(destination)

newSettings = {}
globFilePaths = None

assert destPath.is_dir()

for klass, _ in activeInterfaces:
interfaceFileNames = klass.specifyInputs(cs)
# returned files can be absolute paths, relative paths, or even glob patterns.
# Since we don't have an explicit way to signal about these, we sort of have to
# guess. In future, it might be nice to have interfaces specify which
# explicitly.
for key, files in interfaceFileNames.items():

if not isinstance(key, settings.Setting):
try:
key = cs.getSetting(key)
except NonexistentSetting(key):
raise ValueError(
"{} is not a valid setting. Ensure the relevant specifyInputs method uses a correct setting name.".format(
key
)
f"{key} is not a valid setting. Ensure the relevant specifyInputs "
f"method uses a correct setting name."
)
label = key.name

newSettings[label] = []
for f in files:
path = pathlib.Path(f)
if path.is_absolute() and path.exists() and path.is_file():
# looks like an extant, absolute path; no need to do anything
# Path is absolute, no settings modification or filecopy needed
pass
else:
# An OSError can occur if a wildcard is included in the file name so
# this is wrapped in a try/except to circumvent instances where an
# interface requests to copy multiple files based on some prefix/suffix.
# Path is either relative or includes a wildcard
try:
if not (path.exists() and path.is_file()):
runLog.extra(
f"Input file `{f}` not found. Checking for file at path `{sourceDirPath}`"
f"Input file for `{label}` setting could not be resolved "
f"with the following file path: `{path}`. Checking for "
f"file at path `{sourceDirPath}`."
)
except OSError:
pass

# relative path/glob. Should be safe to just use glob resolution.
# Note that `glob.glob` is being used here rather than `pathlib.glob` because
# `pathlib.glob` for Python 3.7.2 does not handle case sensitivity for file and
# path names. This is required for copying and using Python scripts (e.g., fuel management,
# control logic, etc.).
srcFiles = [
pathlib.Path(os.path.join(sourceDirPath, g))
for g in glob.glob(os.path.join(sourceDirPath, f))
]
for sourceFullPath in srcFiles:
if not sourceFullPath:
continue
sourceName = os.path.basename(sourceFullPath.name)
destFilePath = os.path.abspath(destPath / sourceName)
pathTools.copyOrWarn(label, sourceFullPath, destFilePath)
if len(srcFiles) == 0:
runLog.warning(
f"No input files for `{label}` could be resolved "
f"with the following file path: `{f}`."
)
elif len(srcFiles) > 1:
runLog.warning(
f"Input files for `{label}` resolved to more "
f"than one file; cannot update settings safely. "
f"Discovered input files: {srcFiles}"
# 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."
)
elif len(srcFiles) == 1:
newSettings[label] = str(destFilePath)

# 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}`."
)
# 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))
else:
for gFile in globFilePaths:
destFilePath = copyInputsHelper(label, gFile, destPath)
newSettings[label].append(str(destFilePath))
return newSettings
91 changes: 86 additions & 5 deletions armi/cases/tests/test_cases.py
Expand Up @@ -19,12 +19,13 @@
import os
import io
import platform
import pathlib

from armi import cases
from armi import plugins
from armi import settings
from armi.utils import directoryChangers
from armi.tests import ARMI_RUN_PATH
from armi.tests import TEST_ROOT
from armi.tests import ARMI_RUN_PATH, TEST_ROOT
from armi.reactor import blueprints, systemLayoutInput


Expand Down Expand Up @@ -272,20 +273,100 @@ def test_writeInput(self):
self.assertTrue(os.path.exists(cs["shuffleLogic"]))


class TestPluginForCopyInterfaces(plugins.ArmiPlugin):
@staticmethod
@plugins.HOOKIMPL
def defineSettings():
return [
settings.setting.Setting(
"multipleFilesSetting",
default=[],
label="multiple files",
description="testing stuff",
)
]


class TestCopyInterfaceInputs(unittest.TestCase):
"""Ensure filepath is updated properly."""
"""Ensure file path is found and updated properly."""

def test_copyInputsHelper(self):
"""Test the helper function for copyInterfaceInputs."""
testSetting = "shuffleLogic"
cs = settings.Settings(ARMI_RUN_PATH)
shuffleFile = cs[testSetting]
# null case, give it just the base of shuffleFile
with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in TEST_ROOT
destFilePath = cases.case.copyInputsHelper(
testSetting,
fileFullPath=pathlib.Path(shuffleFile),
destPath=pathlib.Path(newDir.destination),
)
newFilepath = os.path.join(newDir.destination, shuffleFile)
self.assertEqual(destFilePath, str(newFilepath))
# test with full filepath too
fileFullPath = pathlib.Path(os.path.join(TEST_ROOT, shuffleFile))
with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in TEST_ROOT
destFilePath = cases.case.copyInputsHelper(
testSetting,
fileFullPath=fileFullPath,
destPath=pathlib.Path(newDir.destination),
)
newFilepath = os.path.join(newDir.destination, shuffleFile)
self.assertEqual(destFilePath, str(newFilepath))

def test_copyInterfaceInputs(self):
def test_copyInterfaceInputs_singleFile(self):
testSetting = "shuffleLogic"
cs = settings.Settings(ARMI_RUN_PATH)
shuffleFile = cs[testSetting]
with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in IN_USE_TEST_ROOT
with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in TEST_ROOT
newSettings = cases.case.copyInterfaceInputs(
cs, destination=newDir.destination
)
newFilepath = os.path.join(newDir.destination, shuffleFile)
self.assertEqual(newSettings[testSetting], str(newFilepath))

def test_copyInterfaceInputs_nonFilePath(self):
testSetting = "shuffleLogic"
cs = settings.Settings(ARMI_RUN_PATH)
fakeShuffle = "fakeFile.py"
cs = cs.modified(newSettings={testSetting: fakeShuffle})
with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in TEST_ROOT
self.assertRaises(
Exception,
cases.case.copyInterfaceInputs(cs, destination=newDir.destination),
)

# def test_copyInterfaceInputs_multipleFiles(self):
# testSetting = "multipleFilesSetting"
# cs = settings.Settings(ARMI_RUN_PATH)
# settingFiles = ["ISOAA", "COMPXS.ascii"]
# cs = cs.modified(
# newSettings={
# testSetting: settingFiles,
# "testing": ["armi.tests.test_something.TestPluginCopyInterfaces"],
# }
# )
# with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in TEST_ROOT
# newSettings = cases.case.copyInterfaceInputs(
# cs, destination=newDir.destination
# )
# newFilepaths = [os.path.join(newDir.destination, f) for f in settingFiles]
# self.assertEqual(newSettings[testSetting], newFilepaths)

def test_copyInterfaceInputs_wildcardFile(self):
testSetting = "shuffleLogic"
cs = settings.Settings(ARMI_RUN_PATH)
# Use something that isn't the shuffle logic file in the case settings
wcFile = "ISO*"
cs = cs.modified(newSettings={testSetting: wcFile})
with directoryChangers.TemporaryDirectoryChanger() as newDir: # ensure we are not in TEST_ROOT
newSettings = cases.case.copyInterfaceInputs(
cs, destination=newDir.destination
)
newFilepath = [os.path.join(newDir.destination, "ISOAA")]
self.assertEqual(newSettings[testSetting], newFilepath)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions doc/release/0.2.rst
Expand Up @@ -37,6 +37,7 @@ Bug fixes
#. Remove ``copy.deepcopy`` from ``armi/reactor/converters/uniformMesh.py``
#. Clarify docstring for ``armi/reactor/components/complexShapes.py::Helix``
#. Bug fix in ``armi/reactor/components/complexShapes.py::Helix::getCircleInnerDiameter``
#. Bug fix in ``armi/cases/case.py::copyInterfaceInputs``
#. TBD


Expand Down

0 comments on commit 86c38e4

Please sign in to comment.