Skip to content

Commit

Permalink
Merge dd3c789 into 6733433
Browse files Browse the repository at this point in the history
  • Loading branch information
ntouran committed Mar 6, 2020
2 parents 6733433 + dd3c789 commit 32bf13d
Show file tree
Hide file tree
Showing 21 changed files with 729 additions and 510 deletions.
4 changes: 2 additions & 2 deletions armi/bookkeeping/db/database3.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,9 +1489,9 @@ def _initComps(self, cs, bp):
Klass = ArmiObject.TYPES[compType]

if issubclass(Klass, Reactor):
comp = Klass(cs, bp)
comp = Klass(cs.caseTitle, bp)
elif issubclass(Klass, Core):
comp = Klass(name, cs)
comp = Klass(name)
elif issubclass(Klass, Component):
# XXX: initialize all dimensions to 0, they will be loaded and assigned
# after load
Expand Down
32 changes: 29 additions & 3 deletions armi/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,11 @@ def bolForce(self, flag=None): # pylint: disable=inconsistent-return-statements

def writeInput(self, inName):
"""Write input file(s)."""
pass
raise NotImplementedError()

def readOutput(self, outName):
"""Read output file(s)."""
pass
raise NotImplementedError()

@staticmethod
def specifyInputs(cs) -> Dict[str, List[str]]: # pylint: disable=unused-argument
Expand Down Expand Up @@ -482,7 +482,21 @@ def write(self, fName):


class OutputReader(object):
"""Read output files from external codes."""
"""
A generic representation of a particular module's output.
Attributes
----------
success : bool
False by default, set to True if the run is considered
to have completed without error.
Notes
-----
Should ideally not require r, eci, and fname arguments
and would rather just have an apply(reactor) method.
"""

def __init__(self, r=None, externalCodeInterface=None, fName=None):
self.externalCodeInterface = externalCodeInterface
Expand All @@ -494,6 +508,7 @@ def __init__(self, r=None, externalCodeInterface=None, fName=None):
else:
self.output = None
self.fName = fName
self.success = False

def getInterface(self, name):
"""Get another interface by name."""
Expand All @@ -505,6 +520,17 @@ def read(self, fileName):
"""Read the output file."""
raise NotImplementedError

def apply(self, reactor):
"""
Apply the output back to a reactor state.
This provides a generic interface for the output data of anything
to be applied to a reactor state. The application could involve
reading text or binary output or simply parameters to appropriate
values in some other data structure.
"""
raise NotImplementedError()


def getActiveInterfaceInfo(cs):
"""
Expand Down
20 changes: 5 additions & 15 deletions armi/operators/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ def getInterfaces(self):
"""
return self.interfaces[:]

def reattach(self, r, cs):
def reattach(self, r, cs=None):
"""Add links to globally-shared objects to this operator and all interfaces.
Notes
Expand All @@ -713,11 +713,13 @@ def reattach(self, r, cs):
"""
self.r = r
self.r.o = self
self.cs = cs
if cs is not None:
self.cs = cs
for i in self.interfaces:
i.r = r
i.o = self
i.cs = cs
if cs is not None:
i.cs = cs

def detach(self):
"""
Expand Down Expand Up @@ -876,18 +878,6 @@ def snapshotRequest(self, cycle, node):
else:
os.mkdir(newFolder)

inf = "{0}{1:03d}{2:03d}.inp".format(self.cs.caseTitle, cycle, node)
writer = self.getInterface("dif3d")

if not writer:
writer = self.getInterface("rebus")
if not writer:
runLog.warning(
"There are no interface attached that can write a snapshot input"
)
else:
writer.writeInput(os.path.join(newFolder, inf))

# copy the cross section inputs
for fileName in os.listdir("."):
if "mcc" in fileName and re.search(r"[A-Z]AF?\d?.inp", fileName):
Expand Down
4 changes: 2 additions & 2 deletions armi/operators/operatorMPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ def workerOperate(self):
bp = self.r.blueprints
spatialGrid = self.r.core.spatialGrid
self.detach()
self.r = reactors.Reactor(cs, bp)
core = reactors.Core("Core", cs)
self.r = reactors.Reactor(cs.caseTitle, bp)
core = reactors.Core("Core")
self.r.add(core)
core.spatialGrid = spatialGrid
self.reattach(self.r, cs)
Expand Down
177 changes: 177 additions & 0 deletions armi/physics/executers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
Executors are useful for having a standard way to run physics calculations.
They may involve external codes (with inputs/execution/output) or in-memory
data pathways.
"""

import os

from armi.utils import directoryChangers
from armi import runLog


class ExecutionOptions:
"""
A data structure representing all options needed for a physics kernel.
Attributes
----------
inputFile : str
Name of main input file. Often passed to stdin of external code.
outputFile : str
Name of main output file. Often the stdout of external code.
extraInputFiles : list of tuples
(sourceName, destName) pairs of file names that will be brought from the
working dir into the runDir. Allows renames while in transit.
extraOutputFiles : list of tuples
(sourceName, destName) pairs of file names that will be extracted from the
runDir to the working dir
executablePath : str
Path to external executable to run (if external code is used)
runDir : str
Path on running system where the run will take place. This is often used
to ensure external codes that use hard-drive disk space run on a local disk
rather than a shared network drive
workingDir : str
Path on system where results will be placed after the run. This is often
a shared network location. Auto-applied during execution by default.
label : str
A name for the run that may be used as a prefix for input/output files generated.
applyResultsToReactor : bool
Update the in-memory reactor model with results upon completion. Set to False
when information from a run is needed for auxiliary purposes rather than progressing
the reactor model.
"""

def __init__(self, label=None):
self.inputFile = None
self.outputFile = None
self.extraInputFiles = []
self.extraOutputFiles = []
self.executablePath = None
self.runDir = None
self.workingDir = None
self.label = label
self.applyResultsToReactor = True
self.paramsToScaleSubset = None

def fromUserSettings(self, cs):
"""Set options from a particular CaseSettings object."""
raise NotImplementedError()

def fromReactor(self, reactor):
"""Set options from a particular reactor object."""
raise NotImplementedError()

def resolveDerivedOptions(self):
"""Called by executers right before executing."""

def describe(self):
"""Make a string summary of all options."""
lines = ["Options summary:", "----------------"]
for key, val in self.__dict__.items():
if not key.startswith("_"):
lines.append(f" {key:40s}{str(val)[:80]:80s}")
return "\n".join(lines)


class Executer:
"""
Coordinates the execution of some calculation.
This is a semi-generic way to run some kind of calculation based on experience
of running many related calculations. It may
be an external code execution but could also be some internal calculation.
The generic characteristics of a execution are some optional subset of the following:
* Choose modeling options (either from the global run settings input or dictated programmatically)
* Apply geometry transformations to the ARMI Reactor as needed
* Build run-specific working directory
* Write input file(s)
* Put specific input files and libs in run directory
* Run the analysis (external execution, or not)
* Process output while still in run directory
* Check error conditions
* Move desired output files back to main working directory
* Clean up run directory
* Un-apply geometry transformations as needed
* Update ARMI data model as desired
Notes
-----
This is deliberately **not** a :py:class:`~mpiActions.MpiAction`. Thus, Executers can run as
potentially multiple steps in a parent (parallelizable ) MpiAction or in other flexible
ways. This is intended to maximize reusability.
"""

def __init__(self, options, reactor):
self.options = options
self.r = reactor

def run(self):
"""
Run the executer steps.
.. warning::
If a calculation requires anything different from what this method does,
do not update this method with new complexity! Instead, simply make your own
run sequence and/or class. This pattern is useful only in that it is fairly simple.
By all means, do use ``DirectoryChanger`` and ``ExecuterOptions``
and other utilities.
"""
self.options.resolveDerivedOptions()
runLog.debug(self.options.describe())
if self.options.executablePath and not os.path.exists(
self.options.executablePath
):
raise IOError(
f"Required executable `{self.options.executablePath}` not found for {self}"
)
self._performGeometryTransformations()
inputs, outputs = self._collectInputsAndOutputs()
# must either write input to CWD for analysis and then copy to runDir
# or not list it in inputs (for optimization)
self.writeInput()
with directoryChangers.ForcedCreationDirectoryChanger(
self.options.runDir, filesToMove=inputs, filesToRetrieve=outputs
) as dc:
self.options.workingDir = dc.initial
self._execute()
output = self._readOutput()
if self.options.applyResultsToReactor:
output.apply(self.r)
self._undoGeometryTransformations()
return output

def _execute(self):
runLog.extra(
f"Executing {self.options.executablePath}\n"
f"\tInput: {self.options.inputFile}\n"
f"\tOutput: {self.options.outputFile}\n"
f"\tWorking dir: {self.options.runDir}"
)
return True

def _collectInputsAndOutputs(self):
"""Get total lists of input and output files."""
inputs = [self.options.inputFile] if self.options.inputFile else []
inputs.extend(self.options.extraInputFiles)
outputs = [self.options.outputFile] if self.options.outputFile else []
outputs.extend(self.options.extraOutputFiles)
return inputs, outputs

def writeInput(self):
pass

def _readOutput(self):
raise NotImplementedError()

def _applyOutputToDataModel(self, output):
pass

def _performGeometryTransformations(self):
pass

def _undoGeometryTransformations(self):
pass
Loading

0 comments on commit 32bf13d

Please sign in to comment.