diff --git a/.coveragerc b/.coveragerc index a16bccc42..902482ffc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,11 @@ omit = armi/cli/gridGui.py armi/utils/gridEditor.py armi/utils/tests/test_gridGui.py + venv/ +source = armi + +[coverage:run] +parallel = true [report] omit = diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e69de29bb..000000000 diff --git a/.github/workflows/black.yaml b/.github/workflows/black.yaml index 356cfb0b1..97bd73787 100644 --- a/.github/workflows/black.yaml +++ b/.github/workflows/black.yaml @@ -13,6 +13,6 @@ jobs: with: python-version: 3.9 - name: Install Black - run: pip install 'black==20.8b1' + run: pip install 'black==20.8b1' 'click==8.0.1' - name: Run black --check . run: black --check . diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index df66a48b5..3249d0762 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -22,6 +22,10 @@ jobs: run: sudo apt-get -y install libopenmpi-dev - name: Install Tox and any other packages run: pip install tox - - name: Run Tox - run: tox -e cov,report + - name: Run Coverage Part 1 + if: always() + run: tox -e cov1 || true + - name: Run Coverage Part 2 + if: always() + run: tox -e cov2,report diff --git a/.gitignore b/.gitignore index c1364e061..8f7ee2b18 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ bin/* /bin dist/ dist-*/ -doc/*.png +*.png doc/_build doc/user/tutorials doc/tutorials/anl-afci-177* diff --git a/MANIFEST.in b/MANIFEST.in index 974c1c484..4424857da 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -38,7 +38,6 @@ include armi/nuclearDataIO/tests/library-file-generation/mc2v3-dlayxs.inp include armi/nuclearDataIO/tests/longLivedRipleData.dat include armi/nuclearDataIO/tests/simple_hexz.inp include armi/nuclearDataIO/tests/z036.dat -include armi/settings/tests/old_xml_settings_input.xml include armi/tests/1DslabXSByCompTest.yaml include armi/tests/armiRun-SHUFFLES.txt include armi/tests/armiRun.yaml diff --git a/README.rst b/README.rst index 43a0160aa..e70d10e51 100644 --- a/README.rst +++ b/README.rst @@ -434,6 +434,6 @@ only use third-party Python libraries that have MIT or BSD licenses. .. |Build Status| image:: https://github.com/terrapower/armi/actions/workflows/unittests.yaml/badge.svg?branch=master :target: https://github.com/terrapower/armi/actions/workflows/unittests.yaml -.. |Code Coverage| image:: https://coveralls.io/repos/github/terrapower/armi/badge.svg?branch=master&kill_cache=2 +.. |Code Coverage| image:: https://coveralls.io/repos/github/terrapower/armi/badge.svg?branch=master&kill_cache=3 :target: https://coveralls.io/github/terrapower/armi?branch=master diff --git a/armi/__init__.py b/armi/__init__.py index 34a0da3a6..d1e1e504f 100644 --- a/armi/__init__.py +++ b/armi/__init__.py @@ -101,6 +101,12 @@ _ignoreConfigures = False +def disableFutureConfigures(): + """Exposed function to ensure armi.configure() isn't called more than once""" + global _ignoreConfigures + _ignoreConfigures = True + + def isStableReleaseVersion(version=None): """Determine if the version should be considered a stable release""" version = version or __version__ @@ -322,13 +328,13 @@ def configure(app: Optional[apps.App] = None, permissive=False): _ARMI_CONFIGURE_CONTEXT = "".join(traceback.format_stack()) _app = app + context.APP_NAME = app.name if _liveInterpreter(): runLog.LOG.startLog(name=f"interactive-{app.name}") cli.splash() pm = app.pluginManager - context.APP_NAME = app.name parameters.collectPluginParameters(pm) parameters.applyAllParameters() flags.registerPluginFlags(pm) diff --git a/armi/__main__.py b/armi/__main__.py index 6b1d46310..3389d548e 100644 --- a/armi/__main__.py +++ b/armi/__main__.py @@ -20,17 +20,17 @@ """ import sys -import armi from armi import apps -from armi.cli import ArmiCLI +from armi import configure, isConfigured from armi import context +from armi.cli import ArmiCLI def main(): # Main entry point into ARMI try: - if not armi.isConfigured(): - armi.configure(apps.App()) + if not isConfigured(): + configure(apps.App()) code = ArmiCLI().run() # sys exit interprets None as 0 sys.exit(code) @@ -42,20 +42,20 @@ def main(): # TODO: change to critical after critical no longer throws an exception. print( "[CRIT {:03} ] Unhandled exception in __main__ on {}.".format( - armi.MPI_RANK, armi.MPI_NODENAME + context.MPI_RANK, context.MPI_NODENAME ), file=sys.__stderr__, ) print( "[CRIT {:03} ] Stack trace: {}".format( - armi.MPI_RANK, traceback.format_exc() + context.MPI_RANK, traceback.format_exc() ), file=sys.__stderr__, ) - if armi.MPI_SIZE > 1: + if context.MPI_SIZE > 1: print( "[CRIT {:03} ] killing all MPI tasks from __main__.\n".format( - armi.MPI_RANK + context.MPI_RANK ), file=sys.__stderr__, ) @@ -63,7 +63,7 @@ def main(): # will not allow for @atexit.register or except/finally code to be called so calling here as well context.cleanTempDirs() # .Abort will not allow for @atexit.register or except/finally code to be called - armi.MPI_COMM.Abort(errorcode=-1) + context.MPI_COMM.Abort(errorcode=-1) raise SystemExit(1) diff --git a/armi/apps.py b/armi/apps.py index 10fa63973..61d184894 100644 --- a/armi/apps.py +++ b/armi/apps.py @@ -31,7 +31,7 @@ from typing import Dict, Optional, Tuple, List import collections -from armi import plugins, pluginManager, meta, settings +from armi import context, plugins, pluginManager, meta, settings from armi.reactor import parameters from armi.settings import Setting from armi.settings import fwSettings @@ -96,10 +96,16 @@ def __init__(self): self._paramRenames: Optional[Tuple[Dict[str, str], int]] = None @property - def pluginManager(self) -> pluginManager.ArmiPluginManager: - """ - Return the App's PluginManager. + def version(self) -> str: + """Grab the version of this app (defaults to ARMI version). + + NOTE: This is designed to be over-ridable by Application developers. """ + return meta.__version__ + + @property + def pluginManager(self) -> pluginManager.ArmiPluginManager: + """Return the App's PluginManager.""" return self._pm def getSettings(self) -> Dict[str, Setting]: @@ -221,19 +227,37 @@ def splashText(self): Return a textual splash screen. Specific applications will want to customize this, but by default the ARMI one - is produced. + is produced, with extra data on the App name and version, if available. """ - # Don't move the triple quotes from the beginning of the line - return r""" - --------------------------------------------------- - | _ ____ __ __ ___ | - | / \ | _ \ | \/ | |_ _| | - | / _ \ | |_) | | |\/| | | | | - | / ___ \ | _ < | | | | | | | - | /_/ \_\ |_| \_\ |_| |_| |___| | - | Advanced Reactor Modeling Interface | - --------------------------------------------------- - Version {0:10s} -""".format( + # typical ARMI splash text + splash = r""" ++===================================================+ +| _ ____ __ __ ___ | +| / \ | _ \ | \/ | |_ _| | +| / _ \ | |_) | | |\/| | | | | +| / ___ \ | _ < | | | | | | | +| /_/ \_\ |_| \_\ |_| |_| |___| | +| Advanced Reactor Modeling Interface | +| | +| version {0:10s} | +| |""".format( meta.__version__ ) + + # add the name/version of the current App, if it's not the default + if context.APP_NAME != "armi": + # pylint: disable=import-outside-toplevel # avoid cyclic import + from armi import getApp + + splash += r""" +|---------------------------------------------------| +| {0:>17s} app version {1:10s} |""".format( + context.APP_NAME, getApp().version + ) + + # bottom border of the splash + splash += r""" ++===================================================+ +""" + + return splash diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 8b99edef5..c561f42b5 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -62,12 +62,12 @@ import itertools import os import pathlib -from platform import uname import re -import sys -import time import shutil import subprocess +import sys +import time +from platform import uname from typing import ( Optional, Tuple, @@ -80,12 +80,13 @@ Generator, ) -import numpy import h5py +import numpy -import armi from armi import context +from armi import getApp from armi import interfaces +from armi import meta from armi import runLog from armi import settings from armi.reactor import parameters @@ -294,7 +295,7 @@ def interactDistributeState(self) -> None: DB is created and managed by the master node only but we can still connect to it from workers to enable things like history tracking. """ - if armi.MPI_RANK > 0: + if context.MPI_RANK > 0: # DB may not exist if distribute state is called early. if self._dbPath is not None and os.path.exists(self._dbPath): self._db = Database3(self._dbPath, "r") @@ -327,6 +328,7 @@ def prepRestartRun(self, dbCycle, dbNode): self._db.mergeHistory(inputDB, self.cs["startCycle"], self.cs["startNode"]) self.loadState(dbCycle, dbNode) + # TODO: The use of "yield" here is suspect. def _getLoadDB(self, fileName): """ Return the database to load from in order of preference. @@ -366,7 +368,6 @@ def loadState( If fileName is not specified and neither the database in memory, nor the `cs["reloadDBName"]` have the time step specified. """ - for potentialDatabase in self._getLoadDB(fileName): with potentialDatabase as loadDB: if loadDB.hasTimeStep(cycle, timeNode, statePointName=timeStepName): @@ -581,13 +582,13 @@ def open(self): runLog.info("Opening database file at {}".format(os.path.abspath(filePath))) self.h5db = h5py.File(filePath, self._permission) self.h5db.attrs["successfulCompletion"] = False - self.h5db.attrs["version"] = armi.__version__ + self.h5db.attrs["version"] = meta.__version__ self.h5db.attrs["databaseVersion"] = self.version - self.h5db.attrs["user"] = armi.USER + self.h5db.attrs["user"] = context.USER self.h5db.attrs["python"] = sys.version - self.h5db.attrs["armiLocation"] = os.path.dirname(armi.ROOT) - self.h5db.attrs["startTime"] = armi.START_TIME - self.h5db.attrs["machines"] = numpy.array(armi.MPI_NODENAMES).astype("S") + self.h5db.attrs["armiLocation"] = os.path.dirname(context.ROOT) + self.h5db.attrs["startTime"] = context.START_TIME + self.h5db.attrs["machines"] = numpy.array(context.MPI_NODENAMES).astype("S") # store platform data platform_data = uname() self.h5db.attrs["platform"] = platform_data.system @@ -596,7 +597,7 @@ def open(self): self.h5db.attrs["platformVersion"] = platform_data.version self.h5db.attrs["platformArch"] = platform_data.processor # store app and plugin data - app = armi.getApp() + app = getApp() self.h5db.attrs["appName"] = app.name plugins = app.pluginManager.list_name_plugin() ps = [ @@ -1095,7 +1096,12 @@ def load( parameterCollections.GLOBAL_SERIAL_NUM, layout.serialNum.max() ) root = comps[0][0] - return root # usually reactor object + + # ensure the max assembly number is correct + updateGlobalAssemblyNum(root) + + # usually a reactor object + return root @staticmethod def _assignBlueprintsParams(blueprints, groupedComps): @@ -1298,7 +1304,7 @@ def _addHomogenizedNumberDensityParams(blocks, h5group): def _readParams(h5group, compTypeName, comps, allowMissing=False): g = h5group[compTypeName] - renames = armi.getApp().getParamRenames() + renames = getApp().getParamRenames() pDefs = comps[0].pDefs diff --git a/armi/bookkeeping/db/tests/test_comparedb3.py b/armi/bookkeeping/db/tests/test_comparedb3.py index 0e5dab535..43c6d013f 100644 --- a/armi/bookkeeping/db/tests/test_comparedb3.py +++ b/armi/bookkeeping/db/tests/test_comparedb3.py @@ -30,7 +30,7 @@ def setUp(self): def tearDown(self): self.td.__exit__(None, None, None) - def test_outputWriter(self) -> None: + def test_outputWriter(self): fileName = "test_outputWriter.txt" with OutputWriter(fileName) as out: out.writeln("Rubber Baby Buggy Bumpers") @@ -38,7 +38,7 @@ def test_outputWriter(self) -> None: txt = open(fileName, "r").read() self.assertIn("Rubber", txt) - def test_diffResultsBasic(self) -> None: + def test_diffResultsBasic(self): # init an instance of the class dr = DiffResults(0.01) self.assertEqual(len(dr._columns), 0) diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py index 17d1a02c2..e73fc75c7 100644 --- a/armi/bookkeeping/db/tests/test_database3.py +++ b/armi/bookkeeping/db/tests/test_database3.py @@ -17,8 +17,8 @@ import subprocess import unittest -import numpy import h5py +import numpy from armi.bookkeeping.db import database3 from armi.reactor import grids @@ -34,9 +34,8 @@ def setUp(self): self.td = TemporaryDirectoryChanger() self.td.__enter__() self.o, self.r = test_reactors.loadTestReactor(TEST_ROOT) - cs = self.o.cs - self.dbi = database3.DatabaseInterface(self.r, cs) + self.dbi = database3.DatabaseInterface(self.r, self.o.cs) self.dbi.initDB(fName=self._testMethodName + ".h5") self.db: db.Database3 = self.dbi.database self.stateRetainer = self.r.retainState().__enter__() @@ -193,7 +192,7 @@ def test_computeParents(self): database3.Layout.computeAncestors(serialNums, numChildren, 3), expected_3 ) - def test_load(self) -> None: + def test_load(self): self.makeShuffleHistory() with self.assertRaises(KeyError): _r = self.db.load(0, 0) @@ -203,6 +202,10 @@ def test_load(self) -> None: del self.db.h5db["c00n00/Reactor/missingParam"] _r = self.db.load(0, 0, allowMissing=False) + # we shouldn't be able to set the fileName if a file is open + with self.assertRaises(RuntimeError): + self.db.fileName = "whatever.h5" + def test_history(self): self.makeShuffleHistory() @@ -244,7 +247,7 @@ def test_auxData(self): with self.assertRaises(KeyError): self.db.genAuxiliaryData((-1, -1)) - # TODO: This definitely needs some work + # TODO: This should be expanded. def test_replaceNones(self): """Super basic test that we handle Nones correctly in database read/writes""" data3 = numpy.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) @@ -352,6 +355,13 @@ def test_splitDatabase(self): self.assertIn(meta_key, newDb.attrs) self.assertTrue(newDb.attrs[meta_key] is not None) + # test an edge case - no DB to split + with self.assertRaises(ValueError): + self.db.h5db = None + self.db.splitDatabase( + [(c, n) for c in (1, 2) for n in range(3)], "-all-iterations" + ) + def test_grabLocalCommitHash(self): """test of static method to grab a local commit hash with ARMI version""" # 1. test outside a Git repo @@ -391,8 +401,51 @@ def test_grabLocalCommitHash(self): localHash = database3.Database3.grabLocalCommitHash() self.assertEqual(localHash, "thanks") + def test_fileName(self): + # test the file name getter + self.assertEqual(str(self.db.fileName), "test_fileName.h5") + + # test the file name setter + self.db.close() + self.db.fileName = "thing.h5" + self.assertEqual(str(self.db.fileName), "thing.h5") + + def test_readInputsFromDB(self): + inputs = self.db.readInputsFromDB() + self.assertEqual(len(inputs), 3) + + self.assertGreater(len(inputs[0]), 100) + self.assertIn("metadata:", inputs[0]) + self.assertIn("settings:", inputs[0]) + + self.assertEqual(len(inputs[1]), 0) + + self.assertGreater(len(inputs[2]), 100) + self.assertIn("custom isotopics:", inputs[2]) + self.assertIn("blocks:", inputs[2]) + + def test_deleting(self): + self.assertEqual(type(self.db), database3.Database3) + del self.db + self.assertFalse(hasattr(self, "db")) + self.db = self.dbi.database + + def test_open(self): + with self.assertRaises(ValueError): + self.db.open() + + def test_loadCS(self): + cs = self.db.loadCS() + self.assertEqual(cs["numProcessors"], 1) + self.assertEqual(cs["nCycles"], 3) + + def test_loadBlueprints(self): + bp = self.db.loadBlueprints() + self.assertIsNone(bp.nuclideFlags) + self.assertEqual(len(bp.assemblies), 0) -class Test_LocationPacking(unittest.TestCase): + +class TestLocationPacking(unittest.TestCase): r"""Tests for database location""" def test_locationPacking(self): @@ -416,7 +469,7 @@ def test_locationPacking(self): self.assertEqual(unpackedData[1], (4.0, 5.0, 6.0)) self.assertEqual(unpackedData[2], [(7, 8, 9), (10, 11, 12)]) - def test_locationPackingOlderVerions(self): + def test_locationPackingOlderVersions(self): # pylint: disable=protected-access for version in [1, 2]: loc1 = grids.IndexLocation(1, 2, 3, None) @@ -439,7 +492,30 @@ def test_locationPackingOlderVerions(self): self.assertEqual(unpackedData[2][0].tolist(), [7, 8, 9]) self.assertEqual(unpackedData[2][1].tolist(), [10, 11, 12]) + def test_locationPackingOldVersion(self): + # pylint: disable=protected-access + version = 3 + + loc1 = grids.IndexLocation(1, 2, 3, None) + loc2 = grids.CoordinateLocation(4.0, 5.0, 6.0, None) + loc3 = grids.MultiIndexLocation(None) + loc3.append(grids.IndexLocation(7, 8, 9, None)) + loc3.append(grids.IndexLocation(10, 11, 12, None)) + + locs = [loc1, loc2, loc3] + tp, data = database3._packLocations(locs, minorVersion=version) + + self.assertEqual(tp[0], "I") + self.assertEqual(tp[1], "C") + self.assertEqual(tp[2], "M:2") + + unpackedData = database3._unpackLocations(tp, data, minorVersion=version) + + self.assertEqual(unpackedData[0], (1, 2, 3)) + self.assertEqual(unpackedData[1], (4.0, 5.0, 6.0)) + self.assertEqual(unpackedData[2][0], (7, 8, 9)) + self.assertEqual(unpackedData[2][1], (10, 11, 12)) + if __name__ == "__main__": - # import sys;sys.argv = ["", "TestDatabase3.test_splitDatabase"] unittest.main() diff --git a/armi/bookkeeping/memoryProfiler.py b/armi/bookkeeping/memoryProfiler.py index 984b0a59e..1c39d2ce3 100644 --- a/armi/bookkeeping/memoryProfiler.py +++ b/armi/bookkeeping/memoryProfiler.py @@ -47,7 +47,7 @@ import tabulate from typing import Optional -import armi +from armi import context from armi import interfaces from armi import mpiActions from armi import runLog @@ -161,10 +161,8 @@ def _reactorAssemblyTrackingBreakdown(self): ) runLog.important("SFP has {:4d} assemblies".format(len(self.r.core.sfp))) - def _checkForDuplicateObjectsOnArmiModel(self, attrName, refObject): - """ - Scans thorugh ARMI model for duplicate objects - """ + def checkForDuplicateObjectsOnArmiModel(self, attrName, refObject): + """Scans thorugh ARMI model for duplicate objects""" if self.r is None: return uniqueIds = set() @@ -330,9 +328,7 @@ def getSpecificReferrers(klass, ancestorKlass): @staticmethod def getReferrers(obj): - """ - Print referrers in a useful way (as opposed to gigabytes of text - """ + """Print referrers in a useful way (as opposed to gigabytes of text""" runLog.info("Printing first 100 character of first 100 referrers") for ref in gc.get_referrers(obj)[:100]: print("ref for {}: {}".format(obj, repr(ref)[:100])) @@ -521,10 +517,10 @@ def invokeHook(self): class SystemAndProcessMemoryUsage: def __init__(self): - self.nodeName = armi.MPI_NODENAME - # no psutil, no memory diagnostics. TODO: Ideally, we could just cut - # MemoryProfiler out entirely, but it is referred to directly by the standard - # operator and reports, so easier said than done. + self.nodeName = context.MPI_NODENAME + # no psutil, no memory diagnostics. + # TODO: Ideally, we could cut MemoryProfiler entirely, but it is referred to + # directly by the standard operator and reports, so easier said than done. self.percentNodeRamUsed: Optional[float] = None self.processMemoryInMB: Optional[float] = None if _havePsutil: @@ -636,10 +632,7 @@ def _getFunctionObject(): def _walkReferrers(o, maxLevel=0, level=0, memo=None, whitelist=None): - """ - Walk down the tree of objects that refer to the passed object, printing diagnostics along the - way. - """ + """Walk the tree of objects that refer to the passed object, printing diagnostics.""" if maxLevel and level > maxLevel: return if level == 0: diff --git a/armi/bookkeeping/newReports.py b/armi/bookkeeping/newReports.py index 59a62db72..83560c435 100644 --- a/armi/bookkeeping/newReports.py +++ b/armi/bookkeeping/newReports.py @@ -12,21 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from abc import ABC, abstractmethod from enum import Enum from enum import auto -import collections -import shutil -import os -import copy +from operator import itemgetter from typing import Union, Dict -from abc import ABC, abstractmethod import base64 -from operator import itemgetter +import collections +import copy +import os +import shutil -import matplotlib.pyplot as plt import htmltree +import matplotlib.pyplot as plt -import armi.context +from armi import context from armi import runLog @@ -56,7 +56,7 @@ def writeReports(self): ) header.C.append( htmltree.H1( - "{} Report".format(armi.context.APP_NAME.capitalize()), + "{} Report".format(context.APP_NAME.capitalize()), _class="heading", id="titleFont", ) diff --git a/armi/bookkeeping/report/html.py b/armi/bookkeeping/report/html.py index bb4757853..70f449418 100644 --- a/armi/bookkeeping/report/html.py +++ b/armi/bookkeeping/report/html.py @@ -13,14 +13,14 @@ # limitations under the License. """ - +HTML-formatted reports """ -import os -import html -import datetime import base64 +import datetime +import html +import os -import armi +from armi import context from armi import settings @@ -228,7 +228,9 @@ def writeStandardReportTemplate(f, report): f, attrs={ "src": encode64( - os.path.join(armi.RES, "images", "armiicon.ico") + os.path.join( + context.RES, "images", "armiicon.ico" + ) ) }, ): @@ -249,7 +251,7 @@ def writeStandardReportTemplate(f, report): f, attrs={"class": "navbar-text navbar-version pull-left"} ): with B(f): - f.write(armi.USER) + f.write(context.USER) with Span( f, attrs={"class": "navbar-text navbar-version pull-left"} @@ -285,12 +287,3 @@ def writeStandardReportTemplate(f, report): f.write("ARMI docs") with P(f): f.write("Automatically generated by ARMI") - - # with Script(f, attrs={"type": r"text/javascript"}): - # load jquery js here (through CDN?) - # - # pass - - # with Script(f, attrs={"type": r"text/javascript"}): - # load boostrap.min.js here (prefer through CDN) - # pass diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py index 0a22755e9..01cd6a03e 100644 --- a/armi/bookkeeping/report/reportingUtils.py +++ b/armi/bookkeeping/report/reportingUtils.py @@ -16,31 +16,31 @@ A collection of miscellaneous functions used by ReportInterface to generate various reports """ -import re -import os import collections +import os import pathlib -import textwrap +import re import sys -import time import tabulate +import textwrap +import time from copy import copy import numpy -import armi +from armi import context +from armi import interfaces from armi import runLog +from armi.bookkeeping import report +from armi.operators import RunTypes +from armi.reactor.components import ComponentType +from armi.reactor.flags import Flags from armi.utils import getFileSHA1Hash from armi.utils import iterables -from armi.utils import units -from armi.utils import textProcessors from armi.utils import plotting +from armi.utils import textProcessors +from armi.utils import units from armi.utils.mathematics import findClosest -from armi import interfaces -from armi.bookkeeping import report -from armi.reactor.flags import Flags -from armi.reactor.components import ComponentType -from armi.operators import RunTypes # Set to prevent the image and text from being too small to read. @@ -74,13 +74,13 @@ def _writeCaseInformation(o, cs): Operator_TypeOfRun, "{} - {}".format(cs["runType"], o.__class__.__name__), ), - (Operator_CurrentUser, armi.USER), - (Operator_ArmiCodebase, armi.ROOT), + (Operator_CurrentUser, context.USER), + (Operator_ArmiCodebase, context.ROOT), (Operator_WorkingDirectory, os.getcwd()), (Operator_PythonInterperter, sys.version), (Operator_MasterMachine, os.environ.get("COMPUTERNAME", "?")), - (Operator_NumProcessors, armi.MPI_SIZE), - (Operator_Date, armi.START_TIME), + (Operator_NumProcessors, context.MPI_SIZE), + (Operator_Date, context.START_TIME), ] runLog.header("=========== Case Information ===========") @@ -155,8 +155,8 @@ def _writeInputFileInformation(cs): def _writeMachineInformation(): """Create a table that contains basic machine and rank information.""" - if armi.MPI_SIZE > 1: - processorNames = armi.MPI_NODENAMES + if context.MPI_SIZE > 1: + processorNames = context.MPI_NODENAMES uniqueNames = set(processorNames) nodeMappingData = [] for uniqueName in uniqueNames: @@ -196,7 +196,7 @@ def _writeReactorCycleInformation(o, cs): runLog.header("=========== Reactor Cycle Information ===========") runLog.info(tabulate.tabulate(operatingData, tablefmt="armi")) - if armi.MPI_RANK > 0: + if context.MPI_RANK > 0: return # prevent the worker nodes from printing the same thing _writeCaseInformation(o, cs) @@ -403,7 +403,6 @@ def setNeutronBalancesReport(core): core : armi.reactor.reactors.Core """ - if not core.getFirstBlock().p.rateCap: runLog.warning( "No rate information (rateCap, rateAbs, etc.) available " @@ -450,7 +449,7 @@ def setNeutronBalancesReport(core): report.NEUT_LOSS, ) - runLog.info(report.ALL[report.NEUT_PROD]) # TODO: print in "lite" + runLog.info(report.ALL[report.NEUT_PROD]) runLog.info(report.ALL[report.NEUT_LOSS]) diff --git a/armi/bookkeeping/report/tests/test_report.py b/armi/bookkeeping/report/tests/test_report.py index 83f44f391..c6dbfdfea 100644 --- a/armi/bookkeeping/report/tests/test_report.py +++ b/armi/bookkeeping/report/tests/test_report.py @@ -60,19 +60,6 @@ def test_setData(self): self.assertEqual(filled_instance["banana_2"], ["sundae", "vanilla"]) self.assertEqual(filled_instance["banana_3"], ["sundae", "chocolate"]) - def test_printReports(self): - """testing printReports method""" - repInt = reportInterface.ReportInterface(None, None) - rep = repInt.printReports() - - self.assertIn("REPORTS BEGIN", rep) - self.assertIn("REPORTS END", rep) - - def test_writeReports(self): - """Test writing html reports.""" - repInt = reportInterface.ReportInterface(None, None) - repInt.writeReports() - def test_reactorSpecificReporting(self): """Test a number of reporting utils that require reactor/core information""" o, r = loadTestReactor() @@ -94,6 +81,17 @@ def test_reactorSpecificReporting(self): self.assertIn("No rate information", mock._outputStream) mock._outputStream = "" + r.core.getFirstBlock().p.rateCap = 1.0 + r.core.getFirstBlock().p.rateProdFis = 1.02 + r.core.getFirstBlock().p.rateFis = 1.01 + r.core.getFirstBlock().p.rateAbs = 1.0 + setNeutronBalancesReport(r.core) + self.assertIn("Fission", mock._outputStream) + self.assertIn("Capture", mock._outputStream) + self.assertIn("Absorption", mock._outputStream) + self.assertIn("Leakage", mock._outputStream) + mock._outputStream = "" + summarizePinDesign(r.core) self.assertIn("Assembly Design Summary", mock._outputStream) self.assertIn("Design & component information", mock._outputStream) @@ -127,5 +125,71 @@ def test_reactorSpecificReporting(self): self.assertTrue(len(mock._outputStream) == 0) +class TestReportInterface(unittest.TestCase): + def test_printReports(self): + """testing printReports method""" + repInt = reportInterface.ReportInterface(None, None) + rep = repInt.printReports() + + self.assertIn("REPORTS BEGIN", rep) + self.assertIn("REPORTS END", rep) + + def test_writeReports(self): + """Test writing html reports.""" + repInt = reportInterface.ReportInterface(None, None) + repInt.writeReports() + + def test_distributableReportInt(self): + repInt = reportInterface.ReportInterface(None, None) + self.assertEqual(repInt.distributable(), 4) + + def test_interactBOLReportInt(self): + o, r = loadTestReactor() + repInt = reportInterface.ReportInterface(r, o.cs) + + with mockRunLogs.BufferLog() as mock: + repInt.interactBOL() + self.assertIn("Writing assem layout", mock._outputStream) + self.assertIn("BOL Assembly", mock._outputStream) + self.assertIn("wetMass", mock._outputStream) + self.assertIn("moveable plenum", mock._outputStream) + + def test_interactEveryNode(self): + o, r = loadTestReactor() + repInt = reportInterface.ReportInterface(r, o.cs) + + with mockRunLogs.BufferLog() as mock: + repInt.interactEveryNode(0, 0) + self.assertIn("Cycle 0", mock._outputStream) + self.assertIn("node 0", mock._outputStream) + self.assertIn("keff=", mock._outputStream) + + def test_interactBOC(self): + o, r = loadTestReactor() + repInt = reportInterface.ReportInterface(r, o.cs) + + self.assertEqual(repInt.fuelCycleSummary["bocFissile"], 0.0) + repInt.interactBOC(1) + self.assertEqual(repInt.fuelCycleSummary["bocFissile"], 0.0) + + def test_interactEOC(self): + o, r = loadTestReactor() + repInt = reportInterface.ReportInterface(r, o.cs) + + with mockRunLogs.BufferLog() as mock: + repInt.interactEOC(0) + self.assertIn("Cycle 0", mock._outputStream) + self.assertIn("TIMER REPORTS", mock._outputStream) + + def test_interactEOL(self): + o, r = loadTestReactor() + repInt = reportInterface.ReportInterface(r, o.cs) + + with mockRunLogs.BufferLog() as mock: + repInt.interactEOL() + self.assertIn("Comprehensive Core Report", mock._outputStream) + self.assertIn("Assembly Area Fractions", mock._outputStream) + + if __name__ == "__main__": unittest.main() diff --git a/armi/bookkeeping/tests/test_databaseInterface.py b/armi/bookkeeping/tests/test_databaseInterface.py index 31791e04f..3ad7649ab 100644 --- a/armi/bookkeeping/tests/test_databaseInterface.py +++ b/armi/bookkeeping/tests/test_databaseInterface.py @@ -14,27 +14,26 @@ r""" Tests of the Database Interface """ # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access - import os -import unittest import types +import unittest import h5py import numpy from numpy.testing import assert_allclose, assert_equal -from armi.reactor.flags import Flags +from armi import __version__ as version from armi import interfaces -from armi.bookkeeping.db.database3 import DatabaseInterface, Database3 +from armi import runLog from armi import settings -from armi.tests import TEST_ROOT -from armi import __version__ as version +from armi.bookkeeping.db.database3 import DatabaseInterface, Database3 from armi.cases import case -from armi.utils import directoryChangers -from armi import runLog -from armi.reactor.tests import test_reactors from armi.reactor import grids +from armi.reactor.flags import Flags +from armi.reactor.tests import test_reactors from armi.settings.fwSettings.databaseSettings import CONF_FORCE_DB_PARAMS +from armi.tests import TEST_ROOT +from armi.utils import directoryChangers def getSimpleDBOperator(cs): @@ -83,6 +82,39 @@ def interactEveryNode(self, cycle, node): self.action(cycle, node) +class TestDatabaseInterface(unittest.TestCase): + r"""Tests for the DatabaseInterface class""" + + def setUp(self): + self.td = directoryChangers.TemporaryDirectoryChanger() + self.td.__enter__() + self.o, self.r = test_reactors.loadTestReactor(TEST_ROOT) + + self.dbi = DatabaseInterface(self.r, self.o.cs) + self.dbi.initDB(fName=self._testMethodName + ".h5") + self.db: db.Database3 = self.dbi.database + self.stateRetainer = self.r.retainState().__enter__() + + def tearDown(self): + self.db.close() + self.stateRetainer.__exit__() + self.td.__exit__(None, None, None) + + def test_interactBOL(self): + self.assertTrue(self.dbi._db is not None) + self.dbi.interactBOL() + + self.dbi._db = None + self.assertTrue(self.dbi._db is None) + self.dbi.interactBOL() + self.assertTrue(self.dbi._db is not None) + + def test_distributable(self): + self.assertEqual(self.dbi.distributable(), 4) + self.dbi.interactDistributeState() + self.assertEqual(self.dbi.distributable(), 4) + + class TestDatabaseWriter(unittest.TestCase): def setUp(self): self.td = directoryChangers.TemporaryDirectoryChanger() @@ -120,7 +152,7 @@ def goodMethod(cycle, node): # pylint: disable=unused-argument self.assertIn("blueprints", h5["inputs"]) self.assertIn("baseBu", h5["c01n02/HexBlock"]) - def test_metaData_endFail(self): + def test_metaDataEndFail(self): def failMethod(cycle, node): # pylint: disable=unused-argument if cycle == 1 and node == 1: raise Exception("forcing failure") @@ -247,6 +279,60 @@ def tearDownClass(cls): del cls.r cls.r = None + def test_growToFullCore(self): + with Database3(self.dbName, "r") as db: + r = db.load(0, 0, allowMissing=True) + + r.core.growToFullCore(None) + + self.assertEqual(r.core.numRings, 9) + self.assertEqual(r.p.cycle, 0) + self.assertEqual(len(r.core.assembliesByName), 217) + self.assertEqual(len(r.core.circularRingList), 0) + self.assertEqual(len(r.core.blocksByName), 1085) + + def test_growToFullCoreWithCS(self): + with Database3(self.dbName, "r") as db: + r = db.load(0, 0, allowMissing=True) + + r.core.growToFullCore(self.cs) + + self.assertEqual(r.core.numRings, 9) + self.assertEqual(r.p.cycle, 0) + self.assertEqual(len(r.core.assembliesByName), 217) + self.assertEqual(len(r.core.circularRingList), 0) + self.assertEqual(len(r.core.blocksByName), 1085) + + def test_growToFullCoreFromFactory(self): + from armi.bookkeeping.db import databaseFactory + + db = databaseFactory(self.dbName, "r") + with db: + r = db.load(0, 0, allowMissing=True) + + r.core.growToFullCore(None) + + self.assertEqual(r.core.numRings, 9) + self.assertEqual(r.p.cycle, 0) + self.assertEqual(len(r.core.assembliesByName), 217) + self.assertEqual(len(r.core.circularRingList), 0) + self.assertEqual(len(r.core.blocksByName), 1085) + + def test_growToFullCoreFromFactoryWithCS(self): + from armi.bookkeeping.db import databaseFactory + + db = databaseFactory(self.dbName, "r") + with db: + r = db.load(0, 0, allowMissing=True) + + r.core.growToFullCore(self.cs) + + self.assertEqual(r.core.numRings, 9) + self.assertEqual(r.p.cycle, 0) + self.assertEqual(len(r.core.assembliesByName), 217) + self.assertEqual(len(r.core.circularRingList), 0) + self.assertEqual(len(r.core.blocksByName), 1085) + def test_readWritten(self): with Database3(self.dbName, "r") as db: r2 = db.load(0, 0, self.cs, self.bp) @@ -407,5 +493,4 @@ def test_standardRestart(self): if __name__ == "__main__": - # import sys;sys.argv = ["", "TestStandardFollowOn.test_standardRestart"] unittest.main() diff --git a/armi/bookkeeping/tests/test_report.py b/armi/bookkeeping/tests/test_report.py index 57213e45f..c5b26ae7b 100644 --- a/armi/bookkeeping/tests/test_report.py +++ b/armi/bookkeeping/tests/test_report.py @@ -13,26 +13,27 @@ # limitations under the License. """Test reports.""" -import os import collections +import os import unittest import htmltree -import armi -from armi.tests import TEST_ROOT -from armi.reactor.tests import test_reactors +from armi import getPluginManagerOrFail from armi.bookkeeping import newReports -from armi.utils import directoryChangers +from armi.bookkeeping.report import data from armi.physics.neutronics.reports import neutronicsPlotting -import armi.bookkeeping.newReports +from armi.reactor.tests import test_reactors +from armi.tests import TEST_ROOT +from armi.tests import mockRunLogs +from armi.utils import directoryChangers class TestReportContentCreation(unittest.TestCase): def setUp(self): self.o, self.r = test_reactors.loadTestReactor(TEST_ROOT) - def testTimeSeries(self): + def test_TimeSeries(self): """Test execution of TimeSeries object.""" with directoryChangers.TemporaryDirectoryChanger(): times = [0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3] @@ -55,8 +56,7 @@ def testTimeSeries(self): series.plot() self.assertTrue(os.path.exists("ReactorName.plotexample.png")) - def testTableCreation(self): - + def test_TableCreation(self): header = ["item", "value"] table = newReports.Table("Assembly Table", "table of assemblies", header) @@ -66,11 +66,11 @@ def testTableCreation(self): result = table.render(0) self.assertTrue(isinstance(result, htmltree.HtmlElement)) - def testReportContents(self): + def test_ReportContents(self): with directoryChangers.TemporaryDirectoryChanger(): reportTest = newReports.ReportContent("Test") - armi.getPluginManagerOrFail().hook.getReportContents( + getPluginManagerOrFail().hook.getReportContents( r=self.r, cs=self.o.cs, report=reportTest, @@ -85,8 +85,7 @@ def testReportContents(self): isinstance(reportTest.tableOfContents(), htmltree.HtmlElement) ) - def testNeutronicsPlotFunctions(self): - + def test_neutronicsPlotFunctions(self): reportTest = newReports.ReportContent("Test") neutronicsPlotting(self.r, reportTest, self.o.cs) @@ -95,7 +94,7 @@ def testNeutronicsPlotFunctions(self): isinstance(reportTest["Neutronics"]["Keff-Plot"], newReports.TimeSeries) ) - def testWriteReports(self): + def test_writeReports(self): with directoryChangers.TemporaryDirectoryChanger(): reportTest = newReports.ReportContent("Test") table = newReports.Table("Example") @@ -113,6 +112,32 @@ def testWriteReports(self): times = times + 1 self.assertTrue(times == 2) + def test_reportBasics(self): + env = data.Report("Environment", "ARMI Env Info") + + s = str(env) + self.assertIn("Environment", s) + self.assertIn("Env Info", s) + + gro = env._groupRenderOrder + self.assertEqual(len(gro), 0) + + self.assertIsNone(env["Environment"]) + + def test_reportLogs(self): + env = data.Report("Environment", "ARMI Env Info") + + with mockRunLogs.BufferLog() as mock: + self.assertEqual("", mock._outputStream) + _ = env["badStuff"] + self.assertIn("Cannot locate group", mock._outputStream) + + mock._outputStream = "" + self.assertEqual("", mock._outputStream) + env.writeHTML() + self.assertIn("Writing HTML document", mock._outputStream) + self.assertIn("[info] HTML document", mock._outputStream) + if __name__ == "__main__": unittest.main() diff --git a/armi/cases/case.py b/armi/cases/case.py index ca9a130f7..e372dba82 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -39,22 +39,22 @@ import six import coverage -import armi from armi import context -from armi import settings +from armi import getPluginManager +from armi import interfaces from armi import operators from armi import runLog -from armi import interfaces +from armi import settings +from armi.bookkeeping.db import compareDatabases from armi.cli import reportsEntryPoint +from armi.nucDirectory import nuclideBases from armi.reactor import blueprints -from armi.reactor import systemLayoutInput from armi.reactor import reactors -from armi.bookkeeping.db import compareDatabases +from armi.reactor import systemLayoutInput from armi.utils import pathTools +from armi.utils import textProcessors from armi.utils.directoryChangers import DirectoryChanger from armi.utils.directoryChangers import ForcedCreationDirectoryChanger -from armi.utils import textProcessors -from armi.nucDirectory import nuclideBases # change from default .coverage to help with Windows dotfile issues. # Must correspond with data_file entry in `coveragerc`!! @@ -184,7 +184,7 @@ def dependencies(self): """ dependencies = set() if self._caseSuite is not None: - pm = armi.getPluginManager() + pm = getPluginManager() if pm is not None: for pluginDependencies in pm.hook.defineCaseDependencies( case=self, suite=self._caseSuite @@ -226,6 +226,10 @@ def getPotentialParentFromSettingValue(self, settingValue, filePattern): """ Get a parent case based on a setting value and a pattern. + This is a convenient way for a plugin to express a dependency. It uses the + ``match.groupdict`` functionality to pull the directory and case name out of a + specific setting value an regular expression. + Parameters ---------- settingValue : str @@ -236,10 +240,6 @@ def getPotentialParentFromSettingValue(self, settingValue, filePattern): If the ``settingValue`` matches the passed pattern, this function will attempt to extract the ``dirName`` and ``title`` groups to find the dependency. - - This is a convenient way for a plugin to express a dependency. It uses the - ``match.groupdict`` functionality to pull the directory and case name out of a - specific setting value an regular expression. """ m = re.match(filePattern, settingValue, re.IGNORECASE) deps = self._getPotentialDependencies(**m.groupdict()) if m else set() @@ -343,7 +343,7 @@ def run(self): # can be configured based on the user settings for the rest of the # run. runLog.LOG.startLog(self.cs.caseTitle) - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: runLog.setVerbosity(self.cs["verbosity"]) else: runLog.setVerbosity(self.cs["branchVerbosity"]) @@ -351,7 +351,7 @@ def run(self): cov = None if self.cs["coverage"]: cov = coverage.Coverage( - config_file=os.path.join(armi.RES, "coveragerc"), debug=["dataio"] + config_file=os.path.join(context.RES, "coveragerc"), debug=["dataio"] ) if context.MPI_SIZE > 1: # interestingly, you cannot set the parallel flag in the constructor @@ -370,7 +370,7 @@ def run(self): o = self.initializeOperator() with o: - if self.cs["trace"] and armi.MPI_RANK == 0: + if self.cs["trace"] and context.MPI_RANK == 0: # only trace master node. tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix], trace=1) tracer.runctx("o.operate()", globals(), locals()) @@ -379,15 +379,15 @@ def run(self): if profiler is not None: profiler.disable() - profiler.dump_stats("profiler.{:0>3}.stats".format(armi.MPI_RANK)) + profiler.dump_stats("profiler.{:0>3}.stats".format(context.MPI_RANK)) statsStream = six.StringIO() summary = pstats.Stats(profiler, stream=statsStream).sort_stats( "cumulative" ) summary.print_stats() - if armi.MPI_SIZE > 0: - allStats = armi.MPI_COMM.gather(statsStream.getvalue(), root=0) - if armi.MPI_RANK == 0: + if context.MPI_SIZE > 0: + allStats = context.MPI_COMM.gather(statsStream.getvalue(), root=0) + if context.MPI_RANK == 0: for rank, statsString in enumerate(allStats): # using print statements because the logger has been turned off print("=" * 100) @@ -405,14 +405,15 @@ def run(self): cov.stop() cov.save() - if armi.MPI_SIZE > 1: - armi.MPI_COMM.barrier() # force waiting for everyone to finish + if context.MPI_SIZE > 1: + context.MPI_COMM.barrier() # force waiting for everyone to finish - if armi.MPI_RANK == 0 and armi.MPI_SIZE > 1: + if context.MPI_RANK == 0 and context.MPI_SIZE > 1: # combine all the parallel coverage data files into one and make # the XML and HTML reports for the whole run. combinedCoverage = coverage.Coverage( - config_file=os.path.join(armi.RES, "coveragerc"), debug=["dataio"] + config_file=os.path.join(context.RES, "coveragerc"), + debug=["dataio"], ) combinedCoverage.config.parallel = True # combine does delete the files it merges @@ -461,7 +462,7 @@ def checkInputs(self): operatorClass = operators.getOperatorClassFromSettings(self.cs) inspector = operatorClass.inspector(self.cs) inspectorIssues = [query for query in inspector.queries if query] - if armi.CURRENT_MODE == armi.Mode.INTERACTIVE: + if context.CURRENT_MODE == context.Mode.INTERACTIVE: # if interactive, ask user to deal with settings issues inspector.run() else: @@ -480,7 +481,7 @@ def checkInputs(self): ) ) - if queryData and armi.MPI_RANK == 0: + if queryData and context.MPI_RANK == 0: runLog.header("=========== Settings Input Queries ===========") runLog.info( tabulate.tabulate( @@ -518,9 +519,8 @@ def buildCommand(self, python="python"): command += "{} -u ".format(python) if not __debug__: command += " -O " - command += ' -m {} run "{}.yaml"'.format( - armi.context.APP_NAME, self.cs.caseTitle - ) + + command += ' -m {} run "{}.yaml"'.format(context.APP_NAME, self.cs.caseTitle) return command diff --git a/armi/cases/inputModifiers/tests/test_inputModifiers.py b/armi/cases/inputModifiers/tests/test_inputModifiers.py index 9b3545c47..d95f1b882 100644 --- a/armi/cases/inputModifiers/tests/test_inputModifiers.py +++ b/armi/cases/inputModifiers/tests/test_inputModifiers.py @@ -123,7 +123,7 @@ def setUpClass(cls): bp._prepConstruction(cs) cls.baseCase = cases.Case(cs=cs, bp=bp, geom=geom) - def test_SmearDensityFail(self): + def test_smearDensityFail(self): builder = suiteBuilder.FullFactorialSuiteBuilder(self.baseCase) builder.addDegreeOfFreedom( @@ -137,7 +137,7 @@ def test_SmearDensityFail(self): with self.assertRaisesRegex(RuntimeError, "before .*SmearDensityModifier"): builder.buildSuite() - def test_example(self): + def test_settingsModifier(self): builder = suiteBuilder.SeparateEffectsSuiteBuilder(self.baseCase) builder.addDegreeOfFreedom( inputModifiers.SettingsModifier("fpModel", v) @@ -164,11 +164,9 @@ def test_example(self): self.assertTrue(os.path.exists("case-suite")) - def test_BluePrintBlockModifier(self): + def test_bluePrintBlockModifier(self): """test BluePrintBlockModifier with build suite naming function argument""" - case_nbr = 1 - builder = suiteBuilder.FullFactorialSuiteBuilder(self.baseCase) builder.addDegreeOfFreedom( @@ -249,5 +247,4 @@ def test_fullCoreConversion(self): if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] unittest.main() diff --git a/armi/cases/tests/test_suiteBuilder.py b/armi/cases/tests/test_suiteBuilder.py index da07d05c1..f2f3bf9a9 100644 --- a/armi/cases/tests/test_suiteBuilder.py +++ b/armi/cases/tests/test_suiteBuilder.py @@ -50,7 +50,7 @@ def __call__(self, cs, bp, geom): class TestLatinHyperCubeSuiteBuilder(unittest.TestCase): """Class to test LatinHyperCubeSuiteBuilder.""" - def testInitialize(self): + def test_initialize(self): builder = LatinHyperCubeSuiteBuilder(case, size=20) assert builder.modifierSets == [] diff --git a/armi/cli/__init__.py b/armi/cli/__init__.py index 9686f9e44..ee195875f 100644 --- a/armi/cli/__init__.py +++ b/armi/cli/__init__.py @@ -42,12 +42,13 @@ # classes import argparse -import textwrap import re import sys +import textwrap from typing import Optional -import armi +from armi import context +from armi import meta from armi import plugins from armi import runLog @@ -110,8 +111,10 @@ class ArmiCLI: """ def __init__(self): + from armi import getPluginManager # pylint: disable=import-outside-toplevel + self._entryPoints = dict() - for pluginEntryPoints in armi.getPluginManager().hook.defineEntryPoints(): + for pluginEntryPoints in getPluginManager().hook.defineEntryPoints(): for entryPoint in pluginEntryPoints: if entryPoint.name in self._entryPoints: raise KeyError( @@ -124,7 +127,7 @@ def __init__(self): self._entryPoints[entryPoint.name] = entryPoint parser = ArmiParser( - prog=armi.context.APP_NAME, + prog=context.APP_NAME, description=self.__doc__, usage="%(prog)s [-h] [-l | command [args]]", ) @@ -132,7 +135,7 @@ def __init__(self): group = parser.add_mutually_exclusive_group() group.add_argument( - "-v", "--version", action="version", version="%(prog)s " + armi.__version__ + "-v", "--version", action="store_true", help="display the version" ) group.add_argument( @@ -143,6 +146,17 @@ def __init__(self): self.parser = parser + def showVersion(self): + """Print the App name and version on the command line""" + from armi import getApp # pylint: disable=import-outside-toplevel + + prog = context.APP_NAME + app = getApp() + if app is None or prog == "armi": + print("{0} {1}".format(prog, meta.__version__)) + else: + print("{0} {1}".format(prog, app.version)) + def listCommands(self): """List commands with a short description.""" splash() @@ -176,12 +190,12 @@ def run(self) -> Optional[int]: if args.list_commands: self.listCommands() - return 0 - - if args.command == "help": + elif args.version: + self.showVersion() + return 0 + elif args.command == "help": self.parser.print_help() - return 0 return self.executeCommand(args.command, args.args) @@ -208,19 +222,19 @@ def executeCommand(self, command, args) -> Optional[int]: cmd.parse(args) if cmd.args.batch: - armi.Mode.setMode(armi.Mode.BATCH) + context.Mode.setMode(context.Mode.BATCH) elif cmd.mode is not None: - armi.Mode.setMode(cmd.mode) + context.Mode.setMode(cmd.mode) # do whatever there is to be done! return cmd.invoke() def splash(): - """ - Emit a the active App's splash text to the runLog for the master node. - """ - app = armi.getApp() + """Emit a the active App's splash text to the runLog for the master node.""" + from armi import getApp # pylint: disable=import-outside-toplevel + + app = getApp() assert app is not None - if armi.context.MPI_RANK == 0: + if context.MPI_RANK == 0: runLog.raw(app.splashText) diff --git a/armi/cli/checkInputs.py b/armi/cli/checkInputs.py index 2dbc8c0e3..8173c0999 100644 --- a/armi/cli/checkInputs.py +++ b/armi/cli/checkInputs.py @@ -25,19 +25,6 @@ from armi.utils.textProcessors import resolveMarkupInclusions -def _runInspectorOnSettings(cs): - from armi import operators - - operator_class = operators.getOperatorClassFromSettings(cs) - inspector = operator_class.inspector(cs) - issues = [ - query - for query in inspector.queries - if query and query.correction is not None and query._passed != True - ] # pylint: disable=protected-access - return issues - - class ExpandBlueprints(EntryPoint): """ Perform expansion of !include directives in a blueprint file. diff --git a/armi/cli/compareCases.py b/armi/cli/compareCases.py index f85398cc6..4c25e1808 100644 --- a/armi/cli/compareCases.py +++ b/armi/cli/compareCases.py @@ -182,14 +182,7 @@ def invoke(self): # contains all tests that user had access to allTests = [] for pat in self.args.patterns + self.args.additional_comparisons: - name, ext = os.path.splitext(pat) allTests.append(pat) - if ext == ".yaml": - # auto-add XML variants of yaml settings - # to accommodate comparisons against xml suites (e.g. testing) - xmlName = name + ".xml" - runLog.extra("Including {} in reference patterns.".format(xmlName)) - allTests.append(xmlName) refSuite.discover( rootDir=self.args.reference, patterns=allTests, diff --git a/armi/cli/database.py b/armi/cli/database.py index 2e0d2cab3..383fb2a35 100644 --- a/armi/cli/database.py +++ b/armi/cli/database.py @@ -19,7 +19,7 @@ import pathlib import re -import armi +from armi import context from armi import runLog from armi.cli.entryPoint import EntryPoint from armi.utils.textProcessors import resolveMarkupInclusions @@ -29,7 +29,7 @@ class ConvertDB(EntryPoint): """Convert databases between different versions""" name = "convert-db" - mode = armi.Mode.BATCH + mode = context.Mode.BATCH def addOptions(self): self.parser.add_argument("h5db", help="Input database path", type=str) @@ -93,7 +93,7 @@ class ExtractInputs(EntryPoint): """ name = "extract-inputs" - mode = armi.Mode.BATCH + mode = context.Mode.BATCH def addOptions(self): self.parser.add_argument("h5db", help="Path to input database", type=str) @@ -120,11 +120,7 @@ def invoke(self): with db: settings, geom, bp = db.readInputsFromDB() - settingsExt = ".yaml" - if settings.lstrip()[0] == "<": - settingsExt = ".xml" - - settingsPath = self.args.output_base + "_settings" + settingsExt + settingsPath = self.args.output_base + "_settings.yaml" bpPath = self.args.output_base + "_blueprints.yaml" geomPath = None @@ -163,7 +159,7 @@ class InjectInputs(EntryPoint): """ name = "inject-inputs" - mode = armi.Mode.BATCH + mode = context.Mode.BATCH def addOptions(self): self.parser.add_argument("h5db", help="Path to affected database", type=str) diff --git a/armi/cli/migrateInputs.py b/armi/cli/migrateInputs.py index b85420c66..d11e0c028 100644 --- a/armi/cli/migrateInputs.py +++ b/armi/cli/migrateInputs.py @@ -61,7 +61,6 @@ def _migrate(settingsPath, dbPath): Notes ----- Some migrations change the paths so we update them one by one. - For example, a migration converts a settings file from xml to yaml. """ for migrationI in ACTIVE_MIGRATIONS: if ( diff --git a/armi/cli/reportsEntryPoint.py b/armi/cli/reportsEntryPoint.py index dc54e2a87..ca0bca0a0 100644 --- a/armi/cli/reportsEntryPoint.py +++ b/armi/cli/reportsEntryPoint.py @@ -14,21 +14,19 @@ import pathlib import webbrowser -import armi -from armi.cli import entryPoint -from armi.reactor import reactors -from armi.utils import runLog +from armi import getPluginManagerOrFail from armi import settings -from armi.utils import directoryChangers -from armi.reactor import blueprints from armi.bookkeeping import newReports as reports from armi.bookkeeping.db import databaseFactory +from armi.cli import entryPoint +from armi.reactor import blueprints +from armi.reactor import reactors +from armi.utils import directoryChangers +from armi.utils import runLog class ReportsEntryPoint(entryPoint.EntryPoint): - """ - Create report from database files. - """ + """Create report from database files.""" name = "report" settingsArgument = "optional" @@ -100,7 +98,7 @@ def invoke(self): else: report = reports.ReportContent("Overview") - pm = armi.getPluginManagerOrFail() + pm = getPluginManagerOrFail() db = databaseFactory(self.args.h5db, "r") if self.args.bp is not None: blueprint = self.args.bp @@ -116,14 +114,12 @@ def invoke(self): blueprint = db.loadBlueprints() r = reactors.factory(cs, blueprint) report.title = r.name - pluginContent = ( - armi.getPluginManagerOrFail().hook.getReportContents( - r=r, - cs=cs, - report=report, - stage=reports.ReportStage.Begin, - blueprint=blueprint, - ) + pluginContent = getPluginManagerOrFail().hook.getReportContents( + r=r, + cs=cs, + report=report, + stage=reports.ReportStage.Begin, + blueprint=blueprint, ) stage = reports.ReportStage.Standard for cycle, node in dbNodes: @@ -164,7 +160,6 @@ def createReportFromSettings(cs): This will construct a reactor from the given settings and create BOL reports for that reactor/settings. """ - # not sure if this is necessary, but need to investigate more to understand possible # side-effects before removing. Probably better to get rid of all uses of # getMasterCs(), then we can remove all setMasterCs() calls without worrying. @@ -173,7 +168,7 @@ def createReportFromSettings(cs): blueprint = blueprints.loadFromCs(cs) r = reactors.factory(cs, blueprint) report = reports.ReportContent("Overview") - pm = armi.getPluginManagerOrFail() + pm = getPluginManagerOrFail() report.title = r.name with directoryChangers.ForcedCreationDirectoryChanger( diff --git a/armi/cli/tests/test_runEntryPoint.py b/armi/cli/tests/test_runEntryPoint.py index 2dcd8f0f0..6647d687c 100644 --- a/armi/cli/tests/test_runEntryPoint.py +++ b/armi/cli/tests/test_runEntryPoint.py @@ -29,6 +29,8 @@ from armi.cli.reportsEntryPoint import ReportsEntryPoint from armi.cli.run import RunEntryPoint from armi.cli.runSuite import RunSuiteCommand +from armi.physics.neutronics.diffIsotxs import CompareIsotxsLibraries +from armi.tests import mockRunLogs class TestCheckInputEntryPoint(unittest.TestCase): @@ -91,6 +93,13 @@ def test_convertDbBasics(self): self.assertEqual(cdb.name, "convert-db") self.assertIsNone(cdb.args.nodes) + # Since the file is fake, invoke() should exit early. + with mockRunLogs.BufferLog() as mock: + cdb.args.nodes = [1, 2, 3] + with self.assertRaises(ValueError): + cdb.invoke() + self.assertIn("Converting the", mock._outputStream) + class TestCopyDB(unittest.TestCase): def test_copyDBBasics(self): @@ -106,12 +115,18 @@ def test_copyDBBasics(self): class TestExpandBlueprints(unittest.TestCase): def test_expandBlueprintsBasics(self): - eb = ExpandBlueprints() - eb.addOptions() - eb.parse_args(["/path/to/fake.yaml"]) + ebp = ExpandBlueprints() + ebp.addOptions() + ebp.parse_args(["/path/to/fake.yaml"]) + + self.assertEqual(ebp.name, "expand-bp") + self.assertEqual(ebp.args.blueprints, "/path/to/fake.yaml") - self.assertEqual(eb.name, "expand-bp") - self.assertEqual(eb.args.blueprints, "/path/to/fake.yaml") + # Since the file is fake, invoke() should exit early. + with mockRunLogs.BufferLog() as mock: + self.assertEqual("", mock._outputStream) + ebp.invoke() + self.assertIn("does not exist", mock._outputStream) class TestExtractInputs(unittest.TestCase): @@ -164,6 +179,18 @@ def test_reportsEntryPointBasics(self): self.assertEqual(rep.settingsArgument, "optional") +class TestCompareIsotxsLibsEntryPoint(unittest.TestCase): + def test_compareIsotxsLibsBasics(self): + com = CompareIsotxsLibraries() + com.addOptions() + com.parse_args( + ["--fluxFile", "/path/to/fluxfile.txt", "reference", "comparisonFiles"] + ) + + self.assertEqual(com.name, "diff-isotxs") + self.assertIsNone(com.settingsArgument) + + class TestRunEntryPoint(unittest.TestCase): def test_runEntryPointBasics(self): rep = RunEntryPoint() @@ -173,13 +200,24 @@ def test_runEntryPointBasics(self): self.assertEqual(rep.name, "run") self.assertEqual(rep.settingsArgument, "required") - def test_runCommand(self): + def test_runCommandHelp(self): """Ensure main entry point with no args completes.""" with self.assertRaises(SystemExit) as excinfo: - sys.argv = [""] # have to override the pytest args + # have to override the pytest args + sys.argv = [""] main() self.assertEqual(excinfo.exception.code, 0) + def test_executeCommand(self): + """use executeCommand to call run, + But we expect it to fail because we provide a fictional settings YAML + """ + with self.assertRaises(SystemExit) as excinfo: + # override the pytest args + sys.argv = ["run", "path/to/fake.yaml"] + main() + self.assertEqual(excinfo.exception.code, 1) + class TestRunSuiteCommand(unittest.TestCase): def test_runSuiteCommandBasics(self): diff --git a/armi/cli/tests/test_runSuite.py b/armi/cli/tests/test_runSuite.py index a3e8b6115..647757314 100644 --- a/armi/cli/tests/test_runSuite.py +++ b/armi/cli/tests/test_runSuite.py @@ -33,8 +33,26 @@ def test_listCommand(self): cli.listCommands() finally: sys.stdout = origout + self.assertIn("run-suite", out.getvalue()) + def test_showVersion(self): + """Test the ArmiCLI.showVersion method""" + from armi import cli, meta + + cli = cli.ArmiCLI() + + origout = sys.stdout + try: + out = io.StringIO() + sys.stdout = out + cli.showVersion() + finally: + sys.stdout = origout + + self.assertIn("armi", out.getvalue()) + self.assertIn(meta.__version__, out.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/armi/materials/alloy200.py b/armi/materials/alloy200.py index 1bafa7111..af609f1bf 100644 --- a/armi/materials/alloy200.py +++ b/armi/materials/alloy200.py @@ -99,7 +99,6 @@ def setDefaultMassFracs(self): ----- It is assumed half the max composition for the impurities and the rest is Ni. """ - nickleMassFrac = 1.0 for elementSymbol, massFrac in self.referenceMaxPercentImpurites: diff --git a/armi/materials/mgO.py b/armi/materials/mgO.py index 5284ee0f5..d70a5452d 100644 --- a/armi/materials/mgO.py +++ b/armi/materials/mgO.py @@ -32,7 +32,7 @@ def linearExpansionPercent(self, Tk=None, Tc=None): """THE COEFFICIENT OF EXPANSION OF MAGNESIUM OXIDE Milo A. Durand - Journal of Applied Physics 7, 297 (1936); doi: 10.1063/1.174539 + Journal of Applied Physics 7, 297 (1936); doi: 10.1063/1.174539 This is based on a 3rd order polynomial fit of the data in Table I. """ @@ -50,7 +50,6 @@ def density(self, Tk=None, Tc=None): self.checkTempRange(273, 1273, Tk, "density") dLL = self.linearExpansionPercent(Tk=Tk) - # runLog.debug('dLL: {0} m/m/K'.format(dLL)) dRho = (1 - (1 + dLL) ** 3) / (1 + dLL) ** 3 density = rho0 * (1 + dRho) return density diff --git a/armi/materials/tests/test_b4c.py b/armi/materials/tests/test_b4c.py index f6cb55790..892005ad1 100644 --- a/armi/materials/tests/test_b4c.py +++ b/armi/materials/tests/test_b4c.py @@ -17,9 +17,9 @@ import unittest -from armi.materials.tests.test_materials import _Material_Test from armi.materials.b4c import B4C from armi.materials.b4c import DEFAULT_THEORETICAL_DENSITY_FRAC +from armi.materials.tests.test_materials import _Material_Test class B4C_TestCase(_Material_Test, unittest.TestCase): diff --git a/armi/materials/tests/test_lithium.py b/armi/materials/tests/test_lithium.py index 2b0c6eaa0..2123da5aa 100644 --- a/armi/materials/tests/test_lithium.py +++ b/armi/materials/tests/test_lithium.py @@ -48,6 +48,34 @@ def test_Lithium_material_modifications(self): self.assertEqual(self.Lithium_both.getMassFrac("LI6"), 0.8) + def test_density(self): + ref = self.mat.density(Tc=100) + cur = 0.512 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + ref = self.mat.density(Tc=200) + cur = 0.512 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + def test_meltingPoint(self): + ref = self.mat.meltingPoint() + cur = 453.69 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + def test_boilingPoint(self): + ref = self.mat.boilingPoint() + cur = 1615.0 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + def test_heatCapacity(self): + ref = self.mat.heatCapacity(Tc=100) + cur = 3570.0 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + ref = self.mat.heatCapacity(Tc=200) + cur = 3570.0 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + if __name__ == "__main__": unittest.main() diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index 37076cb52..a13804f59 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -14,15 +14,15 @@ r"""Tests materials.py""" # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access,no-member,invalid-name -import unittest import pickle +import unittest from numpy import testing from armi import materials, settings -from armi.utils import units from armi.nucDirectory import nuclideBases from armi.reactor import blueprints +from armi.utils import units class _Material_Test: @@ -37,10 +37,10 @@ def test_isPicklable(self): """Test that all materials are picklable so we can do MPI communication of state.""" stream = pickle.dumps(self.mat) mat = pickle.loads(stream) + + # check a property that is sometimes interpolated. self.assertEqual( - # check a property that is sometimes interpolated. - self.mat.thermalConductivity(500), - mat.thermalConductivity(500), + self.mat.thermalConductivity(500), mat.thermalConductivity(500) ) @@ -98,6 +98,30 @@ def test_density(self): self.assertAlmostEqual(cur, ref, delta=delta) +class MagnesiumOxide_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.MgO + + def test_density(self): + cur = self.mat.density(923) + ref = 3.48887 + delta = ref * 0.05 + self.assertAlmostEqual(cur, ref, delta=delta) + + cur = self.mat.density(1390) + ref = 3.418434 + delta = ref * 0.05 + self.assertAlmostEqual(cur, ref, delta=delta) + + def test_linearExpansionPercent(self): + cur = self.mat.linearExpansionPercent(Tc=100) + ref = 0.00110667 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.linearExpansionPercent(Tc=400) + ref = 0.0049909 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + class Molybdenum_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Molybdenum @@ -113,26 +137,74 @@ def test_density(self): self.assertAlmostEqual(cur, ref, delta=delta) +class NaCl_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.NaCl + + def test_density(self): + cur = self.mat.density(Tc=100) + ref = 2.113204 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.density(Tc=300) + ref = 2.050604 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + +class NiobiumZirconium_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.NZ + + def test_density(self): + cur = self.mat.density(Tk=100) + ref = 8.66 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.density(Tk=1390) + ref = 8.66 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + class Potassium_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Potassium def test_density(self): - cur = self.mat.density(333) + cur = self.mat.density(Tk=333) ref = 0.828 delta = ref * 0.001 self.assertAlmostEqual(cur, ref, delta=delta) - cur = self.mat.density(500) + cur = self.mat.density(Tk=500) ref = 0.7909 delta = ref * 0.001 self.assertAlmostEqual(cur, ref, delta=delta) - cur = self.mat.density(750) + cur = self.mat.density(Tk=750) ref = 0.732 delta = ref * 0.001 self.assertAlmostEqual(cur, ref, delta=delta) +class ScandiumOxide_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.Sc2O3 + + def test_density(self): + cur = self.mat.density(Tc=100) + ref = 3.86 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.density(Tc=400) + ref = 3.86 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + def test_linearExpansionPercent(self): + cur = self.mat.linearExpansionPercent(Tc=100) + ref = 0.0623499 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.linearExpansionPercent(Tc=400) + ref = 0.28322 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + class Sodium_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Sodium @@ -181,6 +253,55 @@ def test_thermalConductivity(self): self.assertAlmostEqual(cur, ref, delta=delta) +class Tantalum_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.Tantalum + + def test_density(self): + cur = self.mat.density(Tc=100) + ref = 16.6 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.density(Tc=300) + ref = 16.6 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + +class ThoriumOxide_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.ThU + + def test_density(self): + cur = self.mat.density(Tc=100) + ref = 11.68 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.density(Tc=300) + ref = 11.68 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + def test_meltingPoint(self): + cur = self.mat.meltingPoint() + ref = 2025.0 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + def test_thermalConductivity(self): + cur = self.mat.thermalConductivity(Tc=100) + ref = 43.1 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.thermalConductivity(Tc=300) + ref = 43.1 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + def test_linearExpansion(self): + cur = self.mat.linearExpansion(Tc=100) + ref = 11.9e-6 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.linearExpansion(Tc=300) + ref = 11.9e-6 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + class Uranium_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Uranium @@ -290,6 +411,15 @@ def test_linearExpansion(self): accuracy = 2 self.assertAlmostEqual(cur, ref, accuracy) + def test_linearExpansionPercent(self): + cur = self.mat.linearExpansionPercent(Tk=500) + ref = 0.222826 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + + cur = self.mat.linearExpansionPercent(Tk=950) + ref = 0.677347 + self.assertAlmostEqual(cur, ref, delta=abs(ref * 0.001)) + def test_heatCapacity(self): """Check against Figure 4.2 from ORNL 2000-1723 EFG""" self.assertAlmostEqual(self.mat.heatCapacity(300), 230.0, delta=20) @@ -524,6 +654,15 @@ def test_getTempChangeForDensityChange(self): ) self.assertAlmostEqual(expectedDeltaT, actualDeltaT) + def test_dynamicVisc(self): + ref = self.mat.dynamicVisc(Tc=100) + cur = 0.0037273 + self.assertAlmostEqual(ref, cur, delta=ref * 0.001) + + ref = self.mat.dynamicVisc(Tc=200) + cur = 0.0024316 + self.assertAlmostEqual(ref, cur, delta=ref * 0.001) + class Sulfur_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Sulfur @@ -705,7 +844,7 @@ def test_Iconel800_linearExpansion(self): class Inconel600_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Inconel600 - def test00_setDefaultMassFracs(self): + def test_00_setDefaultMassFracs(self): massFracNameList = ["NI", "CR", "FE", "C", "MN55", "S", "SI", "CU"] massFracRefValList = [ 0.7541, @@ -723,7 +862,7 @@ def test00_setDefaultMassFracs(self): ref = frac self.assertAlmostEqual(cur, ref) - def test01_linearExpansionPercent(self): + def test_01_linearExpansionPercent(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 0.105392, @@ -744,7 +883,7 @@ def test01_linearExpansionPercent(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) - def test02_linearExpansion(self): + def test_02_linearExpansion(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 1.3774400000000001e-05, @@ -765,7 +904,7 @@ def test02_linearExpansion(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) - def test03_density(self): + def test_03_density(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 8.452174779681522, @@ -786,11 +925,44 @@ def test03_density(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) + def test_polyfitThermalConductivity(self): + ref = self.mat.polyfitThermalConductivity(power=2) + cur = [3.49384e-06, 0.01340, 14.57241] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=curVal * 0.001) + + def test_polyfitHeatCapacity(self): + ref = self.mat.polyfitHeatCapacity(power=2) + cur = [7.40206e-06, 0.20573, 441.29945] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=curVal * 0.001) + + def test_polyfitLinearExpansionPercent(self): + ref = self.mat.polyfitLinearExpansionPercent(power=2) + cur = [3.72221e-07, 0.00130308, -0.0286255941973353] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_heatCapacity(self): + ref = self.mat.heatCapacity(Tc=100) + cur = 461.947021 + self.assertAlmostEqual(ref, cur, delta=cur * 0.001) + + ref = self.mat.heatCapacity(Tc=200) + cur = 482.742084 + self.assertAlmostEqual(ref, cur, delta=cur * 0.001) + class Inconel625_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Inconel625 - def test00_setDefaultMassFracs(self): + def test_00_setDefaultMassFracs(self): massFracNameList = [ "NI", "CR", @@ -827,7 +999,7 @@ def test00_setDefaultMassFracs(self): ref = frac self.assertAlmostEqual(cur, ref) - def test01_linearExpansionPercent(self): + def test_01_linearExpansionPercent(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 0.09954299999999999, @@ -848,7 +1020,7 @@ def test01_linearExpansionPercent(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) - def test02_linearExpansion(self): + def test_02_linearExpansion(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 1.22666e-05, @@ -869,7 +1041,7 @@ def test02_linearExpansion(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) - def test03_density(self): + def test_03_density(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 8.423222197446128, @@ -890,11 +1062,44 @@ def test03_density(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) + def test_polyfitThermalConductivity(self): + ref = self.mat.polyfitThermalConductivity(power=2) + cur = [2.7474128e-06, 0.01290669, 9.6253227] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_polyfitHeatCapacity(self): + ref = self.mat.polyfitHeatCapacity(power=2) + cur = [-5.377736582e-06, 0.250006, 404.26111] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_polyfitLinearExpansionPercent(self): + ref = self.mat.polyfitLinearExpansionPercent(power=2) + cur = [5.08303200671101e-07, 0.001125487, -0.0180449] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_heatCapacity(self): + ref = self.mat.heatCapacity(Tc=100) + cur = 429.206223 + self.assertAlmostEqual(ref, cur, delta=cur * 0.001) + + ref = self.mat.heatCapacity(Tc=200) + cur = 454.044892 + self.assertAlmostEqual(ref, cur, delta=cur * 0.001) + class InconelX750_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.InconelX750 - def test00_setDefaultMassFracs(self): + def test_00_setDefaultMassFracs(self): massFracNameList = [ "NI", "CR", @@ -929,7 +1134,7 @@ def test00_setDefaultMassFracs(self): ref = frac self.assertAlmostEqual(cur, ref) - def test01_linearExpansionPercent(self): + def test_01_linearExpansionPercent(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 0.09927680000000001, @@ -950,7 +1155,7 @@ def test01_linearExpansionPercent(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) - def test02_linearExpansion(self): + def test_02_linearExpansion(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 1.1927560000000001e-05, @@ -971,7 +1176,7 @@ def test02_linearExpansion(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) - def test03_density(self): + def test_03_density(self): TcList = [100, 200, 300, 400, 500, 600, 700, 800] refList = [ 8.263584211566972, @@ -992,15 +1197,72 @@ def test03_density(self): ) self.assertAlmostEqual(cur, ref, delta=10e-7, msg=errorMsg) + def test_polyfitThermalConductivity(self): + ref = self.mat.polyfitThermalConductivity(power=2) + cur = [1.48352396e-06, 0.012668, 11.631576] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_polyfitHeatCapacity(self): + ref = self.mat.polyfitHeatCapacity(power=2) + cur = [0.000269809, 0.05272799, 446.51227] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_polyfitLinearExpansionPercent(self): + ref = self.mat.polyfitLinearExpansionPercent(power=2) + cur = [6.8377787e-07, 0.0010559998, -0.013161] + + self.assertEqual(len(ref), len(cur)) + for i, curVal in enumerate(cur): + self.assertAlmostEqual(ref[i], curVal, delta=abs(curVal * 0.001)) + + def test_heatCapacity(self): + ref = self.mat.heatCapacity(Tc=100) + cur = 459.61381 + self.assertAlmostEqual(ref, cur, delta=cur * 0.001) + + ref = self.mat.heatCapacity(Tc=200) + cur = 484.93968 + self.assertAlmostEqual(ref, cur, delta=cur * 0.001) + + +class Alloy200_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.Alloy200 -class Alloy200_TestCase(unittest.TestCase): def test_nickleContent(self): - """ - Assert alloy 200 has more than 99% nickle per its spec - """ - from armi.materials.alloy200 import Alloy200 + """Assert alloy 200 has more than 99% nickle per its spec""" + self.assertGreater(self.mat.p.massFrac["NI"], 0.99) + + +class CaH2_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.CaH2 + + def test_density(self): + cur = 1.7 + + ref = self.mat.density(Tc=100) + self.assertAlmostEqual(cur, ref, ref * 0.01) - self.assertGreater(Alloy200().p.massFrac["NI"], 0.99) + ref = self.mat.density(Tc=300) + self.assertAlmostEqual(cur, ref, ref * 0.01) + + +class Hafnium_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.Hafnium + + def test_density(self): + cur = 13.07 + + ref = self.mat.density(Tc=100) + self.assertAlmostEqual(cur, ref, ref * 0.01) + + ref = self.mat.density(Tc=300) + self.assertAlmostEqual(cur, ref, ref * 0.01) class HastelloyN_TestCase(_Material_Test, unittest.TestCase): @@ -1093,7 +1355,7 @@ def test_meanCoefficientThermalExpansion(self): class TZM_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.TZM - def test00_applyInputParams(self): + def test_00_applyInputParams(self): massFracNameList = ["C", "TI", "ZR", "MO"] massFracRefValList = [2.50749e-05, 0.002502504, 0.000761199, 0.996711222] @@ -1104,12 +1366,12 @@ def test00_applyInputParams(self): ref = frac self.assertEqual(cur, ref) - def test01_density(self): + def test_01_density(self): ref = 10.16 # g/cc cur = self.mat.density() self.assertEqual(cur, ref) - def test02_linearExpansionPercent(self): + def test_02_linearExpansionPercent(self): TcList = [ 21.11, 456.11, @@ -1146,6 +1408,50 @@ def test02_linearExpansionPercent(self): self.assertAlmostEqual(cur, ref, delta=10e-3, msg=errorMsg) +class YttriumOxide_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.Y2O3 + + def test_density(self): + cur = 5.03 + + ref = self.mat.density(Tc=100) + self.assertAlmostEqual(cur, ref, ref * 0.01) + + ref = self.mat.density(Tc=300) + self.assertAlmostEqual(cur, ref, ref * 0.01) + + def test_linearExpansionPercent(self): + ref = self.mat.linearExpansionPercent(Tc=100) + cur = 0.069662 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + ref = self.mat.linearExpansionPercent(Tc=100) + cur = 0.0696622 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + +class ZincOxide_TestCase(_Material_Test, unittest.TestCase): + MAT_CLASS = materials.ZnO + + def test_density(self): + cur = 5.61 + + ref = self.mat.density(Tc=100) + self.assertAlmostEqual(cur, ref, ref * 0.01) + + ref = self.mat.density(Tc=300) + self.assertAlmostEqual(cur, ref, ref * 0.01) + + def test_linearExpansionPercent(self): + ref = self.mat.linearExpansionPercent(Tc=100) + cur = -99670.4933 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + ref = self.mat.linearExpansionPercent(Tc=100) + cur = -99670.4933 + self.assertAlmostEqual(ref, cur, delta=abs(ref * 0.001)) + + class FuelMaterial_TestCase(unittest.TestCase): baseInput = r""" nuclide flags: diff --git a/armi/mpiActions.py b/armi/mpiActions.py index e3b39886d..c7bd97f83 100644 --- a/armi/mpiActions.py +++ b/armi/mpiActions.py @@ -30,15 +30,15 @@ * - 1 - **master**: :py:class:`distributeState = DistributeStateAction() ` - **worker**: :code:`action = armi.MPI_COMM.bcast(None, root=0)` + **worker**: :code:`action = context.MPI_COMM.bcast(None, root=0)` - **master**: Initializing a distribute state action. **worker**: Waiting for something to do, as determined by the master, this happens within the worker's :py:meth:`~armi.operators.MpiOperator.workerOperate`. * - 2 - - **master**: :code:`armi.MPI_COMM.bcast(distributeState, root=0)` + - **master**: :code:`context.MPI_COMM.bcast(distributeState, root=0)` - **worker**: :code:`action = armi.MPI_COMM.bcast(None, root=0)` + **worker**: :code:`action = context.MPI_COMM.bcast(None, root=0)` - **master**: Broadcasts a distribute state action to all the worker nodes **worker**: Receives the action from the master, which is a @@ -62,14 +62,14 @@ from six.moves import cPickle import tabulate -import armi +from armi import context +from armi import interfaces from armi import runLog from armi import settings -from armi import interfaces +from armi import utils from armi.reactor import reactors from armi.reactor import assemblies from armi.reactor.parameters import parameterDefinitions -from armi import utils from armi.utils import iterables @@ -168,13 +168,13 @@ def broadcast(self, obj=None): """ if self.serial: return obj if obj is not None else self - if armi.MPI_SIZE > 1: - result = self._mpiOperationHelper(obj, armi.MPI_COMM.bcast) + if context.MPI_SIZE > 1: + result = self._mpiOperationHelper(obj, context.MPI_COMM.bcast) # the following if-branch prevents the creation of duplicate objects on the master node # if the object is large with lots of links, it is prudent to call gc.collect() - if obj is None and armi.MPI_RANK == 0: + if obj is None and context.MPI_RANK == 0: return self - elif armi.MPI_RANK == 0: + elif context.MPI_RANK == 0: return obj else: return result @@ -193,9 +193,9 @@ def gather(self, obj=None): """ if self.serial: return [obj if obj is not None else self] - if armi.MPI_SIZE > 1: - result = self._mpiOperationHelper(obj, armi.MPI_COMM.gather) - if armi.MPI_RANK == 0: + if context.MPI_SIZE > 1: + result = self._mpiOperationHelper(obj, context.MPI_COMM.gather) + if context.MPI_RANK == 0: # this cannot be result[0] = obj or self, because 0.0, 0, [] all eval to False if obj is None: result[0] = self @@ -263,12 +263,12 @@ def mpiIter(objectsForAllCoresToIter): mpiFlatten : used for collecting results """ ntasks = len(objectsForAllCoresToIter) - numLocalObjects, deficit = divmod(ntasks, armi.MPI_SIZE) - if armi.MPI_RANK < deficit: + numLocalObjects, deficit = divmod(ntasks, context.MPI_SIZE) + if context.MPI_RANK < deficit: numLocalObjects += 1 - first = armi.MPI_RANK * numLocalObjects + first = context.MPI_RANK * numLocalObjects else: - first = armi.MPI_RANK * numLocalObjects + deficit + first = context.MPI_RANK * numLocalObjects + deficit for objIndex in range(first, first + numLocalObjects): yield objectsForAllCoresToIter[objIndex] @@ -294,20 +294,20 @@ def runActions(o, r, cs, actions, numPerNode=None, serial=False): Notes ----- - The number of actions DOES NOT need to match :code:`armi.MPI_SIZE`. + The number of actions DOES NOT need to match :code:`context.MPI_SIZE`. Calling this method may invoke MPI Split which will change the MPI_SIZE during the action. This allows someone to call MPI operations without being blocked by tasks which are not doing the same thing. """ - if not armi.MPI_DISTRIBUTABLE or serial: + if not context.MPI_DISTRIBUTABLE or serial: return runActionsInSerial(o, r, cs, actions) - useForComputation = [True] * armi.MPI_SIZE + useForComputation = [True] * context.MPI_SIZE if numPerNode != None: if numPerNode < 1: raise ValueError("numPerNode must be >= 1") - numThisNode = {nodeName: 0 for nodeName in armi.MPI_NODENAMES} - for rank, nodeName in enumerate(armi.MPI_NODENAMES): + numThisNode = {nodeName: 0 for nodeName in context.MPI_NODENAMES} + for rank, nodeName in enumerate(context.MPI_NODENAMES): useForComputation[rank] = numThisNode[nodeName] < numPerNode numThisNode[nodeName] += 1 numBatches = int( @@ -329,7 +329,7 @@ def runActions(o, r, cs, actions, numPerNode=None, serial=False): for useRank in useForComputation: actionsThisRound.append(queue.pop(0) if useRank and queue else None) realActions = [ - (armi.MPI_NODENAMES[rank], rank, act) + (context.MPI_NODENAMES[rank], rank, act) for rank, act in enumerate(actionsThisRound) if act is not None ] @@ -377,7 +377,7 @@ class DistributionAction(MpiAction): it possible for sub-tasks to manage their own communicators and spawn their own work within some sub-communicator. - This performs an MPI Split operation and takes over the armi.MPI_COMM and associated varaibles. + This performs an MPI Split operation and takes over the context.MPI_COMM and associated varaibles. For this reason, it is possible that when someone thinks they have distributed information to all nodes, it may only be a subset that was necessary to perform the number of actions needed by this DsitributionAction. @@ -402,11 +402,11 @@ def invokeHook(self): ===== Two things about this method make it non-recursiv """ - canDistribute = armi.MPI_DISTRIBUTABLE - mpiComm = armi.MPI_COMM - mpiRank = armi.MPI_RANK - mpiSize = armi.MPI_SIZE - mpiNodeNames = armi.MPI_NODENAMES + canDistribute = context.MPI_DISTRIBUTABLE + mpiComm = context.MPI_COMM + mpiRank = context.MPI_RANK + mpiSize = context.MPI_SIZE + mpiNodeNames = context.MPI_NODENAMES if self.cs["verbosity"] == "debug" and mpiRank == 0: runLog.debug("Printing diagnostics for MPI actions!") @@ -423,21 +423,21 @@ def invokeHook(self): try: action = mpiComm.scatter(self._actions, root=0) # create a new communicator that only has these specific dudes running - armi.MPI_DISTRIBUTABLE = False + context.MPI_DISTRIBUTABLE = False hasAction = action is not None - armi.MPI_COMM = mpiComm.Split(int(hasAction)) - armi.MPI_RANK = armi.MPI_COMM.Get_rank() - armi.MPI_SIZE = armi.MPI_COMM.Get_size() - armi.MPI_NODENAMES = armi.MPI_COMM.allgather(armi.MPI_NODENAME) + context.MPI_COMM = mpiComm.Split(int(hasAction)) + context.MPI_RANK = context.MPI_COMM.Get_rank() + context.MPI_SIZE = context.MPI_COMM.Get_size() + context.MPI_NODENAMES = context.MPI_COMM.allgather(context.MPI_NODENAME) if hasAction: return action.invoke(self.o, self.r, self.cs) finally: # restore the global variables - armi.MPI_DISTRIBUTABLE = canDistribute - armi.MPI_COMM = mpiComm - armi.MPI_RANK = mpiRank - armi.MPI_SIZE = mpiSize - armi.MPI_NODENAMES = mpiNodeNames + context.MPI_DISTRIBUTABLE = canDistribute + context.MPI_COMM = mpiComm + context.MPI_RANK = mpiRank + context.MPI_SIZE = mpiSize + context.MPI_NODENAMES = mpiNodeNames class MpiActionError(Exception): @@ -458,7 +458,7 @@ def invokeHook(self): """ - if armi.MPI_SIZE <= 1: + if context.MPI_SIZE <= 1: runLog.extra("Not distributing state because there is only one processor") return # Detach phase: @@ -489,13 +489,13 @@ def invokeHook(self): runLog.error("Failed to transmit on distribute state root MPI bcast") runLog.error(error) # workers are still waiting for a reactor object - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: _diagnosePickleError(self.o) - armi.MPI_COMM.bcast("quit") # try to get the workers to quit. + context.MPI_COMM.bcast("quit") # try to get the workers to quit. raise - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: self.r.core.regenAssemblyLists() # pylint: disable=no-member # check to make sure that everything has been properly reattached @@ -516,18 +516,18 @@ def invokeHook(self): ) def _distributeSettings(self): - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: runLog.debug("Sending the settings object") self.cs = cs = self.broadcast(self.o.cs) if isinstance(cs, settings.Settings): runLog.setVerbosity( - cs["verbosity"] if armi.MPI_RANK == 0 else cs["branchVerbosity"] + cs["verbosity"] if context.MPI_RANK == 0 else cs["branchVerbosity"] ) runLog.debug("Received settings object") else: raise RuntimeError("Failed to transmit settings, received: {}".format(cs)) - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: settings.setMasterCs(cs) self.o.cs = cs return cs @@ -541,7 +541,7 @@ def _distributeReactor(self, cs): else: raise RuntimeError("Failed to transmit reactor, received: {}".format(r)) - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: # on the master node this unfortunately created a __deepcopy__ of the reactor, delete it del r else: @@ -561,7 +561,7 @@ def _distributeReactor(self, cs): def _distributeParamAssignments(self): data = dict() - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: data = { (pName, pdType.__name__): pDef.assigned for ( @@ -570,9 +570,9 @@ def _distributeParamAssignments(self): ), pDef in parameterDefinitions.ALL_DEFINITIONS.items() } - data = armi.MPI_COMM.bcast(data, root=0) + data = context.MPI_COMM.bcast(data, root=0) - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: for (pName, pdType), pDef in parameterDefinitions.ALL_DEFINITIONS.items(): pDef.assigned = data[pName, pdType.__name__] @@ -597,7 +597,7 @@ def _distributeInterfaces(self): armi.interfaces.Interface.interactDistributeState : runs on workers after DS """ - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: # These run on the master node. (Worker nodes run sychronized code below) toRestore = {} for i in self.o.getInterfaces(): diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py index 23e6f4fa7..16451d954 100644 --- a/armi/nucDirectory/nuclideBases.py +++ b/armi/nucDirectory/nuclideBases.py @@ -95,16 +95,16 @@ def getAttributes(nuc): """ +import glob import os import pathlib -import glob from ruamel import yaml -import armi +from armi import context +from armi import runLog from armi.nucDirectory import elements from armi.nucDirectory import transmutations -from armi import runLog from armi.utils.units import HEAVY_METAL_CUTOFF_Z # used to prevent multiple applications of burn chains, which would snowball @@ -275,7 +275,7 @@ def __readRiplNuclides(): elements.clearNuclideBases() for z, a, symbol, mass, _err in ripl.readFRDMMassFile( - os.path.join(armi.context.RES, "ripl-mass-frdm95.dat") + os.path.join(context.RES, "ripl-mass-frdm95.dat") ): if z == 0 and a == 1: # skip the neutron @@ -292,7 +292,7 @@ def __readRiplAbundance(): """ from armi.nuclearDataIO import ripl - with open(os.path.join(armi.context.RES, "ripl-abundance.dat")) as ripl_abundance: + with open(os.path.join(context.RES, "ripl-abundance.dat")) as ripl_abundance: for _z, a, sym, percent, _err in ripl.readAbundanceFile(ripl_abundance): nb = byName[sym + "{}".format(a)] nb.abundance = percent / 100.0 @@ -307,7 +307,7 @@ def __readMc2Nuclides(): This assigns MC2 labels and often adds metastable versions of nuclides that have already been added from RIPL. """ - with open(os.path.join(armi.context.RES, "mc2Nuclides.yaml"), "r") as mc2Nucs: + with open(os.path.join(context.RES, "mc2Nuclides.yaml"), "r") as mc2Nucs: mc2Nuclides = yaml.load(mc2Nucs, yaml.RoundTripLoader) # now add the mc2 specific nuclideBases, and correct the mc2Ids when a > 0 and state = 0 diff --git a/armi/nucDirectory/tests/test_thermalScattering.py b/armi/nucDirectory/tests/test_thermalScattering.py index e1d2249fc..1d9a0827b 100644 --- a/armi/nucDirectory/tests/test_thermalScattering.py +++ b/armi/nucDirectory/tests/test_thermalScattering.py @@ -61,34 +61,34 @@ def test_graphiteOnReactor(self): self.assertIs(tsl[carbon12], ts.byNbAndCompound[carbon, ts.GRAPHITE_10P]) - def testEndf8Compound(self): + def test_endf8Compound(self): si = nb.byName["SI"] o = nb.byName["O"] sio2 = ts.ThermalScattering((si, o), "SiO2-alpha") self.assertEqual(sio2._genENDFB8Label(), "tsl-SiO2-alpha.endf") - def testEndf8ElementInCompound(self): + def test_endf8ElementInCompound(self): hyd = nb.byName["H"] hInH2O = ts.ThermalScattering(hyd, "H2O") self.assertEqual(hInH2O._genENDFB8Label(), "tsl-HinH2O.endf") - def testEndf8Isotope(self): + def test_endf8Isotope(self): fe56 = nb.byName["FE56"] fe56tsl = ts.ThermalScattering(fe56) self.assertEqual(fe56tsl._genENDFB8Label(), "tsl-026_Fe_056.endf") - def testACECompound(self): + def test_ACECompound(self): si = nb.byName["SI"] o = nb.byName["O"] sio2 = ts.ThermalScattering((si, o), "SiO2-alpha") self.assertEqual(sio2._genACELabel(), "sio2") - def testACEElementInCompound(self): + def test_ACEElementInCompound(self): hyd = nb.byName["H"] hInH2O = ts.ThermalScattering(hyd, "H2O") self.assertEqual(hInH2O._genACELabel(), "h-h2o") - def testACEIsotope(self): + def test_ACEIsotope(self): fe56 = nb.byName["FE56"] fe56tsl = ts.ThermalScattering(fe56) self.assertEqual(fe56tsl._genACELabel(), "fe-56") diff --git a/armi/nuclearDataIO/cccc/tests/test_dlayxs.py b/armi/nuclearDataIO/cccc/tests/test_dlayxs.py index f17a3409f..ca6fada3a 100644 --- a/armi/nuclearDataIO/cccc/tests/test_dlayxs.py +++ b/armi/nuclearDataIO/cccc/tests/test_dlayxs.py @@ -142,7 +142,7 @@ def test_chi_delaySumsTo1(self): for dlayData in self.dlayxs3.values(): self.assertAlmostEqual(6.0, dlayData.delayEmissionSpectrum.sum(), 6) - def testNuDelay(self): + def test_NuDelay(self): delay = self.dlayxs3 # this data has NOT been compared to ENDF-V, it was created before modifying the read/write of DLAYXS and # was used to make sure the data wasn't accidentally transposed, hence there are two nuclides with two vectors diff --git a/armi/nuclearDataIO/tests/library-file-generation/mc2v2-dlayxs.inp b/armi/nuclearDataIO/tests/library-file-generation/mc2v2-dlayxs.inp index b95554273..d163aa19a 100644 --- a/armi/nuclearDataIO/tests/library-file-generation/mc2v2-dlayxs.inp +++ b/armi/nuclearDataIO/tests/library-file-generation/mc2v2-dlayxs.inp @@ -272,7 +272,7 @@ DATASET=A.MCC2 22 CM2455 7 202.55000 22 CM2465 7 202.55000 22 CM2475 7 202.55000 -01 ARMI generated \\albert\apps\dev\mc2\2.0\mc2.exe case for caseTitle mrtTWRP600v6rev3OS, block +01 ARMI generated \\path\to\mc2\2.0\mc2.exe case for caseTitle mrtTWRP600v6rev3OS, block DATASET=A.DLAY 01 CREATE VERSION VI DLAYXS FILE FROM FUELI 03 1 0 0 0 0 diff --git a/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AA.inp b/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AA.inp index 0bd24fa38..c7dede892 100644 --- a/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AA.inp +++ b/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AA.inp @@ -8,8 +8,8 @@ $control l_gamma = T / $library - c_mcclibdir ="\\albert\apps\prod\mc2\3.2.2\libraries\endfb-vii.0\lib.mcc.e70" - c_gammalibdir = "\\albert\apps\prod\mc2\3.2.2\libraries\endfb-vii.0\lib.gamma.e70" + c_mcclibdir ="\\path\to\mc2\3.2.2\libraries\endfb-vii.0\lib.mcc.e70" + c_gammalibdir = "\\path\to\mc2\3.2.2\libraries\endfb-vii.0\lib.gamma.e70" / $material t_composition(:,1) = U235_7 "U235AA" 1.00000E-03 873.000 ! Fuel diff --git a/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AB.inp b/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AB.inp index 90eec1475..8dabb5ad7 100644 --- a/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AB.inp +++ b/armi/nuclearDataIO/tests/library-file-generation/mc2v3-AB.inp @@ -8,8 +8,8 @@ $control l_gamma = T / $library - c_mcclibdir ="\\albert\apps\prod\mc2\3.2.2\libraries\endfb-vii.0\lib.mcc.e70" - c_gammalibdir = "\\albert\apps\prod\mc2\3.2.2\libraries\endfb-vii.0\lib.gamma.e70" + c_mcclibdir ="\\path\to\mc2\3.2.2\libraries\endfb-vii.0\lib.mcc.e70" + c_gammalibdir = "\\path\to\mc2\3.2.2\libraries\endfb-vii.0\lib.gamma.e70" / $material t_composition(:,1) = U235_7 "U235AB" 1.10000E-03 873.000 ! Fuel diff --git a/armi/nuclearDataIO/tests/test_xsLibraries.py b/armi/nuclearDataIO/tests/test_xsLibraries.py index 5b326d8d4..b52bf1f15 100644 --- a/armi/nuclearDataIO/tests/test_xsLibraries.py +++ b/armi/nuclearDataIO/tests/test_xsLibraries.py @@ -175,7 +175,9 @@ def createTestXSLibraryFiles(cachePath): shutil.move("ISOTXS.merged", GAMISO_LUMPED) -class TempFileMixin: # really a test case... +class TempFileMixin: + """really a test case""" + @property def testFileName(self): return os.path.join( diff --git a/armi/nuclearDataIO/xsLibraries.py b/armi/nuclearDataIO/xsLibraries.py index a634b40e1..c9be299df 100644 --- a/armi/nuclearDataIO/xsLibraries.py +++ b/armi/nuclearDataIO/xsLibraries.py @@ -406,7 +406,7 @@ def get(self, nuclideLabel, default): def getNuclide(self, nucName, suffix): """ - Get a nuclide object from the XS library or None. + Get a nuclide object from the XS library. Parameters ---------- @@ -420,8 +420,8 @@ def getNuclide(self, nucName, suffix): nuclide : Nuclide object A nuclide from the library or None """ - libLabel = nuclideBases.byName[nucName].label + suffix + try: return self[libLabel] except KeyError: @@ -512,7 +512,6 @@ def getScatterWeights(self, scatterMatrixKey="elasticScatter"): -------- _buildScatterWeights """ - if not self._scatterWeights.get(scatterMatrixKey): self._scatterWeights[scatterMatrixKey] = self._buildScatterWeights( scatterMatrixKey @@ -557,7 +556,6 @@ def plotNucXs( """ generates a XS plot for a nuclide on the ISOTXS library - nucName : str or list The nuclides to plot xsName : str or list @@ -583,7 +581,6 @@ def plotNucXs( armi.nucDirectory.nuclide.plotScatterMatrix """ - # convert all input to lists if isinstance(nucNames, str): nucNames = [nucNames] diff --git a/armi/operators/__init__.py b/armi/operators/__init__.py index 8f06bf824..d3ebd94df 100644 --- a/armi/operators/__init__.py +++ b/armi/operators/__init__.py @@ -39,7 +39,8 @@ math on the reactor model """ -import armi +from armi import context +from armi import getPluginManagerOrFail from armi import runLog from armi.operators.runTypes import RunTypes from armi.operators.operator import Operator @@ -71,7 +72,7 @@ def getOperatorClassFromSettings(cs): # pylint: disable=too-many-return-stateme runType = cs["runType"] if runType == RunTypes.STANDARD: - if armi.MPI_SIZE == 1: + if context.MPI_SIZE == 1: return Operator else: return OperatorMPI @@ -80,9 +81,7 @@ def getOperatorClassFromSettings(cs): # pylint: disable=too-many-return-stateme return OperatorSnapshots plugInOperator = None - for ( - potentialOperator - ) in armi.getPluginManagerOrFail().hook.getOperatorClassFromRunType( + for potentialOperator in getPluginManagerOrFail().hook.getOperatorClassFromRunType( runType=runType ): if plugInOperator: diff --git a/armi/operators/operator.py b/armi/operators/operator.py index ce9a64f11..254c45d19 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -26,12 +26,11 @@ :id: IMPL_EVOLVING_STATE_0 :links: REQ_EVOLVING_STATE """ -import time -import shutil -import re import os +import re +import shutil +import time -import armi from armi import context from armi import interfaces from armi import runLog @@ -206,7 +205,7 @@ def initializeInterfaces(self, r): with self.timer.getTimer("Interface Creation"): self.createInterfaces() self._processInterfaceDependencies() - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: runLog.header("=========== Interface Stack Summary ===========") runLog.info(reportingUtils.getInterfaceStackSummary(self)) self.interactAllInit() diff --git a/armi/operators/operatorMPI.py b/armi/operators/operatorMPI.py index e915c4b22..1100142f1 100644 --- a/armi/operators/operatorMPI.py +++ b/armi/operators/operatorMPI.py @@ -31,20 +31,21 @@ tasks are on the same machine. Everything goes through MPI. This can be optimized as needed. """ -import time -import re -import os import gc +import os +import re +import time import traceback -import armi -from armi.operators.operator import Operator +from armi import context +from armi import getPluginManager from armi import mpiActions from armi import runLog +from armi import settings from armi.bookkeeping import memoryProfiler -from armi.reactor import reactors +from armi.operators.operator import Operator from armi.reactor import assemblies -from armi import settings +from armi.reactor import reactors class OperatorMPI(Operator): @@ -56,8 +57,8 @@ def __init__(self, cs): except: # kill the workers too so everything dies. runLog.important("Master node failed on init. Quitting.") - if armi.MPI_COMM: # else it's a single cpu case. - armi.MPI_COMM.bcast("quit", root=0) + if context.MPI_COMM: # else it's a single cpu case. + context.MPI_COMM.bcast("quit", root=0) raise def operate(self): @@ -68,7 +69,7 @@ def operate(self): handles errors. """ runLog.debug("OperatorMPI.operate") - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: # this is the master try: # run the regular old operate function @@ -80,15 +81,15 @@ def operate(self): ) raise finally: - if armi.MPI_SIZE > 0: + if context.MPI_SIZE > 0: runLog.important( "Stopping all MPI worker nodes and cleaning temps." ) - armi.MPI_COMM.bcast( + context.MPI_COMM.bcast( "quit", root=0 ) # send the quit command to the workers. runLog.debug("Waiting for all nodes to close down") - armi.MPI_COMM.bcast( + context.MPI_COMM.bcast( "finished", root=0 ) # wait until they're done cleaning up. runLog.important("All worker nodes stopped.") @@ -138,8 +139,8 @@ def workerOperate(self): """ while True: # sit around waiting for a command from the master - runLog.extra("Node {0} ready and waiting".format(armi.MPI_RANK)) - cmd = armi.MPI_COMM.bcast(None, root=0) + runLog.extra("Node {0} ready and waiting".format(context.MPI_RANK)) + cmd = context.MPI_COMM.bcast(None, root=0) runLog.extra("worker received command {0}".format(cmd)) # got a command. go use it. if isinstance(cmd, mpiActions.MpiAction): @@ -156,7 +157,7 @@ def workerOperate(self): elif cmd == "sync": # wait around for a sync runLog.debug("Worker syncing") - note = armi.MPI_COMM.bcast("wait", root=0) + note = context.MPI_COMM.bcast("wait", root=0) if note != "wait": raise RuntimeError('did not get "wait". Got {0}'.format(note)) else: @@ -168,7 +169,7 @@ def workerOperate(self): if handled: break if not handled: - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: print("Interfaces" + str(self.interfaces)) runLog.error( "No interface understood worker command {0}\n check stdout for err\n" @@ -185,7 +186,7 @@ def workerOperate(self): cmd ) ) - pm = armi.getPluginManager() + pm = getPluginManager() resetFlags = pm.hook.mpiActionRequiresReset(cmd=cmd) # only reset if all the plugins agree to reset if all(resetFlags): @@ -209,7 +210,6 @@ def _resetWorker(self): .. warning:: This should build empty non-core systems too. """ - xsGroups = self.getInterface("xsGroups") if xsGroups: xsGroups.clearRepresentativeBlocks() @@ -228,7 +228,7 @@ def workerQuit(): runLog.debug("Worker ending") runLog.close() # no more messages. # wait until all workers are closed so we can delete them. - armi.MPI_COMM.bcast("finished", root=0) + context.MPI_COMM.bcast("finished", root=0) def collapseAllStderrs(self): """Takes all the individual stderr files from each processor and arranges them nicely into one file""" diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py index 3d4b32ec8..ded352472 100644 --- a/armi/operators/settingsValidation.py +++ b/armi/operators/settingsValidation.py @@ -23,7 +23,8 @@ """ import os -import armi +from armi import context +from armi import getPluginManagerOrFail from armi import runLog, settings, utils from armi.utils import pathTools from armi.utils.mathematics import expandRepeatedFloats @@ -90,7 +91,7 @@ def isCorrective(self): def resolve(self): """Standard i/o prompt for resolution of an individual query""" - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: return if self.condition(): @@ -166,7 +167,7 @@ def __init__(self, cs): # Gather and attach validators from all plugins # This runs on all registered plugins, not just active ones. - pluginQueries = armi.getPluginManagerOrFail().hook.defineSettingsValidators( + pluginQueries = getPluginManagerOrFail().hook.defineSettingsValidators( inspector=self ) for queries in pluginQueries: @@ -186,7 +187,7 @@ def run(self, cs=None): RuntimeError When a programming error causes queries to loop. """ - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: return False # the following attribute changes will alter what the queries investigate when @@ -312,13 +313,6 @@ def _inspectSettings(self): # import here to avoid cyclic issues from armi import operators - self.addQuery( - lambda: self.cs.path.endswith(".xml"), - "Your settings were loaded from a XML file. These are being converted to yaml files.", - "Would you like to auto-convert it to YAML?", - lambda: settings.convertSettingsFromXMLToYaml(self.cs), - ) - self.addQueryBadLocationWillLikelyFail("operatorLocation") self.addQuery( diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index be3885b57..0e6041cea 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -17,7 +17,6 @@ # pylint: disable=abstract-method,no-self-use,unused-argument import unittest -import armi from armi import settings from armi.interfaces import Interface from armi.reactor.tests import test_reactors @@ -58,7 +57,6 @@ def test_addInterfaceSubclassCollision(self): # 2) Now we have B which is a subclass of A, # we want to not add A (but also not have an error) - o.addInterface(interfaceA) self.assertEqual(o.getInterface("Second"), interfaceB) self.assertEqual(o.getInterface("First"), None) diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 9a730b49d..6d1b6ba3a 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -1453,11 +1453,6 @@ def _swapFluxParam(self, incoming, outgoing): ---------- incoming, outgoing : Assembly Assembly objects to be swapped - - Notes - ----- - Assumes assemblies have the same block structure. If not, blocks will be swapped one-for-one until - the shortest one has been used up and then the process will truncate. """ # Find the block-based mesh points for each assembly meshIn = self.r.core.findAllAxialMeshPoints([incoming], False) diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index 3b9a6653d..d21ee06c6 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -23,6 +23,8 @@ import os import unittest +import numpy as np + from armi.physics.fuelCycle import fuelHandlers from armi.physics.fuelCycle import settings from armi.reactor import assemblies @@ -473,12 +475,55 @@ def test_simpleAssemblyRotation(self): fh.simpleAssemblyRotation() self.assertEqual(b.getRotationNum(), rotNum + 2) + def test_linPowByPin(self): + fh = fuelHandlers.FuelHandler(self.o) + hist = self.o.getInterface("history") + newSettings = {"assemblyRotationStationary": True} + self.o.cs = self.o.cs.modified(newSettings=newSettings) + assem = self.o.r.core.getFirstAssembly(Flags.FUEL) + b = assem.getBlocks(Flags.FUEL)[0] + + b.p.linPowByPin = [1, 2, 3] + self.assertEqual(type(b.p.linPowByPin), np.ndarray) + + b.p.linPowByPin = np.array([1, 2, 3]) + self.assertEqual(type(b.p.linPowByPin), np.ndarray) + + def test_linPowByPinNeutron(self): + fh = fuelHandlers.FuelHandler(self.o) + hist = self.o.getInterface("history") + newSettings = {"assemblyRotationStationary": True} + self.o.cs = self.o.cs.modified(newSettings=newSettings) + assem = self.o.r.core.getFirstAssembly(Flags.FUEL) + b = assem.getBlocks(Flags.FUEL)[0] + + b.p.linPowByPinNeutron = [1, 2, 3] + self.assertEqual(type(b.p.linPowByPinNeutron), np.ndarray) + + b.p.linPowByPinNeutron = np.array([1, 2, 3]) + self.assertEqual(type(b.p.linPowByPinNeutron), np.ndarray) + + def test_linPowByPinGamma(self): + fh = fuelHandlers.FuelHandler(self.o) + hist = self.o.getInterface("history") + newSettings = {"assemblyRotationStationary": True} + self.o.cs = self.o.cs.modified(newSettings=newSettings) + assem = self.o.r.core.getFirstAssembly(Flags.FUEL) + b = assem.getBlocks(Flags.FUEL)[0] + + b.p.linPowByPinGamma = [1, 2, 3] + self.assertEqual(type(b.p.linPowByPinGamma), np.ndarray) + + b.p.linPowByPinGamma = np.array([1, 2, 3]) + self.assertEqual(type(b.p.linPowByPinGamma), np.ndarray) + def test_buReducingAssemblyRotation(self): fh = fuelHandlers.FuelHandler(self.o) hist = self.o.getInterface("history") newSettings = {"assemblyRotationStationary": True} self.o.cs = self.o.cs.modified(newSettings=newSettings) assem = self.o.r.core.getFirstAssembly(Flags.FUEL) + # apply dummy pin-level data to allow intelligent rotation for b in assem.getBlocks(Flags.FUEL): b.breakFuelComponentsIntoIndividuals() @@ -486,6 +531,7 @@ def test_buReducingAssemblyRotation(self): b.p.percentBuMaxPinLocation = 10 b.p.percentBuMax = 5 b.p.linPowByPin = list(reversed(range(b.getNumPins()))) + addSomeDetailAssemblies(hist, [assem]) rotNum = b.getRotationNum() fh.buReducingAssemblyRotation() diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 3df5cac32..ba13e06ce 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -41,7 +41,6 @@ representativeBlockList = csm.representativeBlocks.values() blockThatRepresentsBA = csm.representativeBlocks['BA'] - The class diagram is provided in `xsgm-class-diagram`_ .. _xsgm-class-diagram: @@ -53,21 +52,21 @@ Class inheritance diagram for :py:mod:`crossSectionGroupManager`. """ +import collections import copy import os import shutil import string -import collections import numpy -import armi -from armi import runLog +from armi import context from armi import interfaces -from armi.reactor.flags import Flags +from armi import runLog +from armi.physics.neutronics.const import CONF_CROSS_SECTION from armi.reactor.components import basicShapes +from armi.reactor.flags import Flags from armi.utils.units import TRACE_NUMBER_DENSITY -from armi.physics.neutronics.const import CONF_CROSS_SECTION ORDER = interfaces.STACK_ORDER.BEFORE + interfaces.STACK_ORDER.FUEL_MANAGEMENT @@ -731,7 +730,7 @@ def xsTypeIsPregenerated(self, xsID): def _copyPregeneratedXSFile(self, xsID): # stop a race condition to copy files between all processors - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: return for xsFileLocation, xsFileName in self._getPregeneratedXsFileLocationData(xsID): diff --git a/armi/physics/neutronics/diffIsotxs.py b/armi/physics/neutronics/diffIsotxs.py index 31ac3f27e..cd3e161d0 100644 --- a/armi/physics/neutronics/diffIsotxs.py +++ b/armi/physics/neutronics/diffIsotxs.py @@ -59,35 +59,7 @@ def invoke(self): runLog.setVerbosity(0) refIsotxs = isotxs.readBinary(self.args.reference) - # flux = read_broad_group_flux(refIsotxs.neutronEnergyGroups) + for fname in self.args.comparisonFiles: cmpIsotxs = isotxs.readBinary(fname) - xsLibraries.compare(refIsotxs, cmpIsotxs) # , flux) - - -# This commented out code is being retained, because, at one point, it worked. It is valuable for reducing cross sections to -# 1-group condensed cross sections, which are easier to grasp. -# def read_broad_group_flux(energy_groups): -# flux = [] -# try: -# flux_pattern = re.compile(r'^\s*\d+ +(?P{0}) +{0} +(?P{0})' -# .format(SCIENTIFIC_PATTERN)) -# energy_iter = iter(energy_groups) -# with open(args.flux_file) as flux_bg: -# for line in flux_bg: -# mm = flux_pattern.match(line) -# if mm: -# flux.append(float(mm.group('flux'))) -# energy = float(mm.group('energy')) -# iso_energy = next(energy_iter) -# ratio = energy / iso_energy -# if 1.001 < ratio < 0.999: -# runLog.warning('Flux energy does not match ISOTXS (ratio:{}).\nISOTXS: {}\n{}: {}' -# .format(ratio, iso_energy, args.flux_file, energy)) -# if len(flux) == len(energy_groups): -# break -# runLog.important('Read flux from {}:\n{}' -# .format(args.flux_file, flux)) -# except (TypeError, StopIteration): -# pass -# return numpy.array(flux) + xsLibraries.compare(refIsotxs, cmpIsotxs) diff --git a/armi/physics/neutronics/fissionProductModel/tests/test_lumpedFissionProduct.py b/armi/physics/neutronics/fissionProductModel/tests/test_lumpedFissionProduct.py index 7d1c42777..ffa2f038e 100644 --- a/armi/physics/neutronics/fissionProductModel/tests/test_lumpedFissionProduct.py +++ b/armi/physics/neutronics/fissionProductModel/tests/test_lumpedFissionProduct.py @@ -50,7 +50,7 @@ class TestFissionProductDefinitionFile(unittest.TestCase): def setUp(self): self.fpd = getDummyLFPFile() - def testCreateLFPs(self): + def test_createLFPs(self): """Test of the fission product model creation""" lfps = self.fpd.createLFPsFromFile() xe135 = nuclideBases.fromName("XE135") @@ -59,7 +59,7 @@ def testCreateLFPs(self): for lfp in lfps.values(): self.assertIn(xe135, lfp) - def testCreateReferenceLFPs(self): + def test_createReferenceLFPs(self): """Test of the reference fission product model creation""" with open(REFERENCE_LUMPED_FISSION_PRODUCT_FILE, "r") as LFP_FILE: LFP_TEXT = LFP_FILE.read() diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 10214f6ef..5e0a9b318 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -632,7 +632,17 @@ class DoseResultsMapper(GlobalFluxResultMapper): """ Updates fluence and dpa when time shifts. - Often called after a depletion step. + Often called after a depletion step. It is invoked using :py:meth:`apply() <.DoseResultsMapper.apply>`. + + Parameters + ---------- + depletionSeconds: float, required + Length of depletion step in units of seconds + + options: GlobalFluxOptions, required + Object describing options used by the global flux solver. A few attributes from + this object are used to run the methods in DoseResultsMapper. An example + attribute is aclpDoseLimit. Notes ----- @@ -647,14 +657,31 @@ def __init__(self, depletionSeconds, options): self.r = None self.depletionSeconds = depletionSeconds - def apply(self, reactor): + def apply(self, reactor, blockList=None): + """ + Invokes :py:meth:`updateFluenceAndDpa() <.DoseResultsMapper.updateFluenceAndDpa>` + for a provided Reactor object. + + Parameters + ---------- + reactor: Reactor, required + ARMI Reactor object + + blockList: list, optional + List of ARMI blocks to be processed by the class. If no blocks are provided, then + blocks returned by :py:meth:`getBlocks() <.reactors.Core.getBlocks>` are used. + + Returns + ------- + None + """ runLog.extra("Updating fluence and dpa on reactor based on depletion step.") self.r = reactor - self.updateFluenceAndDpa(self.depletionSeconds) + self.updateFluenceAndDpa(self.depletionSeconds, blockList=blockList) def updateFluenceAndDpa(self, stepTimeInSeconds, blockList=None): r""" - updates the fast fluence and the DPA of the blocklist + Updates the fast fluence and the DPA of the blocklist The dpa rate from the previous timestep is used to compute the dpa here. @@ -663,7 +690,8 @@ def updateFluenceAndDpa(self, stepTimeInSeconds, blockList=None): * detailedDpaPeak: The peak dpa of a block, considering axial and radial peaking The peaking is based either on a user-provided peaking factor (computed in a pin reconstructed rotation study) or the nodal flux peaking factors - * dpaPeakFromFluence: fast fluence * fluence conversion factor (old and inaccurate). Used to be dpaPeak + * dpaPeakFromFluence: fast fluence * fluence conversion factor (old and inaccurate). + Used to be dpaPeak Parameters ---------- diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 30b312a5a..f3d97efe1 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -172,11 +172,14 @@ def test_mapper(self): mapper.updateDpaRate() block = r.core.getFirstBlock() self.assertGreater(block.p.detailedDpaRate, 0) - self.assertEqual(block.p.detailedDpa, 0) + + # Test DoseResultsMapper. Pass in full list of blocks to apply() in order + # to exercise blockList option (does not change behavior, since this is what + # apply() does anyway) opts = globalFluxInterface.GlobalFluxOptions("test") dosemapper = globalFluxInterface.DoseResultsMapper(1000, opts) - dosemapper.apply(r) + dosemapper.apply(r, blockList=r.core.getBlocks()) self.assertGreater(block.p.detailedDpa, 0) mapper.clearFlux() diff --git a/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py b/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py index 407b954a1..a84c5fa3b 100644 --- a/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py +++ b/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py @@ -116,7 +116,7 @@ def run(self): comm = MPI.COMM_SELF.Spawn(sys.executable,args=['cpi.py'],maxprocs=5) """ - return NotImplementedError + raise NotImplementedError def makeXsecTable( diff --git a/armi/physics/neutronics/macroXSGenerationInterface.py b/armi/physics/neutronics/macroXSGenerationInterface.py index b7b2acd64..194dbad17 100644 --- a/armi/physics/neutronics/macroXSGenerationInterface.py +++ b/armi/physics/neutronics/macroXSGenerationInterface.py @@ -20,12 +20,12 @@ \Sigma_i = N_i \sigma_i """ -import armi -from armi import runLog +from armi import context from armi import interfaces from armi import mpiActions -from armi.utils import iterables +from armi import runLog from armi.nuclearDataIO import xsCollections +from armi.utils import iterables class MacroXSGenerator(mpiActions.MpiAction): @@ -54,7 +54,7 @@ def invokeHook(self): # logic here gets messy due to all the default arguments in the calling # method. There exists a large number of permutations to be handled. - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: allBlocks = self.blocks if allBlocks is None: allBlocks = self.r.core.getBlocks() @@ -69,10 +69,10 @@ def invokeHook(self): self.buildScatterMatrix, self.buildOnlyCoolant ) - if armi.MPI_SIZE > 1: + if context.MPI_SIZE > 1: myBlocks = _scatterList(allBlocks) - lib = armi.MPI_COMM.bcast(lib, root=0) + lib = context.MPI_COMM.bcast(lib, root=0) myMacros = [ mc.createMacrosFromMicros(lib, b, libType=self.libType) @@ -87,7 +87,7 @@ def invokeHook(self): for b in allBlocks ] - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: for b, macro in zip(allBlocks, allMacros): b.macros = macro @@ -119,7 +119,7 @@ def buildMacros( """ Builds block-level macroscopic cross sections for making diffusion equation matrices. - This will use MPI if armi.MPI_SIZE > 1 + This will use MPI if armi.context.MPI_SIZE > 1 Builds G-vectors of the basic XS ('nGamma','fission','nalph','np','n2n','nd','nt') Builds GxG matrices for scatter matrices @@ -163,15 +163,15 @@ def buildMacros( def _scatterList(lst): - if armi.MPI_RANK == 0: - chunked = iterables.split(lst, armi.MPI_SIZE) + if context.MPI_RANK == 0: + chunked = iterables.split(lst, context.MPI_SIZE) else: chunked = None - return armi.MPI_COMM.scatter(chunked, root=0) + return context.MPI_COMM.scatter(chunked, root=0) def _gatherList(localList): - globalList = armi.MPI_COMM.gather(localList, root=0) - if armi.MPI_RANK == 0: + globalList = context.MPI_COMM.gather(localList, root=0) + if context.MPI_RANK == 0: globalList = iterables.flatten(globalList) return globalList diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index e9e369a3a..3dd396249 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -148,7 +148,6 @@ def makeUnique(self): self.p.assemNum = incrementAssemNum() self.name = self.makeNameFromAssemNum(self.p.assemNum) for bi, b in enumerate(self): - b.makeUnique() b.setName(b.makeName(self.p.assemNum, bi)) @staticmethod diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index ddb7cc34d..0066850a6 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -93,7 +93,6 @@ def __init__(self, name, height=1.0, location=None): `getVolume` assumes unit height. """ composites.Composite.__init__(self, name) - self.makeUnique() self.p.height = height if location: @@ -219,21 +218,6 @@ def makeName(self, assemNum, axialIndex): self.p.assemNum = assemNum return "B{0:04d}-{1:03d}".format(assemNum, axialIndex) - def makeUnique(self): - """ - Assign a unique id (integer value) for each block. - - This should be called whenever creating a block that is intended to be treated - as a unique object. For example, if you were to broadcast or pickle a block it - should have the same ID across all nodes. Likewise, if you deepcopy a block for - a temporary purpose to it should have the same ID. However, ARMI's assembly - construction also uses deepcopy, and in order to keep that functionality, this - method needs to be called after creating a fresh assembly (from deepcopy). - """ - - self.p.id = self.__class__.uniqID - self.__class__.uniqID += 1 - def getSmearDensity(self, cold=True): """ Compute the smear density of pins in this block. diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index 47360d240..dbf4cae63 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -37,7 +37,7 @@ import tabulate import yamlize -import armi +from armi import context from armi import runLog from armi.reactor import geometry from armi.reactor import grids @@ -131,7 +131,7 @@ def construct(self, cs, bp, reactor, geom=None): self.origin.x, self.origin.y, self.origin.z, None ) system.spatialLocator = spatialLocator - if armi.MPI_RANK != 0: + if context.MPI_RANK != 0: # on non-master nodes we don't bother building up the assemblies # because they will be populated with DistributeState. return None diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py index 077c6ce55..a58ac5b5d 100644 --- a/armi/reactor/components/__init__.py +++ b/armi/reactor/components/__init__.py @@ -118,9 +118,13 @@ class UnshapedComponent(Component): A component with undefined dimensions. Useful for situations where you just want to enter the area directly. - For instance, in filler situations where the exact shape of this component is - is unknown but you have some left-over space between other components filled - with a known material you might need to model. + + For instance, when you want to model neutronic behavior of an assembly based + on only knowing the area fractions of each material in the assembly. + + See Also + -------- + DerivedShape : Useful to just fill leftover space in a block with a material """ pDefs = componentParameters.getUnshapedParameterDefinitions() @@ -159,11 +163,11 @@ def getComponentArea(self, cold=False): cold : bool, optional Ignored for this component """ - return self.p.area + coldArea = self.p.area + if cold: + return coldArea - def setArea(self, val): - self.p.area = val - self.clearCache() + return self.getThermalExpansionFactor(self.temperatureInC) ** 2 * coldArea def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): """ @@ -171,8 +175,12 @@ def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): This is the smallest it can possibly be. Since this is used to determine the outer component, it will never be allowed to be the outer one. + + Notes + ----- + Tc is not used in this method for this particular component. """ - return math.sqrt(self.p.area / math.pi) + return 2 * math.sqrt(self.getComponentArea(cold=cold) / math.pi) @staticmethod def fromComponent(otherComponent): @@ -180,11 +188,16 @@ def fromComponent(otherComponent): Build a new UnshapedComponent that has area equal to that of another component. This can be used to "freeze" a DerivedShape, among other things. + + Notes + ----- + Components created in this manner will not thermally expand beyond the expanded + area of the original component, but will retain their hot temperature. """ newC = UnshapedComponent( name=otherComponent.name, material=otherComponent.material, - Tinput=otherComponent.inputTemperatureInC, + Tinput=otherComponent.temperatureInC, Thot=otherComponent.temperatureInC, area=otherComponent.getComponentArea(), ) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index bcfa44a3e..43bf24014 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -29,9 +29,9 @@ See Also: :doc:`/developer/index`. """ -import math import collections import itertools +import math import timeit from typing import Dict, Optional, Type, Tuple, List, Union @@ -39,20 +39,19 @@ import tabulate import six -import armi -from armi.reactor import parameters -from armi.reactor.parameters import resolveCollections -from armi.reactor.flags import Flags, TypeSpec +from armi import context from armi import runLog from armi import utils -from armi.utils import units -from armi.utils import densityTools -from armi.nucDirectory import nucDir, nuclideBases from armi.nucDirectory import elements -from armi.reactor import grids - -from armi.physics.neutronics.fissionProductModel import fissionProductModel +from armi.nucDirectory import nucDir, nuclideBases from armi.nuclearDataIO import xsCollections +from armi.physics.neutronics.fissionProductModel import fissionProductModel +from armi.reactor import grids +from armi.reactor import parameters +from armi.reactor.flags import Flags, TypeSpec +from armi.reactor.parameters import resolveCollections +from armi.utils import densityTools +from armi.utils import units from armi.utils.densityTools import calculateNumberDensity @@ -523,7 +522,7 @@ def getChildren(self, deep=False, generationNum=1, includeMaterials=False): def getChildrenWithFlags(self, typeSpec: TypeSpec, exactMatch=True): """Get all children that have given flags.""" - return NotImplementedError + raise NotImplementedError def getComponents(self, typeSpec: TypeSpec = None, exact=False): """ @@ -2866,7 +2865,7 @@ def syncMpiState(self): int number of parameters synchronized over all components """ - if armi.MPI_SIZE == 1: + if context.MPI_SIZE == 1: return 0 startTime = timeit.default_timer() @@ -2876,9 +2875,9 @@ def syncMpiState(self): runLog.debug("syncMpiState has {} comps".format(len(allComps))) try: - armi.MPI_COMM.barrier() # sync up + context.MPI_COMM.barrier() # sync up allGatherTime = -timeit.default_timer() - allSyncData = armi.MPI_COMM.allgather(sendBuf) + allSyncData = context.MPI_COMM.allgather(sendBuf) allGatherTime += timeit.default_timer() except: msg = ["Failure while trying to allgather."] diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index 3bdb4e68b..88057af92 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -146,7 +146,7 @@ def tearDown(self): del self.cs del self.r - def testConvert(self): + def test_convert(self): converterSettings = { "radialConversionType": "Ring Compositions", "axialConversionType": "Axial Coordinates", diff --git a/armi/reactor/reactorParameters.py b/armi/reactor/reactorParameters.py index 012edf8cc..d6c192af8 100644 --- a/armi/reactor/reactorParameters.py +++ b/armi/reactor/reactorParameters.py @@ -49,8 +49,8 @@ def defineReactorParameters(): pb.defParam( "cycleLength", - units="EFP days", - description="The cycle length of the reactor while power is being produced", + units="days", + description="Length of the cycle, including outage time described by availabilityFactor", ) pb.defParam( diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index a7acfccae..2e6a183c9 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -945,44 +945,6 @@ def buildCircularRingDictionary(self, ringPitch=1.0): return circularRingDict - def getPowerProductionMassFromFissionProducts(self): - """ - Determines the power produced by Pu isotopes and Uranium isotopes by examining - the fission products in the block - - The percentage of energy released adjusted mass produced by each LFP can be used to - determine the relative power production of each parent isotope. - - Returns - ------- - resultsEnergyCorrected : list of tuples - Contains the nuclide name, energy released adjusted mass - """ - # get the energy in Joules from the ISOTXS - energyDict = {} - nuclides = ["U235", "U238", "PU239", "PU240", "PU241"] - fissionProducts = ["LFP35", "LFP38", "LFP39", "LFP40", "LFP41"] - - # initialize the energy in each nuclide - totEnergy = 0.0 - for nuc in nuclides: - n = self.lib.getNuclide(nuc) - energyDict[nuc] = n.isotxsMetadata["efiss"] - totEnergy += n.isotxsMetadata["efiss"] - - fissPower = {} - for b in self.getBlocks(Flags.FUEL): - for nuc, lfp in zip(nuclides, fissionProducts): - energy = fissPower.get(nuc, 0.0) - energy += b.getMass(lfp) * energyDict[nuc] - fissPower[nuc] = energy - - resultsEnergyCorrected = [] - # scale the energy mass by energy to get the corrected energy mass of each isotope - for nuc in nuclides: - resultsEnergyCorrected.append(fissPower[nuc] / totEnergy) - return zip(nuclides, resultsEnergyCorrected) - def _getAssembliesByName(self): """ If the assembly name-to-assembly object map is deleted or out of date, then this will @@ -2152,9 +2114,11 @@ def growToFullCore(self, cs): Geometry converter used to do the conversion. """ - import armi.reactor.converters.geometryConverters as gc + from armi.reactor.converters.geometryConverters import ( + ThirdCoreHexToFullCoreChanger, + ) - converter = gc.ThirdCoreHexToFullCoreChanger(cs) + converter = ThirdCoreHexToFullCoreChanger(cs) converter.convert(self.r) return converter diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index ceef09898..f7f8cc203 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -41,7 +41,9 @@ from armi.tests import TEST_ROOT from armi.utils import directoryChangers from armi.utils import textProcessors -import armi.reactor.tests.test_reactors +from armi.reactor.tests import test_reactors +from armi.reactor.assemblies import getAssemNum +from armi.reactor.assemblies import resetAssemNumCounter NUM_BLOCKS = 3 @@ -277,9 +279,9 @@ def setUp(self): self.Assembly.calculateZCoords() def test_resetAssemNumCounter(self): - armi.reactor.assemblies.resetAssemNumCounter() + resetAssemNumCounter() cur = 0 - ref = armi.reactor.assemblies._assemNum + ref = getAssemNum() self.assertEqual(cur, ref) def test_iter(self): @@ -1178,7 +1180,7 @@ def setUpClass(cls): pass def setUp(self): - self.o, self.r = armi.reactor.tests.test_reactors.loadTestReactor(TEST_ROOT) + self.o, self.r = test_reactors.loadTestReactor(TEST_ROOT) def test_snapAxialMeshToReferenceConservingMassBasedOnBlockIgniter(self): originalMesh = [25.0, 50.0, 75.0, 100.0, 175.0] diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index c10cad6f8..b1ba35f31 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -469,7 +469,7 @@ def test_getXsType(self): ref = "BB" self.assertEqual(cur, ref) - def test27b_setBuGroup(self): + def test_27b_setBuGroup(self): type_ = "A" self.Block.p.buGroup = type_ cur = self.Block.p.buGroupNum @@ -1163,7 +1163,6 @@ def test_getSortedComponentsInsideOfComponentSpecifiedTypes(self): self.assertListEqual(actual, expected) def test_getNumComponents(self): - cur = self.Block.getNumComponents(Flags.FUEL) ref = self.Block.getDim(Flags.FUEL, "mult") self.assertEqual(cur, ref) @@ -1173,7 +1172,6 @@ def test_getNumComponents(self): self.assertEqual(1, self.Block.getNumComponents(Flags.DUCT)) def test_getNumPins(self): - cur = self.Block.getNumPins() ref = self.Block.getDim(Flags.FUEL, "mult") self.assertEqual(cur, ref) @@ -1182,7 +1180,6 @@ def test_getNumPins(self): self.assertEqual(emptyBlock.getNumPins(), 0) def test_setPinPowers(self): - numPins = self.Block.getNumPins() neutronPower = [10.0 * i for i in range(numPins)] gammaPower = [1.0 * i for i in range(numPins)] @@ -1241,13 +1238,13 @@ def calcFracManually(names): self.assertAlmostEqual(ref, val) self.assertNotAlmostEqual(refWrong, val) - def test100_getPinPitch(self): + def test_100_getPinPitch(self): cur = self.Block.getPinPitch() ref = self.Block.getDim(Flags.CLAD, "od") + self.Block.getDim(Flags.WIRE, "od") places = 6 self.assertAlmostEqual(cur, ref, places=places) - def test101_getPitch(self): + def test_101_getPitch(self): cur = self.Block.getPitch(returnComp=True) ref = ( self.Block.getDim(Flags.INTERCOOLANT, "op"), @@ -1263,7 +1260,7 @@ def test101_getPitch(self): self.assertTrue(newb.getLargestComponent("op") is c2) self.assertTrue(p1 == p2) - def test102_setPitch(self): + def test_102_setPitch(self): pitch = 17.5 self.Block.setPitch(pitch) cur = self.Block.getPitch() @@ -1272,8 +1269,7 @@ def test102_setPitch(self): self.Block.getComponent(Flags.INTERCOOLANT).getDimension("op"), pitch ) - def test106_getAreaFractions(self): - + def test_106_getAreaFractions(self): cur = self.Block.getVolumeFractions() tot = 0.0 areas = [] @@ -1612,19 +1608,17 @@ def test_coords(self): def test_getNumPins(self): self.assertEqual(self.HexBlock.getNumPins(), 169) - def testSymmetryFactor(self): - self.HexBlock.spatialLocator = self.HexBlock.r.core.spatialGrid[ - 2, 0, 0 - ] # full hex + def test_symmetryFactor(self): + # full hex + self.HexBlock.spatialLocator = self.HexBlock.r.core.spatialGrid[2, 0, 0] self.HexBlock.clearCache() self.assertEqual(1.0, self.HexBlock.getSymmetryFactor()) a0 = self.HexBlock.getArea() v0 = self.HexBlock.getVolume() m0 = self.HexBlock.getMass() - self.HexBlock.spatialLocator = self.HexBlock.r.core.spatialGrid[ - 0, 0, 0 - ] # 1/3 symmetric + # 1/3 symmetric + self.HexBlock.spatialLocator = self.HexBlock.r.core.spatialGrid[0, 0, 0] self.HexBlock.clearCache() self.assertEqual(3.0, self.HexBlock.getSymmetryFactor()) self.assertEqual(a0 / 3.0, self.HexBlock.getArea()) diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index b0af6000f..167b91e82 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -134,7 +134,13 @@ class TestGeneralComponents(unittest.TestCase): componentMaterial = "HT9" componentDims = {"Tinput": 25.0, "Thot": 25.0} - def setUp(self): + def setUp(self, component=None): + """ + Most of the time nothing will be passed as `component` and the result will + be stored in self, but you can also pass a component object as `component`, + in which case the object will be returned with the `parent` attribute assigned. + """ + class _Parent: def getSymmetryFactor(self): return 1.0 @@ -150,10 +156,14 @@ def getChildren(self): derivedMustUpdate = False - self.component = self.componentCls( - "TestComponent", self.componentMaterial, **self.componentDims - ) - self.component.parent = _Parent() + if component == None: + self.component = self.componentCls( + "TestComponent", self.componentMaterial, **self.componentDims + ) + self.component.parent = _Parent() + else: + component.parent = _Parent() + return component class TestComponent(TestGeneralComponents): @@ -189,14 +199,59 @@ def test_getDimension(self): class TestUnshapedComponent(TestGeneralComponents): componentCls = UnshapedComponent - componentMaterial = "Material" + componentMaterial = "HT9" componentDims = {"Tinput": 25.0, "Thot": 430.0, "area": math.pi} + def test_getComponentArea(self): + # a case without thermal expansion + self.assertEqual(self.component.getComponentArea(cold=True), math.pi) + + # a case with thermal expansion + self.assertEqual( + self.component.getComponentArea(cold=False), + math.pi + * self.component.getThermalExpansionFactor(self.component.temperatureInC) + ** 2, + ) + + # show that area expansion is consistent with the density change in the material + hotDensity = self.component.density() + hotArea = self.component.getArea() + thermalExpansionFactor = self.component.getThermalExpansionFactor( + self.component.temperatureInC + ) + + coldComponent = self.setUp( + UnshapedComponent( + name="coldComponent", + material=self.componentMaterial, + Tinput=self.component.inputTemperatureInC, + Thot=self.component.inputTemperatureInC, + area=math.pi, + ) + ) + coldDensity = coldComponent.density() + coldArea = coldComponent.getArea() + + self.assertGreater(thermalExpansionFactor, 1) + self.assertAlmostEqual( + (coldDensity / hotDensity) / (thermalExpansionFactor * hotArea / coldArea), + 1, + ) # account for density being 3D while area is 2D + def test_getBoundingCircleOuterDiameter(self): - self.assertEqual(self.component.getBoundingCircleOuterDiameter(cold=True), 1.0) + # a case without thermal expansion + self.assertEqual(self.component.getBoundingCircleOuterDiameter(cold=True), 2.0) + + # a case with thermal expansion + self.assertEqual( + self.component.getBoundingCircleOuterDiameter(cold=False), + 2.0 + * self.component.getThermalExpansionFactor(self.component.temperatureInC), + ) def test_fromComponent(self): - circle = components.Circle("testCircle", "Material", 25, 25, 1.0) + circle = components.Circle("testCircle", "HT9", 25, 500, 1.0) unshaped = components.UnshapedComponent.fromComponent(circle) self.assertEqual(circle.getComponentArea(), unshaped.getComponentArea()) diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 1f4887b9a..3844d4982 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -91,7 +91,7 @@ def setUp(self): container.add(nested) self.container = container - def testComposite(self): + def test_Composite(self): container = self.container children = container.getChildren() @@ -101,10 +101,10 @@ def testComposite(self): allChildren = container.getChildren(deep=True) self.assertEqual(len(allChildren), 8) - def testIterComponents(self): + def test_iterComponents(self): self.assertIn(self.thirdGen, list(self.container.iterComponents())) - def testGetChildren(self): + def test_getChildren(self): # There are 5 leaves and 1 composite in container. The composite has one leaf. firstGen = self.container.getChildren() self.assertEqual(len(firstGen), 6) @@ -145,6 +145,7 @@ def test_nameContains(self): self.assertTrue(c.nameContains("One")) self.assertTrue(c.nameContains("THREE")) self.assertFalse(c.nameContains("nope")) + self.assertFalse(c.nameContains(["nope"])) self.assertTrue(c.nameContains(["one", "TWO", "three"])) self.assertTrue(c.nameContains(["nope", "dope", "three"])) @@ -200,6 +201,18 @@ def test_getBoundingCirlceOuterDiameter(self): od = self.container.getBoundingCircleOuterDiameter() self.assertAlmostEqual(od, len(list(self.container.iterComponents()))) + def test_getParamNames(self): + params = self.container.getParamNames() + self.assertEqual(len(params), 3) + self.assertIn("flags", params) + self.assertIn("serialNum", params) + self.assertIn("type", params) + + def test_updateVolume(self): + self.assertAlmostEqual(self.container.getVolume(), 0) + self.container._updateVolume() + self.assertAlmostEqual(self.container.getVolume(), 0) + class TestCompositeTree(unittest.TestCase): @@ -294,6 +307,20 @@ def test_ordering(self): otherBlock.spatialLocator = locator self.assertTrue(otherBlock < self.Block) + def test_summing(self): + a = assemblies.Assembly("dummy") + a.spatialGrid = grids.axialUnitGrid(2, armiObject=a) + otherBlock = deepcopy(self.Block) + a.add(self.Block) + a.add(otherBlock) + + b = self.Block + otherBlock + self.assertEqual(len(b), 26) + self.assertFalse(b[0].is3D) + self.assertIn("Circle", str(b[0])) + self.assertFalse(b[-1].is3D) + self.assertIn("Hexagon", str(b[-1])) + def test_constituentReport(self): runLog.info(self.r.core.constituentReport()) runLog.info(self.r.core.getFirstAssembly().constituentReport()) @@ -536,5 +563,4 @@ def test_copyParamsFrom(self): if __name__ == "__main__": - # import sys;sys.argv = ['', 'TestCompositeTree.test_ordering'] unittest.main() diff --git a/armi/reactor/tests/test_geometry.py b/armi/reactor/tests/test_geometry.py index cb308ad4b..1d2803264 100644 --- a/armi/reactor/tests/test_geometry.py +++ b/armi/reactor/tests/test_geometry.py @@ -66,7 +66,7 @@ class TestGeomType(unittest.TestCase): - def testFromStr(self): + def test_fromStr(self): # note the bonkers case and extra whitespace to exercise the canonicalization self.assertEqual(geometry.GeomType.fromStr("HeX"), geometry.GeomType.HEX) self.assertEqual( @@ -78,7 +78,7 @@ def testFromStr(self): with self.assertRaises(ValueError): geometry.GeomType.fromStr("what even is this?") - def testLabel(self): + def test_label(self): gt = geometry.GeomType.fromStr("hex") self.assertEqual(gt.label, "Hexagonal") gt = geometry.GeomType.fromStr("cartesian") @@ -88,13 +88,13 @@ def testLabel(self): gt = geometry.GeomType.fromStr("thetarz") self.assertEqual(gt.label, "R-Z-Theta") - def testStr(self): + def test_str(self): for geom in {geometry.HEX, geometry.CARTESIAN, geometry.RZ, geometry.RZT}: self.assertEqual(str(geometry.GeomType.fromStr(geom)), geom) class TestSymmetryType(unittest.TestCase): - def testFromStr(self): + def test_fromStr(self): # note the bonkers case and extra whitespace to exercise the canonicalization self.assertEqual( geometry.SymmetryType.fromStr("thiRd periodic ").domain, @@ -107,7 +107,7 @@ def testFromStr(self): with self.assertRaises(ValueError): geometry.SymmetryType.fromStr("what even is this?") - def testFromAny(self): + def test_fromAny(self): st = geometry.SymmetryType.fromAny("eighth reflective through center assembly") self.assertTrue(st.isThroughCenterAssembly) self.assertEqual(st.domain, geometry.DomainType.EIGHTH_CORE) @@ -125,7 +125,7 @@ def testFromAny(self): self.assertEqual(newST.domain, geometry.DomainType.EIGHTH_CORE) self.assertEqual(newST.boundary, geometry.BoundaryType.REFLECTIVE) - def testBaseConstructor(self): + def test_baseConstructor(self): self.assertEqual( geometry.SymmetryType( geometry.DomainType.SIXTEENTH_CORE, geometry.BoundaryType.REFLECTIVE @@ -141,7 +141,7 @@ def testBaseConstructor(self): "", ) - def testLabel(self): + def test_label(self): st = geometry.SymmetryType( geometry.DomainType.FULL_CORE, geometry.BoundaryType.NO_SYMMETRY ) @@ -166,7 +166,7 @@ def testLabel(self): ) self.assertEqual(st.domain.label, "Sixteenth") - def testSymmetryFactor(self): + def test_SymmetryFactor(self): st = geometry.SymmetryType( geometry.DomainType.FULL_CORE, geometry.BoundaryType.NO_SYMMETRY ) @@ -225,7 +225,7 @@ def test_checkValidGeomSymmetryCombo(self): class TestSystemLayoutInput(unittest.TestCase): - def testReadHexGeomXML(self): + def test_readHexGeomXML(self): geom = SystemLayoutInput() geom.readGeomFromFile(os.path.join(TEST_ROOT, "geom.xml")) self.assertEqual(str(geom.geomType), geometry.HEX) @@ -234,7 +234,7 @@ def testReadHexGeomXML(self): geom.writeGeom(out) os.remove(out) - def testReadReactor(self): + def test_readReactor(self): reactor = test_reactors.buildOperatorOfEmptyHexBlocks().r reactor.core.symmetry = geometry.SymmetryType( geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC @@ -294,7 +294,7 @@ class TestSystemLayoutInputTRZ(unittest.TestCase): because it can't auto-compute the area. """ - def testReadTRZGeomXML(self): + def test_readTRZGeomXML(self): geom = SystemLayoutInput() geom.readGeomFromFile(os.path.join(TEST_ROOT, "trz_geom.xml")) self.assertEqual(str(geom.geomType), geometry.RZT) diff --git a/armi/reactor/tests/test_grids.py b/armi/reactor/tests/test_grids.py index 0a89ba7c9..1285e41b4 100644 --- a/armi/reactor/tests/test_grids.py +++ b/armi/reactor/tests/test_grids.py @@ -137,7 +137,7 @@ def test_recursionPin(self): class TestGrid(unittest.TestCase): - def testBasicPosition(self): + def test_basicPosition(self): """ Ensure a basic Cartesian grid works as expected. @@ -152,12 +152,12 @@ def testBasicPosition(self): assert_allclose(grid.getCoordinates((0, 0, -1)), (0, 0, -1)) assert_allclose(grid.getCoordinates((1, 0, 0)), (1, 0, 0)) - def testNeighbors(self): + def test_neighbors(self): grid = grids.Grid(unitSteps=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))) neighbs = grid.getNeighboringCellIndices(0, 0, 0) self.assertEqual(len(neighbs), 4) - def testLabel(self): + def test_label(self): grid = grids.Grid(unitSteps=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))) self.assertEqual(grid.getLabel((1, 1, 2)), "001-001-002") @@ -201,7 +201,7 @@ class TestHexGrid(unittest.TestCase): :link: REQ_REACTOR_MESH """ - def testPositions(self): + def test_positions(self): grid = grids.HexGrid.fromPitch(1.0) side = 1.0 / math.sqrt(3) assert_allclose(grid.getCoordinates((0, 0, 0)), (0.0, 0.0, 0.0)) @@ -223,13 +223,13 @@ def testPositions(self): assert_allclose(grid.getCoordinates((1, 0, 0)), iDirection) assert_allclose(grid.getCoordinates((0, 1, 0)), jDirection) - def testNeighbors(self): + def test_neighbors(self): grid = grids.HexGrid.fromPitch(1.0) neighbs = grid.getNeighboringCellIndices(0, 0, 0) self.assertEqual(len(neighbs), 6) self.assertIn((1, -1, 0), neighbs) - def testRingPosFromIndices(self): + def test_ringPosFromIndices(self): """Test conversion from<-->to ring/position based on hand-prepared right answers.""" grid = grids.HexGrid.fromPitch(1.0) for indices, ringPos in [ @@ -251,7 +251,7 @@ def testRingPosFromIndices(self): self.assertEqual(indices, grid.getIndicesFromRingAndPos(*ringPos)) self.assertEqual(ringPos, grid.getRingPos(indices)) - def testLabel(self): + def test_label(self): grid = grids.HexGrid.fromPitch(1.0) indices = grid.getIndicesFromRingAndPos(12, 5) label1 = grid.getLabel(indices) @@ -387,15 +387,15 @@ def test_isInFirstThird(self): class TestBoundsDefinedGrid(unittest.TestCase): - def testPositions(self): + def test_positions(self): grid = grids.Grid(bounds=([0, 1, 2, 3, 4], [0, 10, 20, 50], [0, 20, 60, 90])) assert_allclose(grid.getCoordinates((1, 1, 1)), (1.5, 15.0, 40.0)) - def testBase(self): + def test_base(self): grid = grids.Grid(bounds=([0, 1, 2, 3, 4], [0, 10, 20, 50], [0, 20, 60, 90])) assert_allclose(grid.getCellBase((1, 1, 1)), (1.0, 10.0, 20.0)) - def testPositionsMixedDefinition(self): + def test_positionsMixedDefinition(self): grid = grids.Grid( unitSteps=((1.0, 0.0), (0.0, 1.0)), bounds=(None, None, [0, 20, 60, 90]) ) @@ -415,7 +415,7 @@ class TestThetaRZGrid(unittest.TestCase): :link: REQ_REACTOR_MESH """ - def testPositions(self): + def test_positions(self): grid = grids.ThetaRZGrid( bounds=(numpy.linspace(0, 2 * math.pi, 13), [0, 2, 2.5, 3], [0, 10, 20, 30]) ) @@ -438,7 +438,7 @@ class TestCartesianGrid(unittest.TestCase): :link: REQ_REACTOR_MESH """ - def testRingPosNoSplit(self): + def test_ringPosNoSplit(self): grid = grids.CartesianGrid.fromRectangle(1.0, 1.0, isOffset=True) expectedRing = [ @@ -466,7 +466,7 @@ def testRingPosNoSplit(self): self.assertEqual(ring, expectedRing[j + 3][i + 3]) self.assertEqual(pos, expectedPos[j + 3][i + 3]) - def testRingPosSplit(self): + def test_ringPosSplit(self): grid = grids.CartesianGrid.fromRectangle(1.0, 1.0) expectedRing = [ @@ -496,7 +496,7 @@ def testRingPosSplit(self): self.assertEqual(ring, expectedRing[j + 3][i + 3]) self.assertEqual(pos, expectedPos[j + 3][i + 3]) - def testSymmetry(self): + def test_symmetry(self): # PERIODIC, no split grid = grids.CartesianGrid.fromRectangle( 1.0, diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 7c1b313d5..29664ecf4 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -13,10 +13,11 @@ # limitations under the License. """ tests of the Parameters class """ # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access -import unittest +import copy import traceback +import unittest -import armi +from armi import context from armi.reactor import parameters from armi.reactor import composites @@ -182,8 +183,69 @@ def nPlus1(self, value): self.assertEqual(22, mock.nPlus1) self.assertTrue(all(pd.assigned for pd in mock.paramDefs)) - def test_cannotDefineParameterWithSameName(self): + def test_setterGetterBasics(self): + class Mock(parameters.ParameterCollection): + pDefs = parameters.ParameterDefinitionCollection() + with pDefs.createBuilder() as pb: + + def n(self, value): + self._p_n = value + self._p_nPlus1 = value + 1 + + pb.defParam("n", "units", "description", "location", setter=n) + def nPlus1(self, value): + self._p_nPlus1 = value + self._p_n = value - 1 + + pb.defParam("nPlus1", "units", "description", "location", setter=nPlus1) + + mock = Mock() + mock.n = 15 + mock.nPlus1 = 22 + + # basic tests of setters and getters + self.assertEqual(mock["n"], 21) + self.assertEqual(mock["nPlus1"], 22) + with self.assertRaises(parameters.exceptions.UnknownParameterError): + _ = mock["fake"] + with self.assertRaises(KeyError): + _ = mock[123] + + # basic test of __delitem__ method + del mock["n"] + with self.assertRaises(parameters.exceptions.UnknownParameterError): + _ = mock["n"] + + # basic tests of __in__ method + self.assertFalse("n" in mock) + self.assertTrue("nPlus1" in mock) + + # basic tests of __eq__ method + mock2 = copy.deepcopy(mock) + self.assertTrue(mock == mock) + self.assertFalse(mock == mock2) + + # basic tests of get() method + self.assertEqual(mock.get("nPlus1"), 22) + self.assertIsNone(mock.get("fake")) + self.assertEqual(mock.get("fake", default=333), 333) + + # basic test of values() method + vals = mock.values() + self.assertEqual(len(vals), 2) + self.assertEqual(vals[0], 22) + + # basic test of update() method + mock.update({"nPlus1": 100}) + self.assertEqual(mock.get("nPlus1"), 100) + + # basic test of getSyncData() method + data = mock.getSyncData() + self.assertEqual(data["n"], 99) + self.assertEqual(data["nPlus1"], 100) + + def test_cannotDefineParameterWithSameName(self): with self.assertRaises(parameters.ParameterDefinitionError): class MockParamCollection(parameters.ParameterCollection): @@ -387,7 +449,7 @@ def setUp(self): self.r = makeComp("reactor") self.r.core = makeComp("core") self.r.add(self.r.core) - for ai in range(armi.MPI_SIZE * 4): + for ai in range(context.MPI_SIZE * 4): a = makeComp("assembly{}".format(ai)) self.r.core.add(a) for bi in range(10): @@ -400,7 +462,7 @@ def tearDown(self): del self.r def run(self, testNamePrefix="mpitest_"): - with open("mpitest{}.temp".format(armi.MPI_RANK), "w") as self.l: + with open("mpitest{}.temp".format(context.MPI_RANK), "w") as self.l: for methodName in sorted(dir(self)): if methodName.startswith(testNamePrefix): self.write("{}.{}".format(self.__class__.__name__, methodName)) @@ -449,51 +511,49 @@ def assertNotEqual(self, expected, actual): def mpitest_noConflicts(self): for ci, comp in enumerate(self.comps): - if ci % armi.MPI_SIZE == armi.MPI_RANK: - comp.p.param1 = (armi.MPI_RANK + 1) * 30.0 + if ci % context.MPI_SIZE == context.MPI_RANK: + comp.p.param1 = (context.MPI_RANK + 1) * 30.0 else: - self.assertNotEqual((armi.MPI_RANK + 1) * 30.0, comp.p.param1) + self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param1) - # numUpdates = len(self.comps) // armi.MPI_SIZE + (len(self.comps) % armi.MPI_SIZE > armi.MPI_RANK) self.assertEqual(len(self.comps), self.r.syncMpiState()) for ci, comp in enumerate(self.comps): - self.assertEqual((ci % armi.MPI_SIZE + 1) * 30.0, comp.p.param1) + self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param1) def mpitest_noConflicts_setByString(self): """Make sure params set by string also work with sync.""" for ci, comp in enumerate(self.comps): - if ci % armi.MPI_SIZE == armi.MPI_RANK: - comp.p.param2 = (armi.MPI_RANK + 1) * 30.0 + if ci % context.MPI_SIZE == context.MPI_RANK: + comp.p.param2 = (context.MPI_RANK + 1) * 30.0 else: - self.assertNotEqual((armi.MPI_RANK + 1) * 30.0, comp.p.param2) + self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param2) - # numUpdates = len(self.comps) // armi.MPI_SIZE + (len(self.comps) % armi.MPI_SIZE > armi.MPI_RANK) self.assertEqual(len(self.comps), self.r.syncMpiState()) for ci, comp in enumerate(self.comps): - self.assertEqual((ci % armi.MPI_SIZE + 1) * 30.0, comp.p.param2) + self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param2) def mpitest_withConflicts(self): - self.r.core.p.param1 = (armi.MPI_RANK + 1) * 99.0 + self.r.core.p.param1 = (context.MPI_RANK + 1) * 99.0 with self.assertRaises(ValueError): self.r.syncMpiState() def mpitest_withConflictsButSameValue(self): - self.r.core.p.param1 = (armi.MPI_SIZE + 1) * 99.0 + self.r.core.p.param1 = (context.MPI_SIZE + 1) * 99.0 self.r.syncMpiState() - self.assertEqual((armi.MPI_SIZE + 1) * 99.0, self.r.core.p.param1) + self.assertEqual((context.MPI_SIZE + 1) * 99.0, self.r.core.p.param1) def mpitest_noConflictsMaintainWithStateRetainer(self): assigned = [] with self.r.retainState(parameters.inCategory("cat1")): for ci, comp in enumerate(self.comps): comp.p.param2 = 99 * ci - if ci % armi.MPI_SIZE == armi.MPI_RANK: - comp.p.param1 = (armi.MPI_RANK + 1) * 30.0 + if ci % context.MPI_SIZE == context.MPI_RANK: + comp.p.param1 = (context.MPI_RANK + 1) * 30.0 assigned.append(parameters.SINCE_ANYTHING) else: - self.assertNotEqual((armi.MPI_RANK + 1) * 30.0, comp.p.param1) + self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param1) assigned.append(parameters.NEVER) # 1st inside state retainer @@ -508,12 +568,12 @@ def mpitest_noConflictsMaintainWithStateRetainer(self): self.assertEqual(len(self.comps), self.r.syncMpiState()) for ci, comp in enumerate(self.comps): - self.assertEqual((ci % armi.MPI_SIZE + 1) * 30.0, comp.p.param1) + self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param1) def mpitest_conflictsMaintainWithStateRetainer(self): with self.r.retainState(parameters.inCategory("cat2")): for _, comp in enumerate(self.comps): - comp.p.param2 = 99 * armi.MPI_RANK + comp.p.param2 = 99 * context.MPI_RANK with self.assertRaises(ValueError): self.r.syncMpiState() @@ -528,15 +588,15 @@ def do(): self.r.p.param3 = "hi" for c in self.comps: c.p.param1 = ( - 99 * armi.MPI_RANK + 99 * context.MPI_RANK ) # this will get reset after state retainer - a = self.r.core[passNum * armi.MPI_SIZE + armi.MPI_RANK] - a.p.param2 = armi.MPI_RANK * 20.0 + a = self.r.core[passNum * context.MPI_SIZE + context.MPI_RANK] + a.p.param2 = context.MPI_RANK * 20.0 for b in a: - b.p.param2 = armi.MPI_RANK * 10.0 + b.p.param2 = context.MPI_RANK * 10.0 for ai, a2 in enumerate(self.r): - if ai % armi.MPI_SIZE != armi.MPI_RANK: + if ai % context.MPI_SIZE != context.MPI_RANK: assert "param2" not in a2.p self.assertEqual(parameters.SINCE_ANYTHING, param1.assigned) @@ -579,8 +639,8 @@ def do(): def do_assert(passNum): # ensure all assemblies and blocks set values for param2, but param1 is empty - for rank in range(armi.MPI_SIZE): - a = self.r.core[passNum * armi.MPI_SIZE + rank] + for rank in range(context.MPI_SIZE): + a = self.r.core[passNum * context.MPI_SIZE + rank] assert "param1" not in a.p assert "param3" not in a.p self.assertEqual(rank * 20, a.p.param2) @@ -589,20 +649,20 @@ def do_assert(passNum): assert "param1" not in b.p assert "param3" not in b.p - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: with self.r.retainState(parameters.inCategory("cat2")): - armi.MPI_COMM.bcast(self.r) + context.MPI_COMM.bcast(self.r) do() [do_assert(passNum) for passNum in range(4)] [do_assert(passNum) for passNum in range(4)] else: del self.r - self.r = armi.MPI_COMM.bcast(None) + self.r = context.MPI_COMM.bcast(None) do() if __name__ == "__main__": - if armi.MPI_SIZE == 1: + if context.MPI_SIZE == 1: unittest.main() else: SynchronizationTests().run() diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 1c630d081..f41873be5 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -136,13 +136,16 @@ def loadTestReactor( Parameters ---------- inputFilePath : str - Path to the directory of the armiRun.yaml input file. + Path to the directory of the input file. customSettings : dict with str keys and values of any type For each key in customSettings, the cs which is loaded from the armiRun.yaml will be overwritten to the value given in customSettings for that key. + inputFileName : str, default="armiRun.yaml" + Name of the input file to run. + Returns ------- o : Operator @@ -219,7 +222,7 @@ class HexReactorTests(ReactorTests): def setUp(self): self.o, self.r = loadTestReactor(self.directoryChanger.destination) - def testGetTotalParam(self): + def test_getTotalParam(self): # verify that the block params are being read. val = self.r.core.getTotalBlockParam("power") val2 = self.r.core.getTotalBlockParam("power", addSymmetricPositions=True) @@ -750,6 +753,16 @@ def test_createAssemblyOfType(self): ) self.assertAlmostEqual(aNew3.getMass(), bol.getMass()) + def test_getAvgTemp(self): + t0 = self.r.core.getAvgTemp([Flags.CLAD, Flags.WIRE, Flags.DUCT]) + self.assertAlmostEqual(t0, 459.267, delta=0.01) + + t1 = self.r.core.getAvgTemp([Flags.CLAD, Flags.FUEL]) + self.assertAlmostEqual(t1, 545.043, delta=0.01) + + t2 = self.r.core.getAvgTemp([Flags.CLAD, Flags.WIRE, Flags.DUCT, Flags.FUEL]) + self.assertAlmostEqual(t2, 521.95269, delta=0.01) + class CartesianReactorTests(ReactorTests): def setUp(self): diff --git a/armi/reactor/tests/test_zones.py b/armi/reactor/tests/test_zones.py index 7ac55bd2e..1e863809b 100644 --- a/armi/reactor/tests/test_zones.py +++ b/armi/reactor/tests/test_zones.py @@ -15,9 +15,11 @@ """Test for Zones""" # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access import copy -import unittest +import logging import os +import unittest +from armi import runLog from armi.reactor import assemblies from armi.reactor import blueprints from armi.reactor import geometry @@ -27,6 +29,7 @@ from armi.reactor.flags import Flags from armi.reactor.tests import test_reactors from armi.settings.fwSettings import globalSettings +from armi.tests import mockRunLogs THIS_DIR = os.path.dirname(__file__) @@ -376,7 +379,28 @@ def test_createHotZones(self): self.assertEqual(hotCount, 1) self.assertEqual(normalCount, 2) + def test_zoneSummary(self): + o, r = test_reactors.loadTestReactor() + + r.core.buildZones(o.cs) + daZones = r.core.zones + + # make sure we have a couple of zones to test on + for name0 in ["ring-1-radial-shield-5", "ring-1-feed-fuel-5"]: + self.assertIn(name0, daZones.names) + + with mockRunLogs.BufferLog() as mock: + runLog.LOG.startLog("test_zoneSummary") + runLog.LOG.setVerbosity(logging.INFO) + + self.assertEqual("", mock._outputStream) + + daZones.summary() + + self.assertIn("Zone Summary", mock._outputStream) + self.assertIn("Zone Power", mock._outputStream) + self.assertIn("Zone Average Flow", mock._outputStream) + if __name__ == "__main__": - # import sys;sys.argv = ['', 'Zones_InReactor.test_buildRingZones'] unittest.main() diff --git a/armi/scripts/migration/__init__.py b/armi/scripts/migration/__init__.py index e36450f11..46b21d76b 100644 --- a/armi/scripts/migration/__init__.py +++ b/armi/scripts/migration/__init__.py @@ -33,7 +33,6 @@ """ from . import ( - m0_1_0_settings, m0_1_3, m0_1_0_newDbFormat, crossSectionBlueprintsToSettings, @@ -41,7 +40,6 @@ ) ACTIVE_MIGRATIONS = [ - m0_1_0_settings.ConvertXmlSettingsToYaml, m0_1_0_newDbFormat.ConvertDB2toDB3, m0_1_3.RemoveCentersFromBlueprints, m0_1_3.UpdateElementalNuclides, diff --git a/armi/scripts/migration/m0_1_0_settings.py b/armi/scripts/migration/m0_1_0_settings.py deleted file mode 100644 index 1203b5f9f..000000000 --- a/armi/scripts/migration/m0_1_0_settings.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2019 TerraPower, LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Migrate ARMI settings from old XML to new yaml.""" -import io - -from armi import runLog -from armi.settings import caseSettings -from armi.scripts.migration.base import SettingsMigration -from armi.settings import settingsIO - - -class ConvertXmlSettingsToYaml(SettingsMigration): - """Convert XML settings to YAML settings""" - - fromVersion = "0.0.0" - toVersion = "0.0.0" - - def _applyToStream(self): - """Convert stream to yaml stream""" - cs = caseSettings.Settings() - reader = settingsIO.SettingsReader(cs) - reader.readFromStream(self.stream) - - if reader.invalidSettings: - runLog.info( - "The following deprecated settings will be deleted:\n * {}" - "".format("\n * ".join(list(reader.invalidSettings))) - ) - - cs = _modify_settings(cs) - writer = settingsIO.SettingsWriter(cs) - newStream = io.StringIO() - writer.writeYaml(newStream) - newStream.seek(0) - return newStream - - def _writeNewFile(self, newStream): - if self.path.endswith(".xml"): - self.path = self.path.replace(".xml", ".yaml") - SettingsMigration._writeNewFile(self, newStream) - - -def _modify_settings(cs): - if cs["runType"] == "Rx. Coeffs": - runLog.info( - "Converting deprecated Rx. Coeffs ``runType` setting to Snapshots. " - "You may need to manually disable modules you don't want to run" - ) - cs = cs.modified(newSettings={"runType": "Snapshots"}) - - return cs diff --git a/armi/scripts/migration/tests/test_m0_1_6_locationLabels.py b/armi/scripts/migration/tests/test_m0_1_6_locationLabels.py index 336399081..7a1df309e 100644 --- a/armi/scripts/migration/tests/test_m0_1_6_locationLabels.py +++ b/armi/scripts/migration/tests/test_m0_1_6_locationLabels.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Test Locationlabel migration""" -import unittest import io +import unittest from armi.settings import caseSettings from armi.scripts.migration.m0_1_6_locationLabels import ( @@ -23,7 +23,7 @@ class TestMigration(unittest.TestCase): - def testLocationLabelMigration(self): + def test_locationLabelMigration(self): """Make a setting with an old value and make sure it migrates to expected new value.""" cs = caseSettings.Settings() newSettings = {"detailAssemLocationsBOL": ["B1012"]} @@ -42,4 +42,4 @@ def testLocationLabelMigration(self): if __name__ == "__main__": - unittest.main() + nittest.main() diff --git a/armi/settings/__init__.py b/armi/settings/__init__.py index 42fe4c3fe..a1c41a572 100644 --- a/armi/settings/__init__.py +++ b/armi/settings/__init__.py @@ -136,12 +136,11 @@ def promptForSettingsFile(choice=None): Parameters ---------- choice : int, optional - The item in the list of valid XML files to load - + The item in the list of valid YAML files to load """ runLog.info("Welcome to the ARMI Loader") runLog.info("Scanning for ARMI settings files...") - files = sorted(glob.glob("*.yaml") + glob.glob("*.xml")) # phase out xml later + files = sorted(glob.glob("*.yaml")) if not files: runLog.info( "No eligible settings files found. Creating settings without choice" @@ -185,33 +184,3 @@ def setMasterCs(cs): """ Settings.instance = cs runLog.debug("Master cs set to {} with ID: {}".format(cs, id(cs))) - - -def convertSettingsFromXMLToYaml(cs): - if not cs.path.endswith(".xml"): - raise ValueError("Can only convert XML files") - - old = cs.path - oldCopy = old + "-converted" - newNameBase, _ext = os.path.splitext(old) - newName = newNameBase + ".yaml" - counter = 0 - while os.path.exists(newName): - # don't overwrite anything - newName = "{}{}.yaml".format(newNameBase, counter) - counter += 1 - if counter: - runLog.warning( - "{} already exists in YAML format; writing {} instead".format( - newNameBase, newName - ) - ) - - runLog.info( - "Converting {} to new YAML format. Old copy will remain intact as {}".format( - old, oldCopy - ) - ) - cs.writeToYamlFile(newName) - cs.path = newName - shutil.move(old, oldCopy) diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index 679fb2477..0b40a4b44 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -19,7 +19,7 @@ power level, the input file names, the number of cycles to run, the run type, the environment setup, and hundreds of other things. -A settings object can be saved as or loaded from an XML file. The ARMI GUI is designed to +A settings object can be saved as or loaded from an YAML file. The ARMI GUI is designed to create this settings file, which is then loaded by an ARMI process on the cluster. A master case settings is created as ``masterCs`` @@ -30,7 +30,7 @@ import os from copy import copy, deepcopy -import armi +from armi import context from armi import runLog from armi.settings import settingsIO from armi.settings.setting import Setting @@ -80,9 +80,11 @@ def __init__(self, fName=None): provided by the user on the command line. Therefore, _failOnLoad is used to prevent this from happening. """ + from armi import getApp # pylint: disable=import-outside-toplevel + self.path = "" - app = armi.getApp() + app = getApp() assert app is not None self.__settings = app.getSettings() if not Settings.instance: @@ -170,7 +172,9 @@ def __setstate__(self, state): -------- armi.settings.setting.Setting.__getstate__ : removes schema """ - self.__settings = armi.getApp().getSettings() + from armi import getApp # pylint: disable=import-outside-toplevel + + self.__settings = getApp().getSettings() # restore non-setting instance attrs for key, val in state.items(): @@ -219,11 +223,7 @@ def failOnLoad(self): def loadFromInputFile(self, fName, handleInvalids=True, setPath=True): """ - Read in settings from an input file. - - Supports YAML and two XML formats, the newer (tags are the key, etc.) - and the former (tags are the type, etc.). If the extension is ``xml``, - it assumes XML format. Otherwise, YAML is assumed. + Read in settings from an input YAML file. Passes the reader back out in case you want to know something about how the reading went like for knowing if a file contained deprecated settings, etc. @@ -248,10 +248,7 @@ def _prepToRead(self, fName): return settingsIO.SettingsReader(self), path def loadFromString(self, string, handleInvalids=True): - """Read in settings from a string. - - Supports two xml formats, the newer (tags are the key, etc.) and the former - (tags are the type, etc.) + """Read in settings from a YAML string. Passes the reader back out in case you want to know something about how the reading went like for knowing if a file contained deprecated settings, etc. @@ -265,8 +262,6 @@ def loadFromString(self, string, handleInvalids=True): reader = settingsIO.SettingsReader(self) fmt = reader.SettingsInputFormat.YAML - if string.strip()[0] == "<": - fmt = reader.SettingsInputFormat.XML reader.readFromStream( io.StringIO(string), handleInvalids=handleInvalids, fmt=fmt ) @@ -281,33 +276,20 @@ def _applyReadSettings(self, path=None): if path: self.path = path # can't set this before a chance to fail occurs - # TODO: At some point, much of the logging init will be moved to context, including this. def initLogVerbosity(self): - """Central location to init logging verbosity""" - if armi.MPI_RANK == 0: + """ + Central location to init logging verbosity + + NOTE: This means that creating a Settings object sets the global logging + level of the entire code base. + """ + if context.MPI_RANK == 0: runLog.setVerbosity(self["verbosity"]) else: runLog.setVerbosity(self["branchVerbosity"]) self.setModuleVerbosities(force=True) - def writeToXMLFile(self, fName, style="short"): - """Write out settings to an xml file - - Parameters - ---------- - fName : str - the file to write to - style : str - the method of XML output to be used when creating the xml file for - the current state of settings - """ - self.path = pathTools.armiAbsPath(fName) - writer = settingsIO.SettingsWriter(self, style=style) - with open(self.path, "w") as stream: - writer.writeXml(stream) - return writer - def writeToYamlFile(self, fName, style="short"): """ Write settings to a yaml file. diff --git a/armi/settings/setting.py b/armi/settings/setting.py index dd1fd6805..51ad1ebf1 100644 --- a/armi/settings/setting.py +++ b/armi/settings/setting.py @@ -24,12 +24,6 @@ One reason for complexity of the previous settings implementation was good interoperability with the GUI widgets. - -We originally thought putting settings definitions in XML files would -help with future internationalization. This is not the case. -Internationalization will likely be added later with string interpolators given -the desire to internationalize, which is nicely compatible with this -code-based re-implementation. """ import copy @@ -163,9 +157,7 @@ def _setSchema(self, schema): elif self.options and self.enforcedOptions: self.schema = vol.Schema(vol.In(self.options)) else: - # Coercion is needed to convert XML-read migrations (for old cases) - # as well as in some GUI instances where lists are getting set - # as strings. + # Coercion is needed in some GUI instances where lists are getting set as strings. if isinstance(self.default, list) and self.default: # Non-empty default: assume the default has the desired contained type # Coerce all values to the first entry in the default so mixed floats and ints work. diff --git a/armi/settings/settingsIO.py b/armi/settings/settingsIO.py index fbf0f7461..9bb1874cc 100644 --- a/armi/settings/settingsIO.py +++ b/armi/settings/settingsIO.py @@ -25,7 +25,6 @@ from typing import Dict, Tuple, Set import sys import warnings -import xml.etree.ElementTree as ET from ruamel.yaml import YAML import ruamel.yaml.comments @@ -148,16 +147,15 @@ class SettingsReader: """ class SettingsInputFormat(enum.Enum): - XML = enum.auto() YAML = enum.auto() + # TODO: Is this method still necessary? @classmethod def fromExt(cls, ext): - return {".xml": cls.XML, ".yaml": cls.YAML}[ext] + return {".yaml": cls.YAML}[ext] def __init__(self, cs): self.cs = cs - self.rootTag = Roots.CUSTOM self.format = self.SettingsInputFormat.YAML self.inputPath = "" @@ -181,11 +179,6 @@ def __getattr__(self, attr): def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self.inputPath) - @property - def isXmlFormat(self): - """True if file read is in the old XML format.""" - return self.format == self.SettingsInputFormat.XML - def readFromFile(self, path, handleInvalids=True): """Load file and read it.""" @@ -204,60 +197,13 @@ def readFromStream(self, stream, handleInvalids=True, fmt=SettingsInputFormat.YA """Read from a file-like stream.""" self.format = fmt if self.format == self.SettingsInputFormat.YAML: - try: - self._readYaml(stream) - except ruamel.yaml.scanner.ScannerError: - # mediocre way to detect xml vs. yaml at the stream level - runLog.info( - "Could not read stream in YAML format. Attempting XML format." - ) - self.format = self.SettingsInputFormat.XML - stream.seek(0) - - if self.format == self.SettingsInputFormat.XML: - self._readXml(stream) + self._readYaml(stream) if handleInvalids: self._checkInvalidSettings() - def _readXml(self, stream): - """ - Read user settings from XML stream. - """ - warnings.warn( - "Loading from XML-format settings files is being deprecated.", - DeprecationWarning, - ) - tree = ET.parse(stream) - settingRoot = tree.getroot() - if Roots.VERSION in settingRoot.attrib: - self.inputVersion = settingRoot.attrib[Roots.VERSION] - - if settingRoot.tag != self.rootTag: - # checks to make sure the right kind of settings XML file - # is being applied to the right class - if settingRoot.tag == systemLayoutInput.SystemLayoutInput.ROOT_TAG: - customMsg = ( - "\nSettings file appears to be a reactor geometry file. " - "Please provide a valid settings file." - ) - else: - customMsg = '\nRoot tag "{}" does not match expected value "{}"'.format( - settingRoot.tag, self.rootTag - ) - raise InvalidSettingsFileError(self.inputPath, customMsgEnd=customMsg) - - for settingElement in list(settingRoot): - self._interpretXmlSetting(settingElement) - def _readYaml(self, stream): - """ - Read settings from a YAML stream. - - Notes - ----- - This is intended to replace the XML stuff as we converge on consistent input formats. - """ + """Read settings from a YAML stream.""" from armi.physics.thermalHydraulics import const # avoid circular import yaml = YAML() @@ -296,44 +242,6 @@ def _checkInvalidSettings(self): else: runLog.warning("Ignoring invalid settings: {}".format(invalidNames)) - def _interpretXmlSetting(self, settingElement): - settingName = settingElement.tag - attributes = settingElement.attrib - if settingName in self.settingsAlreadyRead: - raise SettingException( - "The setting {} has been specified more than once in {}. Adjust input." - "".format(settingName, self.inputPath) - ) - # add here, before it gets converted by name cleaning below. - self.settingsAlreadyRead.add(settingName) - if settingName in settingsRules.OLD_TAGS: - # name cleaning - settingName = settingElement.attrib["key"].replace(" ", "") - values = list(settingElement) - if not values: - attributes = {"type": settingsRules.OLD_TAGS[settingElement.tag]} - if "val" in settingElement.attrib: - attributes["value"] = settingElement.attrib["val"] - else: - # means this item has no children and no value, no reason for it to exist. - return - else: - attributes["value"] = [ - subElement.attrib["val"] for subElement in values - ] - attributes["containedType"] = settingsRules.OLD_TAGS[ - settingElement.attrib["type"] - ] - - elif "value" not in attributes: - raise SettingException( - "No value supplied for the setting {} in {}".format( - settingName, self.inputPath - ) - ) - - self._applySettings(settingName, attributes["value"]) - def _applySettings(self, name, val): nameToSet, _wasRenamed = self._renamer.renameSetting(name) settingsToApply = self.applyConversions(nameToSet, val) @@ -343,12 +251,9 @@ def _applySettings(self, name, val): else: # apply validations settingObj = self.cs.getSetting(settingName) - if value: - value = applyTypeConversions(settingObj, value) - # The value is automatically coerced into the - # expected type when set using either the default or - # user-defined schema + # The value is automatically coerced into the expected type + # when set using either the default or user-defined schema self.cs[settingName] = value def applyConversions(self, name, value): @@ -371,18 +276,6 @@ def applyConversions(self, name, value): return settingsToApply -def applyTypeConversions(settingObj, value): - """ - Coerce value to proper type given a valid setting object. - - Useful in converting XML settings with no type info (all string) as well as - in GUI operations. - """ - if settingObj.underlyingType == list and not isinstance(value, list): - return ast.literal_eval(value) - return value - - class SettingsWriter: """Writes settings out to files. @@ -412,21 +305,6 @@ def _getVersion(): tag, attrib = Roots.CUSTOM, {Roots.VERSION: version} return tag, attrib - def writeXml(self, stream): - """Write settings to XML file.""" - settingData = self._getSettingDataToWrite() - tag, attrib = self._getVersion() - root = ET.Element(tag, attrib=attrib) - tree = ET.ElementTree(root) - - for settingObj, settingDatum in settingData.items(): - settingNode = ET.SubElement(root, settingObj.name) - for attribName in settingDatum: - settingNode.set(attribName, str(settingObj.dump())) - - stream.write('\n') - stream.write(self.prettyPrintXmlRecursively(tree.getroot(), spacing=False)) - def writeYaml(self, stream): """Write settings to YAML file.""" settingData = self._getSettingDataToWrite() @@ -478,106 +356,6 @@ def _getSettingDataToWrite(self): settingData[settingObject] = settingDatum return settingData - def prettyPrintXmlRecursively(self, node, indentation=0, spacing=True): - r"""Generates a pretty output string of an element tree better than the default .write() - - Uses helper cleanQuotesFromString to get everything both python and xml readable - - Parameters - ---------- - node : ET.Element - the element tree element to write the output for - indentation : int, - not for manual use, but for the recursion to nicely nest parts of the string - spacing : bool - used to flip the newline behavior for spacing out an xml file or keeping it compact - primarily for the difference between a default settings and a custom settings file. - - """ - if spacing: - spacing = 1 - else: - spacing = 0 - - cleanTag = self.cleanStringForXml(node.tag) - cleanText = self.cleanStringForXml(node.text) - cleanTail = self.cleanStringForXml(node.tail) - - # open the tag - output = "\n" + "\t" * indentation + "<{tag}".format(tag=cleanTag) - indentation += 1 - - # fill in attributes - for key, value in iter(sorted(node.attrib.items())): - cleanKey = self.cleanStringForXml(key) - cleanValue = self.cleanStringForXml(value) - output += ( - "\n" * spacing - + "\t" * indentation * spacing - + " " * ((spacing - 1) * -1) - + '{key}="{value}"'.format(key=cleanKey, value=cleanValue) - ) - - # if there are internal nodes, keep the tag open, otherwise close it immediately - if not node.text and not list(node): # no internal tags - output += " />" + "\n" * spacing - elif node.text and not list(node): # internal text, no children - output += ( - ">" - + "\n" * spacing - + "\t" * indentation - + "{text}\n".format(text=cleanText) - ) - indentation -= 1 - output += "\t" * indentation + "".format(tag=cleanTag) - elif node.text and list(node): # internal text, children - output += ( - ">" - + "\n" * spacing - + "\t" * indentation - + "{text}\n".format(text=cleanText) - ) - for child in list(node): - output += self.prettyPrintXmlRecursively( - child, indentation=indentation, spacing=spacing - ) - indentation -= 1 - output += "\t" * indentation + "".format(tag=cleanTag) - else: # has children, no text - output += ">" + "\n" * spacing - for child in list(node): - output += self.prettyPrintXmlRecursively( - child, indentation=indentation, spacing=spacing - ) - indentation -= 1 - output += "\n" + "\t" * indentation + "".format(tag=cleanTag) - - # add on the tail - if node.tail: - output += "{tail}".format(tail=cleanTail) - - return output - - def cleanStringForXml(self, s): - """Assures no XML entity issues will occur on parsing a string - - A helper function used to make strings xml friendly - XML has some reserved characters, this should handle them. - apostrophes aren't being dealt with but seem to behave nicely as is. - - http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references - """ - if not s: - return "" - - s = ( - s.replace('"', """).replace("<", "<").replace(">", ">") - ) # .replace("'",''') - s = re.sub( - "&(?!quot;|lt;|gt;|amp;|apos;)", "&", s - ) # protects against chaining & - return s - def prompt(statement, question, *options): """Prompt the user for some information.""" @@ -586,17 +364,11 @@ def prompt(statement, question, *options): import wx # pylint: disable=import-error msg = statement + "\n\n\n" + question - if len(msg) < 300: - style = wx.CENTER - for opt in options: - style |= getattr(wx, opt) - dlg = wx.MessageDialog(None, msg, style=style) - else: - # for shame. Might make sense to move the styles stuff back into the - # Framework - from tparmi.gui.styles import dialogues + style = wx.CENTER + for opt in options: + style |= getattr(wx, opt) + dlg = wx.MessageDialog(None, msg, style=style) - dlg = dialogues.ScrolledMessageDialog(None, msg, "Prompt") response = dlg.ShowModal() dlg.Destroy() if response == wx.ID_CANCEL: @@ -623,7 +395,7 @@ def prompt(statement, question, *options): if "NO" in responses: responses.append("N") - # TODO: Using the logger is strange. Perhaps this is a rare use-case for bare print? Or something bespoke. + # Use the logger tools to handle user prompts (runLog supports this). while response not in responses: runLog.LOG.log("prompt", statement) runLog.LOG.log("prompt", "{} ({}): ".format(question, ", ".join(responses))) diff --git a/armi/settings/tests/old_xml_settings_input.xml b/armi/settings/tests/old_xml_settings_input.xml deleted file mode 100644 index 3e8a3f550..000000000 --- a/armi/settings/tests/old_xml_settings_input.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/armi/settings/tests/test_settings.py b/armi/settings/tests/test_settings.py index 64ffdb66b..1b1378e28 100644 --- a/armi/settings/tests/test_settings.py +++ b/armi/settings/tests/test_settings.py @@ -23,21 +23,22 @@ from ruamel.yaml import YAML import voluptuous as vol -import armi +from armi import configure +from armi import getApp +from armi import getPluginManagerOrFail +from armi import plugins +from armi import settings +from armi.operators import settingsValidation from armi.physics.fuelCycle import FuelHandlerPlugin from armi.physics.neutronics import settings as neutronicsSettings -from armi import settings +from armi.reactor.flags import Flags from armi.settings import caseSettings from armi.settings import setting -from armi.operators import settingsValidation from armi.tests import TEST_ROOT -from armi import plugins from armi.utils import directoryChangers -from armi.reactor.flags import Flags from armi.utils.customExceptions import NonexistentSetting THIS_DIR = os.path.dirname(__file__) -TEST_XML = os.path.join(THIS_DIR, "old_xml_settings_input.xml") class DummyPlugin1(plugins.ArmiPlugin): @@ -108,7 +109,7 @@ def tearDown(self): def test_addingOptions(self): # load in the plugin with extra, added options - pm = armi.getPluginManagerOrFail() + pm = getPluginManagerOrFail() pm.register(PluginAddsOptions) # modify the default/text settings YAML file to include neutronicsKernel @@ -128,12 +129,12 @@ class TestSettings2(unittest.TestCase): def setUp(self): # We are going to be messing with the plugin manager, which is global ARMI # state, so we back it up and restore the original when we are done. - self._backupApp = copy.copy(armi._app) + self._backupApp = copy.copy(getApp()) def tearDown(self): - armi._app = self._backupApp + configure(self._backupApp, permissive=True) - def testSchemaChecksType(self): + def test_schemaChecksType(self): newSettings = FuelHandlerPlugin.defineSettings() good_input = io.StringIO( @@ -230,7 +231,7 @@ def test_pluginValidatorsAreDiscovered(self): ) def test_pluginSettings(self): - pm = armi.getPluginManagerOrFail() + pm = getPluginManagerOrFail() pm.register(DummyPlugin1) # We have a setting; this should be fine cs = caseSettings.Settings() @@ -360,15 +361,6 @@ def test_copySettingNotDefault(self): self.assertTrue(hasattr(s2, "schema")) self.assertTrue(hasattr(s2, "_customSchema")) - -class TestSettingsConversion(unittest.TestCase): - """Make sure we can convert from old XML type settings to new Yaml settings.""" - - def test_convert(self): - cs = caseSettings.Settings() - cs.loadFromInputFile(TEST_XML) - self.assertEqual(cs["buGroups"], [3, 10, 20, 100]) - def test_empty(self): cs = caseSettings.Settings() cs = cs.modified(newSettings={"buGroups": []}) diff --git a/armi/settings/tests/test_settingsIO.py b/armi/settings/tests/test_settingsIO.py index 593a62b87..985a58450 100644 --- a/armi/settings/tests/test_settingsIO.py +++ b/armi/settings/tests/test_settingsIO.py @@ -19,12 +19,12 @@ import os import unittest -import armi -from armi.cli import entryPoint -from armi.utils import directoryChangers +from armi import context from armi import settings +from armi.cli import entryPoint from armi.settings import setting from armi.settings import settingsIO +from armi.utils import directoryChangers from armi.utils.customExceptions import ( InvalidSettingsFileError, NonexistentSetting, @@ -40,23 +40,37 @@ def test_settingsObjSetting(self): "idontexist" ] = "this test should fail because no setting named idontexist should exist." - def test_loadFromXmlFailsOnBadNames(self): + def test_loadFromYamlFailsOnBadNames(self): ss = settings.Settings() with self.assertRaises(TypeError): ss.loadFromInputFile(None) with self.assertRaises(IOError): - ss.loadFromInputFile("this-settings-file-does-not-exist.xml") + ss.loadFromInputFile("this-settings-file-does-not-exist.yaml") def test_invalidFile(self): with self.assertRaises(InvalidSettingsFileError): cs = settings.caseSettings.Settings() reader = settingsIO.SettingsReader(cs) reader.readFromStream( - io.StringIO(r"¯\_(ツ)_/¯"), - fmt=settingsIO.SettingsReader.SettingsInputFormat.XML, + io.StringIO("useless:\n should_fail"), + fmt=settingsIO.SettingsReader.SettingsInputFormat.YAML, ) +class SettingsReaderTests(unittest.TestCase): + def setUp(self): + self.cs = settings.caseSettings.Settings() + + def test_basicSettingsReader(self): + reader = settingsIO.SettingsReader(self.cs) + + self.assertEqual(reader["numProcessors"], 1) + self.assertEqual(reader["nCycles"], 1) + + self.assertFalse(getattr(reader, "filelessBP")) + self.assertEqual(getattr(reader, "path"), "") + + class SettingsRenameTests(unittest.TestCase): testSettings = [ setting.Setting( @@ -102,10 +116,7 @@ class SettingsWriterTests(unittest.TestCase): def setUp(self): self.td = directoryChangers.TemporaryDirectoryChanger() self.td.__enter__() - self.init_mode = armi.CURRENT_MODE - self.filepathXml = os.path.join( - os.getcwd(), self._testMethodName + "test_setting_io.xml" - ) + self.init_mode = context.CURRENT_MODE self.filepathYaml = os.path.join( os.getcwd(), self._testMethodName + "test_setting_io.yaml" ) @@ -113,13 +124,13 @@ def setUp(self): self.cs = self.cs.modified(newSettings={"nCycles": 55}) def tearDown(self): - armi.Mode.setMode(self.init_mode) + context.Mode.setMode(self.init_mode) self.td.__exit__(None, None, None) def test_writeShorthand(self): """Setting output as a sparse file""" - self.cs.writeToXMLFile(self.filepathXml, style="short") - self.cs.loadFromInputFile(self.filepathXml) + self.cs.writeToYamlFile(self.filepathYaml, style="short") + self.cs.loadFromInputFile(self.filepathYaml) self.assertEqual(self.cs["nCycles"], 55) def test_writeYaml(self): @@ -127,6 +138,10 @@ def test_writeYaml(self): self.cs.loadFromInputFile(self.filepathYaml) self.assertEqual(self.cs["nCycles"], 55) + def test_errorSettingsWriter(self): + with self.assertRaises(ValueError): + _ = settingsIO.SettingsWriter(self.cs, "wrong") + class MockEntryPoint(entryPoint.EntryPoint): name = "dummy" @@ -149,4 +164,4 @@ def test_cannotLoadSettingsAfterParsingCommandLineSetting(self): self.test_commandLineSetting() with self.assertRaises(RuntimeError): - self.cs.loadFromInputFile("somefile.xml") + self.cs.loadFromInputFile("somefile.yaml") diff --git a/armi/tests/__init__.py b/armi/tests/__init__.py index a4568d6a4..d6f155aa5 100644 --- a/armi/tests/__init__.py +++ b/armi/tests/__init__.py @@ -17,20 +17,19 @@ This package contains some input files that can be used across a wide variety of unit tests in other lower-level subpackages. """ -import os import datetime import itertools -import unittest +import os import re import shutil +import unittest from typing import Optional -import armi +from armi import context from armi import runLog from armi.reactor import geometry -from armi.reactor import reactors from armi.reactor import grids - +from armi.reactor import reactors TEST_ROOT = os.path.dirname(os.path.abspath(__file__)) ARMI_RUN_PATH = os.path.join(TEST_ROOT, "armiRun.yaml") @@ -382,6 +381,6 @@ def rebaselineTextComparisons(root): import sys if len(sys.argv) == 1: - rebaselineTextComparisons(armi.ROOT) + rebaselineTextComparisons(context.ROOT) else: rebaselineTextComparisons(sys.argv[1]) diff --git a/armi/tests/armiRun b/armi/tests/armiRun deleted file mode 100644 index c86557995..000000000 --- a/armi/tests/armiRun +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/armi/tests/test_apps.py b/armi/tests/test_apps.py index ebde3cb9a..04b963f4f 100644 --- a/armi/tests/test_apps.py +++ b/armi/tests/test_apps.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Tests for the App class. -""" - +"""Tests for the App class.""" import copy import unittest -import armi +from armi import cli +from armi import configure +from armi import context +from armi import getApp +from armi import getDefaultPluginManager +from armi import meta from armi import plugins from armi.__main__ import main @@ -34,9 +36,7 @@ def defineParameterRenames(): class TestPlugin2(plugins.ArmiPlugin): - """ - This should lead to an error if it coexists with Plugin1. - """ + """This should lead to an error if it coexists with Plugin1.""" @staticmethod @plugins.HOOKIMPL @@ -55,7 +55,8 @@ def defineParameterRenames(): class TestPlugin4(plugins.ArmiPlugin): """This should be fine on its own, and safe to merge with TestPlugin1. And would - make for a pretty good rename IRL.""" + make for a pretty good rename IRL. + """ @staticmethod @plugins.HOOKIMPL @@ -64,21 +65,22 @@ def defineParameterRenames(): class TestApps(unittest.TestCase): - """ - Test the base apps.App interfaces. - """ + """Test the base apps.App interfaces.""" def setUp(self): """Manipulate the standard App. We can't just configure our own, since the pytest environment bleeds between tests :(""" - self._backupApp = copy.deepcopy(armi._app) + self._backupApp = copy.deepcopy(getApp()) def tearDown(self): """Restore the App to its original state""" + import armi + armi._app = self._backupApp + context.APP_NAME = "armi" def test_getParamRenames(self): - app = armi.getApp() + app = getApp() app.pluginManager.register(TestPlugin1) app.pluginManager.register(TestPlugin4) app._paramRenames = None # need to implement better cache invalidation rules @@ -105,24 +107,57 @@ def test_getParamRenames(self): ): app.getParamRenames() + def test_version(self): + app = getApp() + ver = app.version + self.assertEqual(ver, meta.__version__) + + def test_getSettings(self): + app = getApp() + settings = app.getSettings() + + self.assertGreater(len(settings), 100) + self.assertEqual(settings["numProcessors"].value, 1) + self.assertEqual(settings["nCycles"].value, 1) + + def test_splashText(self): + app = getApp() + splash = app.splashText + self.assertIn("========", splash) + self.assertIn("Advanced", splash) + self.assertIn("version", splash) + self.assertIn(meta.__version__, splash) + + def test_splashTextDifferentApp(self): + import armi + + app = getApp() + name = "DifferentApp" + app.name = name + armi._app = app + context.APP_NAME = name + + splash = app.splashText + self.assertIn("========", splash) + self.assertIn("Advanced", splash) + self.assertIn("version", splash) + self.assertIn(meta.__version__, splash) + self.assertIn("DifferentApp", splash) + class TestArmi(unittest.TestCase): - """ - Tests for functions in the ARMI __init__ module. - """ + """Tests for functions in the ARMI __init__ module.""" def test_getDefaultPlugMan(self): - from armi import cli - - pm = armi.getDefaultPluginManager() - pm2 = armi.getDefaultPluginManager() + pm = getDefaultPluginManager() + pm2 = getDefaultPluginManager() self.assertTrue(pm is not pm2) self.assertIn(cli.EntryPointsPlugin, pm.get_plugins()) def test_overConfigured(self): with self.assertRaises(RuntimeError): - armi.configure() + configure() def test_main(self): with self.assertRaises(SystemExit): diff --git a/armi/tests/test_cartesian.py b/armi/tests/test_cartesian.py index 5db3e4298..4986aeb45 100644 --- a/armi/tests/test_cartesian.py +++ b/armi/tests/test_cartesian.py @@ -13,15 +13,15 @@ # limitations under the License. """ - +Tests for Cartesian reactors """ import unittest -from armi.utils import directoryChangers -from armi.tests import TEST_ROOT -from armi.reactor.flags import Flags -import armi.reactor.tests.test_reactors from armi.reactor import geometry +from armi.reactor.flags import Flags +from armi.reactor.tests import test_reactors +from armi.tests import TEST_ROOT +from armi.utils import directoryChangers class CartesianReactorTests(unittest.TestCase): @@ -37,10 +37,8 @@ def tearDownClass(cls): cls.directoryChanger.close() def setUp(self): - """ - Use the related setup in the testFuelHandlers module. - """ - self.o, self.r = armi.reactor.tests.test_reactors.loadTestReactor( + """Use the related setup in the testFuelHandlers module.""" + self.o, self.r = test_reactors.loadTestReactor( self.directoryChanger.destination, inputFileName="refTestCartesian.yaml" ) diff --git a/armi/tests/test_mpiActions.py b/armi/tests/test_mpiActions.py index 9bd8a27c2..891ee432c 100644 --- a/armi/tests/test_mpiActions.py +++ b/armi/tests/test_mpiActions.py @@ -15,44 +15,44 @@ import unittest -import armi from armi.mpiActions import ( DistributeStateAction, DistributionAction, MpiAction, runActions, ) +from armi import context from armi.reactor.tests import test_reactors from armi.tests import TEST_ROOT from armi.utils import iterables -@unittest.skipUnless(armi.MPI_RANK == 0, "test only on root node") +@unittest.skipUnless(context.MPI_RANK == 0, "test only on root node") class MpiIterTests(unittest.TestCase): def setUp(self): """save MPI size on entry""" - self._mpiSize = armi.MPI_SIZE + self._mpiSize = context.MPI_SIZE self.action = MpiAction() def tearDown(self): """restore MPI rank and size on exit""" - armi.MPI_SIZE = self._mpiSize - armi.MPI_RANK = 0 + context.MPI_SIZE = self._mpiSize + context.MPI_RANK = 0 def test_mpiIter(self): allObjs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] distObjs = [[0, 1, 2], [3, 4, 5], [6, 7], [8, 9], [10, 11]] - armi.MPI_SIZE = 5 - for rank in range(armi.MPI_SIZE): - armi.MPI_RANK = rank + context.MPI_SIZE = 5 + for rank in range(context.MPI_SIZE): + context.MPI_RANK = rank myObjs = list(self.action.mpiIter(allObjs)) self.assertEqual(myObjs, distObjs[rank]) def _distributeObjects(self, allObjs, numProcs): - armi.MPI_SIZE = numProcs + context.MPI_SIZE = numProcs objs = [] - for armi.MPI_RANK in range(armi.MPI_SIZE): + for context.MPI_RANK in range(context.MPI_SIZE): objs.append(list(self.action.mpiIter(allObjs))) return objs diff --git a/armi/tests/test_mpiFeatures.py b/armi/tests/test_mpiFeatures.py index c8fe0c323..71b8787fa 100644 --- a/armi/tests/test_mpiFeatures.py +++ b/armi/tests/test_mpiFeatures.py @@ -26,12 +26,12 @@ # pylint: disable=abstract-method,no-self-use,unused-argument from distutils.spawn import find_executable import os -import unittest import subprocess +import unittest import pytest -import armi +from armi import context from armi import mpiActions from armi import settings from armi.interfaces import Interface @@ -78,7 +78,7 @@ def fail(self): raise RuntimeError("Failing interface critical worker failure") def interactEveryNode(self, c, n): # pylint:disable=unused-argument - armi.MPI_COMM.bcast("fail", root=0) + context.MPI_COMM.bcast("fail", root=0) def workerOperate(self, cmd): if cmd == "fail": @@ -95,28 +95,28 @@ def setUp(self): self.o = OperatorMPI(cs=self.old_op.cs) self.o.r = self.r - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_basicOperatorMPI(self): self.o.operate() - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_masterException(self): self.o.removeAllInterfaces() failer = FailingInterface1(self.o.r, self.o.cs) self.o.addInterface(failer) - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: self.assertRaises(RuntimeError, self.o.operate) else: self.o.operate() - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_masterCritical(self): self.o.removeAllInterfaces() failer = FailingInterface2(self.o.r, self.o.cs) self.o.addInterface(failer) - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: self.assertRaises(Exception, self.o.operate) else: self.o.operate() @@ -128,14 +128,14 @@ def invokeHook(self): nItems = 50 results = [None] * nItems for objIndex in range(nItems): - if objIndex % armi.MPI_SIZE == armi.MPI_RANK: + if objIndex % context.MPI_SIZE == context.MPI_RANK: results[objIndex] = objIndex allResults = self.gather(results) if allResults: # this is confounding!!!! - return [allResults[ai % armi.MPI_SIZE][ai] for ai in range(nItems)] + return [allResults[ai % context.MPI_SIZE][ai] for ai in range(nItems)] class BcastAction2(mpiActions.MpiAction): @@ -161,13 +161,13 @@ def setUp(self): self.action.o = self.o self.action.r = self.o.r - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_distributeSettings(self): """Under normal circumstances, we would not test "private" methods; however, distributeState is quite complicated. """ self.action._distributeSettings() - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: self.assertEqual(self.cs, self.action.o.cs) else: self.assertNotEqual(self.cs, self.action.o.cs) @@ -185,32 +185,32 @@ def test_distributeSettings(self): for key in original.keys(): self.assertEqual(original[key], current[key]) - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_distributeReactor(self): """Under normal circumstances, we would not test "private" methods; however, distributeState is quite complicated. """ original_reactor = self.action.r self.action._distributeReactor(self.cs) - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: self.assertEqual(original_reactor, self.action.r) else: self.assertNotEqual(original_reactor, self.action.r) self.assertIsNone(self.action.r.core.lib) - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_distributeInterfaces(self): """Under normal circumstances, we would not test "private" methods; however, distributeState is quite complicated. """ original_interfaces = self.o.interfaces self.action._distributeInterfaces() - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: self.assertEqual(original_interfaces, self.o.interfaces) else: self.assertEqual(original_interfaces, self.o.interfaces) - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_distributeState(self): original_reactor = self.o.r original_lib = self.o.r.core.lib @@ -218,7 +218,7 @@ def test_distributeState(self): original_bolassems = self.o.r.blueprints.assemblies self.action.invokeHook() - if armi.MPI_RANK == 0: + if context.MPI_RANK == 0: self.assertEqual(self.cs, self.o.cs) self.assertEqual(original_reactor, self.o.r) self.assertEqual(original_interfaces, self.o.interfaces) @@ -236,14 +236,14 @@ def test_distributeState(self): pDef.assigned & parameterDefinitions.SINCE_LAST_DISTRIBUTE_STATE ) - @unittest.skipIf(armi.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_compileResults(self): action1 = BcastAction1() - armi.MPI_COMM.bcast(action1) + context.MPI_COMM.bcast(action1) results1 = action1.invoke(None, None, None) action2 = BcastAction2() - armi.MPI_COMM.bcast(action2) + context.MPI_COMM.bcast(action2) results2 = action2.invoke(None, None, None) self.assertEqual(results1, results2) diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py index 787ec9ff9..4fb990e95 100644 --- a/armi/tests/test_plugins.py +++ b/armi/tests/test_plugins.py @@ -18,9 +18,9 @@ import yamlize -import armi.settings -from armi import plugins from armi import interfaces +from armi import plugins +from armi import settings class TestPlugin(unittest.TestCase): @@ -56,7 +56,7 @@ def test_exposeInterfaces(self): if not hasattr(self.plugin, "exposeInterfaces"): return - cs = armi.settings.getMasterCs() + cs = settings.getMasterCs() results = self.plugin.exposeInterfaces(cs) # each plugin should return a list self.assertIsInstance(results, list) diff --git a/armi/utils/__init__.py b/armi/utils/__init__.py index a9713c773..8fc338b6e 100644 --- a/armi/utils/__init__.py +++ b/armi/utils/__init__.py @@ -31,7 +31,8 @@ import time import traceback -import armi +from armi import __name__ as armi_name +from armi import __path__ as armi_path from armi import runLog from armi.utils import iterables from armi.utils.flags import Flag @@ -288,7 +289,7 @@ def runFunctionFromAllModules(funcName, *args, **kwargs): """ for _modImporter, name, _ispkg in pkgutil.walk_packages( - path=armi.__path__, prefix=armi.__name__ + "." + path=armi_path, prefix=armi_name + "." ): try: mod = importlib.import_module(name) diff --git a/armi/utils/customExceptions.py b/armi/utils/customExceptions.py index 032d0522c..961656496 100644 --- a/armi/utils/customExceptions.py +++ b/armi/utils/customExceptions.py @@ -132,7 +132,7 @@ def __init__(self, setting): class InvalidSettingsFileError(SettingException): - """Not a valid xml or settings file""" + """Not a valid settings file""" def __init__(self, path, customMsgEnd=""): msg = "Attempted to load an invalid settings file from: {}. ".format(path) diff --git a/armi/utils/directoryChangers.py b/armi/utils/directoryChangers.py index b1401890a..b15712448 100644 --- a/armi/utils/directoryChangers.py +++ b/armi/utils/directoryChangers.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import glob import os import pathlib import random import shutil import string -import glob -import armi from armi import context from armi import runLog from armi.utils import pathTools @@ -38,6 +37,12 @@ class DirectoryChanger: """ Utility to change directory. + Use with 'with' statements to execute code in a different dir, guaranteeing a clean + return to the original directory + + >>> with DirectoryChanger('C:\\whatever') + ... pass + Parameters ---------- destination : str @@ -50,13 +55,6 @@ class DirectoryChanger: dumpOnException : bool, optional Flag to tell system to retrieve the entire directory if an exception is raised within a the context manager. - - Use with 'with' statements to execute code in a different dir, guaranteeing a clean - return to the original directory - - >>> with DirectoryChanger('C:\\whatever') - ... pass - """ def __init__( @@ -224,7 +222,7 @@ def __init__( # creating its destination directory, it may always be safe to delete it # regardless of location. if root is None: - root = armi.context.getFastPath() + root = context.getFastPath() # ARMIs temp dirs are in an context.APP_DATA directory: validate this is a temp dir. if pathlib.Path(context.APP_DATA) not in pathlib.Path(root).parents: raise ValueError( @@ -303,7 +301,7 @@ def __enter__(self): def directoryChangerFactory(): - if armi.MPI_SIZE > 1: + if context.MPI_SIZE > 1: from .directoryChangersMpi import MpiDirectoryChanger return MpiDirectoryChanger diff --git a/armi/utils/dynamicImporter.py b/armi/utils/dynamicImporter.py index ddfeb89e8..30f03d90e 100644 --- a/armi/utils/dynamicImporter.py +++ b/armi/utils/dynamicImporter.py @@ -21,36 +21,11 @@ from armi import runLog -def _importModule(modules): - # Import the module containing the interface - try: - module = __import__(".".join(modules)) - except: - runLog.error( - "Failed to dynamically import the module {}".format(".".join(modules)) - ) - raise - # recursively get attributes. - for subMod in modules[1:]: - # Traverse down the chain to the actual module - try: - module = getattr(module, subMod) - except: - runLog.error( - "Attempting to dynamically import subclasses has failed. \n" - "The module {} does not have `{}'".format(module, subMod) - ) - raise - return module - - -def importModule(fullyQualifiedModule): - return _importModule(fullyQualifiedModule.split(".")) - - def importEntirePackage(module): - """Load every module in a package""" - # TODO: this method may only work for a flat directory? + """Load every module in a package + + NOTE: this method may only work for a flat directory? + """ modules = glob.glob(os.path.dirname(module.__file__) + "/*.py") names = [os.path.basename(f)[:-3] for f in modules] for name in names: @@ -63,7 +38,6 @@ def getEntireFamilyTree(cls): One large caveat is it can only locate subclasses that had been imported somewhere Look to use importEntirePackage before searching for subclasses if not all children are being found as expected. - """ return cls.__subclasses__() + [ grandchildren diff --git a/armi/utils/mathematics.py b/armi/utils/mathematics.py index b71cfdc73..5eb2b1a62 100644 --- a/armi/utils/mathematics.py +++ b/armi/utils/mathematics.py @@ -613,7 +613,7 @@ def resampleStepwise(xin, yin, xout, avg=True): chunk[0] *= fraction # return the sum or the average - if None in chunk: + if [1 for c in chunk if (not hasattr(c, "__len__") and c is None)]: yout.append(None) elif avg: weighted_sum = sum([c * l for c, l in zip(chunk, length)]) diff --git a/armi/utils/plotting.py b/armi/utils/plotting.py index bb4a6969e..807d731de 100644 --- a/armi/utils/plotting.py +++ b/armi/utils/plotting.py @@ -697,6 +697,8 @@ def plotAssemblyTypes( assems=None, maxAssems=None, showBlockAxMesh=True, + yAxisLabel=None, + title=None, ) -> plt.Figure: """ Generate a plot showing the axial block and enrichment distributions of each assembly type in the core. @@ -718,16 +720,24 @@ def plotAssemblyTypes( showBlockAxMesh: bool if true, the axial mesh information will be displayed on the right side of the assembly plot. + yAxisLabel: str + Optionally, provide a label for the Y-axis. + + title: str + Optionally, provide a title for the plot. + Returns ------- fig : plt.Figure The figure object created """ - + # handle defaults if assems is None: assems = list(blueprints.assemblies.values()) + if not isinstance(assems, (list, set, tuple)): assems = [assems] + if maxAssems is not None and not isinstance(maxAssems, int): raise TypeError("Maximum assemblies should be an integer") @@ -735,6 +745,12 @@ def plotAssemblyTypes( if maxAssems is None: maxAssems = numAssems + if yAxisLabel is None: + yAxisLabel = "THERMALLY EXPANDED AXIAL HEIGHTS (CM)" + + if title is None: + title = "Assembly Designs" + # Set assembly/block size constants yBlockHeights = [] yBlockAxMesh = OrderedSet() @@ -781,8 +797,8 @@ def plotAssemblyTypes( ax.set_yticks([0.0] + list(set(numpy.cumsum(yBlockHeightDiffs)))) ax.xaxis.set_visible(False) - ax.set_title("Assembly Designs", y=1.03) - ax.set_ylabel("Thermally Expanded Axial Heights (cm)".upper(), labelpad=20) + ax.set_title(title, y=1.03) + ax.set_ylabel(yAxisLabel, labelpad=20) ax.set_xlim([0.0, 0.5 + maxAssems * (assemWidth + assemSeparation)]) # Plot and save figure @@ -805,7 +821,6 @@ def _plotBlocksInAssembly( xAssemEndLoc, showBlockAxMesh, ): - # Set dictionary of pre-defined block types and colors for the plot lightsage = "xkcd:light sage" blockTypeColorMap = collections.OrderedDict( diff --git a/armi/utils/tests/test_custom_exceptions.py b/armi/utils/tests/test_custom_exceptions.py index 97e35496e..76353ef1c 100644 --- a/armi/utils/tests/test_custom_exceptions.py +++ b/armi/utils/tests/test_custom_exceptions.py @@ -16,7 +16,7 @@ # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access,no-self-use,invalid-name import unittest -import armi +from armi import context from armi.tests import mockRunLogs from armi.utils.customExceptions import info, important from armi.utils.customExceptions import warn, warn_when_root @@ -63,6 +63,8 @@ def exampleWarnWhenRootMessage(self): return "warning from root".format() def test_warn_when_root_decorator(self): + import armi # pylint: disable=import-outside-toplevel + with mockRunLogs.BufferLog() as mock: for ii in range(1, 4): self.exampleWarnWhenRootMessage() diff --git a/armi/utils/tests/test_dochelpers.py b/armi/utils/tests/test_dochelpers.py index 05f3f9a5b..274c714fe 100644 --- a/armi/utils/tests/test_dochelpers.py +++ b/armi/utils/tests/test_dochelpers.py @@ -26,7 +26,7 @@ class TestDocHelpers(unittest.TestCase): - """Tests for the utility dochelpers functionns.""" + """Tests for the utility dochelpers functions.""" def test_paramTable(self): diff --git a/armi/utils/tests/test_mathematics.py b/armi/utils/tests/test_mathematics.py index e5bfec5ee..a43cf1e09 100644 --- a/armi/utils/tests/test_mathematics.py +++ b/armi/utils/tests/test_mathematics.py @@ -466,6 +466,38 @@ def test_resampleStepwiseAvgComplicatedNone(self): self.assertIsNone(yout[4]) self.assertEqual(yout[5], 38.5) + def test_resampleStepwiseAvgNpArray(self): + """Test resampleStepwise() averaging when some of the values are arrays""" + xin = [0, 1, 2, 3, 4] + yin = [11, np.array([1, 1]), np.array([2, 2]), 44] + xout = [2, 4, 5, 6, 7] + + yout = resampleStepwise(xin, yin, xout, avg=True) + + self.assertEqual(len(yout), len(xout) - 1) + self.assertTrue(isinstance(yout[0], type(yin[1]))) + self.assertEqual(yout[0][0], 23.0) + self.assertEqual(yout[0][1], 23.0) + self.assertEqual(yout[1], 0) + self.assertEqual(yout[2], 0) + self.assertEqual(yout[3], 0) + + def test_resampleStepwiseAvgNpArray(self): + """Test resampleStepwise() summing when some of the values are arrays""" + xin = [0, 1, 2, 3, 4] + yin = [11, np.array([1, 1]), np.array([2, 2]), 44] + xout = [2, 4, 5, 6, 7] + + yout = resampleStepwise(xin, yin, xout, avg=False) + + self.assertEqual(len(yout), len(xout) - 1) + self.assertTrue(isinstance(yout[0], type(yin[1]))) + self.assertEqual(yout[0][0], 46.0) + self.assertEqual(yout[0][1], 46.0) + self.assertEqual(yout[1], 0) + self.assertEqual(yout[2], 0) + self.assertEqual(yout[3], 0) + def test_rotateXY(self): x = [1.0, -1.0] y = [1.0, 1.0] diff --git a/armi/utils/tests/test_plotting.py b/armi/utils/tests/test_plotting.py index c737275af..c37abbf5b 100644 --- a/armi/utils/tests/test_plotting.py +++ b/armi/utils/tests/test_plotting.py @@ -54,10 +54,15 @@ def test_plotDepthMap(self): # indirectly tests plot face map self._checkExists(fName) def test_plotAssemblyTypes(self): + plotPath = "coreAssemblyTypes1.png" + plotting.plotAssemblyTypes(self.r.core.parent.blueprints, plotPath) + self._checkExists(plotPath) + + plotPath = "coreAssemblyTypes2.png" plotting.plotAssemblyTypes( - self.r.core.parent.blueprints, "coreAssemblyTypes1.png" + self.r.core.parent.blueprints, plotPath, yAxisLabel="y axis", title="title" ) - self._checkExists("coreAssemblyTypes1.png") + self._checkExists(plotPath) def test_plotBlockFlux(self): try: diff --git a/armi/utils/tests/test_textProcessors.py b/armi/utils/tests/test_textProcessors.py index 3aa2b0843..9bbb80ef5 100644 --- a/armi/utils/tests/test_textProcessors.py +++ b/armi/utils/tests/test_textProcessors.py @@ -28,7 +28,7 @@ class YamlIncludeTest(unittest.TestCase): - def testResolveIncludes(self): + def test_resolveIncludes(self): with open(os.path.join(RES_DIR, "root.yaml")) as f: resolved = textProcessors.resolveMarkupInclusions( f, root=pathlib.Path(RES_DIR) @@ -63,7 +63,7 @@ def testResolveIncludes(self): self.assertTrue(commentFound) self.assertTrue(anchorFound) - def testFindIncludes(self): + def test_findIncludes(self): includes = textProcessors.findYamlInclusions( pathlib.Path(RES_DIR) / "root.yaml" ) diff --git a/doc/conf.py b/doc/conf.py index 07e588219..4a5b06021 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -40,24 +40,18 @@ sys.path.insert(0, PYTHONPATH) # Also add to os.environ which will be used by the nbsphinx extension environment os.environ["PYTHONPATH"] = PYTHONPATH -import armi + from armi import apps +from armi import configure as armi_configure +from armi import context +from armi import disableFutureConfigures +from armi import meta from armi.bookkeeping import tests as bookkeepingTests -from armi.context import RES from armi.utils.dochelpers import * # Configure the baseline framework "App" for framework doc building -armi.configure(apps.App()) - -# some examples have import armi;armi.configure() in them that are intended -# to be copy/pasted by new users. However, armi will crash with param locks if it is -# configured twice. We often use if armi.isConfigured() to guard against -# issues here, but prefer not to highlight that somewhat confusing -# conditional if a user is just copy pasting fresh code -# ("How on Earth," they might wonder "would it already be configured!?"). Thus, -# we tell armi to simply disable future configure calls with this advanced flag -armi._ignoreConfigures = True - +armi_configure(apps.App()) +disableFutureConfigures() APIDOC_REL = ".apidocs" SOURCE_DIR = os.path.join("..", "armi") @@ -141,6 +135,8 @@ def setup(app): "private-members": False, } autodoc_member_order = "bysource" +# this line removes huge numbers of false and misleading, inherited docstrings +autodoc_inherit_docstrings = False autoclass_content = "both" apidoc_module_dir = SOURCE_DIR @@ -182,8 +178,8 @@ def setup(app): copyright = "2009-{}, TerraPower, LLC".format(datetime.datetime.now().year) # Use the pre-existing version definition. -version = armi.__version__ -release = armi.__version__ +version = meta.__version__ +release = meta.__version__ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -332,7 +328,7 @@ def setup(app): ] ), "within_subsection_order": FileNameSortKey, - "default_thumb_file": os.path.join(RES, "images", "TerraPowerLogo.png"), + "default_thumb_file": os.path.join(context.RES, "images", "TerraPowerLogo.png"), } suppress_warnings = ["autoapi.python_import_resolution"] diff --git a/doc/developer/reports.rst b/doc/developer/reports.rst index afec8b2a2..779bac572 100644 --- a/doc/developer/reports.rst +++ b/doc/developer/reports.rst @@ -1,6 +1,6 @@ Reports in ARMI ================ -.. note:: The resulting report itself is an Html page with table of contents on the left. +.. note:: The resulting report itself is an HTML page with table of contents on the left. ARMI provides the ability to make a variety of plots and tables describing the state of the reactor. Also, with ARMI's variety of plugins, some plots/analysis could be plugin dependent. If you have certain plugins installed, some new @@ -241,7 +241,7 @@ Here, only one label exists, so we only add one line for ``label[0]``. There are In summary, to add multiple lines (say, for different assembly types on a Peak DPA plot), the label would be the assembly type and the data would be the dpa at the time for that type. The ``uncertainty`` value --> which in general denotes an error bar on the graph---> would be None or 0, for each point if there is no uncertainty. -Html Elements +HTML Elements ------------- One may also want to add just plain prose. To do this, Sections also allow for the addition of htmltree elements so you can add paragraphs, divs, etc, as outlined in htmltree. These parts however will not be titled unless wrapped within a Section, and similarily will not have a direct link @@ -254,4 +254,4 @@ Summary ------- ``ReportContent`` is made up of many different types of elements (``Sections``, ``Tables``, ``Images``, ``HtmlElements``, ``TimeSeries``), that when ``writeReports()`` is called on the ``ReportContent`` object, have the ability to be rendered through their ``render()`` method in order to be translated -to html for the resulting document. This document is saved in a new folder titled reportsOutputFiles. \ No newline at end of file +to html for the resulting document. This document is saved in a new folder titled reportsOutputFiles. diff --git a/doc/developer/standards_and_practices.rst b/doc/developer/standards_and_practices.rst index b50ab9e5a..2ffce15df 100644 --- a/doc/developer/standards_and_practices.rst +++ b/doc/developer/standards_and_practices.rst @@ -47,6 +47,17 @@ Also, do not **ever** code the following things into the code: user names, passw environmental variables where possible and user-configurable settings elsewhere. You can also use the ``armi.ROOT`` variable (for the active code directory) or ``armi.RES``, and some other useful root-level variables. +Avoid the global keyword +======================== +At all costs, avoid use of the ``global`` keyword in your code. Using this keyword can, and usually does, create +extremely fragile code that is nigh-impossible to use a debugger on. Especially as part of object-oriented programming, +this is extremely lazy design. A careful reader might notice that there are several files in ARMI that are currently +using the ``global`` keyword. These are all schedule for a refactor to remove the use of ``global``. But, for now, +changing the code would cause more annoyance for the ARMI ecosystem userbase than fixing it would. Still, all of those +instance in ARMI will be fixed soon. + +No new uses of ``global`` will make it through the ARMI pull request process. + Naming conventions ================== @@ -138,8 +149,6 @@ Naming quick-reference - ``_assemblies`` * - variable names - ``linearHeatGenerationRate`` - - ``lhgr`` if it is commonly used by subject-matter experts. - ``_unusedDescription`` There are not "private" variables, use this for an unused variable. @@ -159,8 +168,8 @@ Other names are also consistently used throughout ARMI for specific objects: * ``lib`` when referring to a cross section library (would have been better as ``xsLib``) -Break large methods into operative sections. -============================================ +Prefer shorter methods +====================== A method should have one clear purpose. If you are writing a method that does one thing after the other, break it up into multiple methods and have a primary method call them in order. If your method is longer than 100 lines, see if you can't break it up. This does a few things: @@ -173,14 +182,8 @@ than 100 lines, see if you can't break it up. This does a few things: Avoid repeating code ==================== In other words, don't repeat yourself. (`D. R. Y. `_). -Repetitious code is harder to read, and will be annoying for others to update in the future. If you ever find -yourself copying and pasting code, consider pulling the repeated code out into it's own function. - -Use comments only when you cannot express what you're doing with names -====================================================================== -Use comments sparingly. This is important because often code gets updated but comments do not, which leads to -confusion and lost time. Strive to express what the code is doing and why with descriptive variable and method names. -Of course, some times you will have to use comments. +Repetitious code is harder to read, and harderd for others to update. If you ever find yourself copying and pasting +code, consider pulling the repeated code out into it's own function, or using a loop. Public methods should have docstrings ===================================== @@ -190,17 +193,17 @@ functions and public classes. Unit tests ========== All ARMI developers are required to write unit tests. In particular, if you are adding new code to the code base, you -will be required to add unit tests for your new code. +are required to add unit tests for your new code. ARMI uses the ``pytest`` library to drive tests, therefore tests need to be runnable from the commandline by ``python -m pytest armi``. Furthermore, for consistency: -* Each individual unit test should take under 5 seconds. -* All unit tests together should take under 30 seconds. -* All unit tests **shall** be placed into a separate module from production code that is prefixed with ``test_``. -* All unit tests **shall** be written in object-oriented fashion, inheriting from ``unittest.TestCase``. -* All test method names **shall** start with ``test_``. -* All test method names **shall** be descriptive. If the test method is not descriptive enough, add a docstring. +* Each individual unit test should take under 10 seconds, on a modern laptop. +* All unit tests together should take under 60 seconds, on a modern laptop. +* All unit tests should be placed into a separate module from production code that is prefixed with ``test_``. +* All unit tests should be written in object-oriented fashion, inheriting from ``unittest.TestCase``. +* All test method names should start with ``test_``. +* All test method names should be descriptive. If the test method is not descriptive enough, add a docstring. * Unit tests should have at least one assertion. Import statements @@ -217,7 +220,7 @@ Import ordering For consistency, import packages in this order: 1. Python built-in packages -2. External 3rd party packages +2. External third-party packages 3. ARMI modules Place a single line between each of these groups, for example: @@ -275,23 +278,23 @@ Data model Any reactor state information that is created by an ``Interface`` should be stored in the ARMI data model. The goal is that given minimal information (i.e. case settings and blueprints) ARMI should be able to load an entire reactor simulation from a given database. If you add state data to your modeling that isn't stored in the reactor, or add -new input files, you will break this paradigm and make everyone's life just a little harder. +new input files, you will break this paradigm and make everyone's life just a little bit harder. Input files =========== -ARMI developers **shall** use one of the following well defined, Python supported, input file formats. +ARMI developers **shall** use one of the following well-defined, Python-supported, input file formats. .json JSON files are used for a variety of data-object representations. There are some limitations of JSON, in that it does not easily support comments. JSON is also very strict. .yaml - YAML files are like JSON files but can have comments in them. + YAML files are like JSON files but can have comments in them. Address the pylint warnings =========================== -Our code review system and IDEs integrate with the automatic code checker, pylint. Any new code you add to the code -base must have zero pylint warnings or errors. +Our pull request system integrates with the automatic code checker, pylint. Any new code you add must have +zero pylint warnings or errors. General do's and don'ts ======================= @@ -304,6 +307,4 @@ do not use ``super`` ``__init__``, use ``ParentClass.__init__(self, plus, additional, arguments)``. do not leave ``TODO`` statements in production code - NQA-1 requires that the code be "complete", and a ``TODO`` statement leaves the code looking - incomplete. Therefore, do not leave ``TODO`` statements within production code. Instead, open a ticket. If your ``TODO`` statement is important, perhaps it should be a GitHub Issue. diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 0a0624f30..22260afbb 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -13,6 +13,7 @@ What's new in ARMI #. Removed unused Thermal Hydraulics settings. #. Minor code re-org, moving math utilities into their own module. #. Removed the ``PyYaml`` dependency. +#. Removed all bare ``import armi`` statements, for code clarity. #. TBD Bug fixes @@ -21,6 +22,7 @@ Bug fixes #. Fixed issues finding ``ISOXX`` files cross-platform. #. Fixed issue with docs not correctly loading their Git submodule in TOX. #. Fixed issue with ``_swapFluxParams`` failing for assemblies with different blocks (`#566 `_) +#. Multiple bug fixes in ``growToFullCore``. #. TBD diff --git a/doc/user/accessingEntryPoints.rst b/doc/user/accessingEntryPoints.rst index cf8ad47df..ade8b1e33 100644 --- a/doc/user/accessingEntryPoints.rst +++ b/doc/user/accessingEntryPoints.rst @@ -10,11 +10,11 @@ There are two ways to access the reports entry point in ARMI. The first way is through a yaml settings file. Here, the call is as follows:: - (venv) C:\Users\username\codes> tparmi report anl-afci-177.yaml + (venv) $ armi report anl-afci-177.yaml It is also possible to call this on an h5 file:: - (venv) C:\Users\username\codes> tparmi report -h5db refTestBase.h5 + (venv) $ armi report -h5db refTestBase.h5 .. note:: When working with a h5 file, -h5db must be included diff --git a/pytest.ini b/pytest.ini index 9b4d20b06..84c958473 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,5 @@ python_functions=nothing matches this pattern addopts = --durations=30 --tb=native filterwarnings = ignore:\s*the matrix subclass is not the recommended way:PendingDeprecationWarning - ignore:\s*Loading from XML-format settings:DeprecationWarning xvfb_width = 1200 xvfb_height = 1050 diff --git a/setup.py b/setup.py index 4ccb50031..b95ba51a6 100644 --- a/setup.py +++ b/setup.py @@ -88,6 +88,7 @@ def collectExtraFiles(): "docutils", "sphinx", "sphinx-rtd-theme", + "click==8.0.1", # fixing click problem in black "black==20.8b1", # for running jupyter dynamically in docs "sphinxcontrib-apidoc", diff --git a/tox.ini b/tox.ini index f013e6099..16eb6abe2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,lint,cov,mpicov +envlist = py38,lint,cov # need pip at least with --prefer-binary requires = pip >= 20.2 @@ -14,7 +14,7 @@ setenv = PYTHONPATH = {toxinidir} USERNAME = armi commands = - pytest --ignore=armi/utils/tests/test_gridGui.py {posargs} armi + pytest --ignore=armi/utils/tests/test_gridGui.py armi [testenv:doc] whitelist_externals = @@ -28,13 +28,30 @@ commands = git submodule update make html -[testenv:cov] +# First, run code coverage over the unit tests that run MPI library code. +[testenv:cov1] deps= pip>=20.2 + mpi4py + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-testing.txt +whitelist_externals = + /usr/bin/mpiexec +commands = + mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --ignore=venv armi/tests/test_mpiFeatures.py + +# Second, run code coverage over the rest of the unit tests, and combine the coverage results together +[testenv:cov2] +deps= + pip>=20.2 + mpi4py -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-testing.txt +whitelist_externals = + /usr/bin/mpiexec commands = - pytest --cov-config=.coveragerc --cov=armi --ignore=armi/utils/tests/test_gridGui.py --cov-fail-under=80 {posargs} armi + coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-append --ignore=armi/utils/tests/test_gridGui.py --ignore=venv armi + coverage combine --rcfile=.coveragerc --keep -a # NOTE: This only runs the MPI unit tests. # NOTE: This will only work in POSIX/BASH Linux.