From eeb28b46de305cf2477ca92fd2aec811704fdd13 Mon Sep 17 00:00:00 2001 From: "Mitchell T.H. Young" Date: Fri, 24 Sep 2021 11:35:12 -0700 Subject: [PATCH] Clean up report code (#416) This includes a handful of cleanups of the newReports code, mostly related to things like: - general code organization - non-standard import locations - documentation In addition, the Case.summarizeDesign() method is changed to use new reports functionality, rather than needing to create an operator and interact with the report interface. --- armi/bookkeeping/__init__.py | 5 +- armi/bookkeeping/newReportUtils.py | 3 +- armi/bookkeeping/newReports.py | 10 ++- armi/bookkeeping/report/reportInterface.py | 1 - armi/bookkeeping/tests/test_report.py | 5 +- armi/cases/case.py | 20 +---- armi/cases/tests/test_cases.py | 6 +- armi/cli/reportsEntryPoint.py | 90 ++++++++++++---------- armi/physics/neutronics/reports.py | 9 +-- armi/plugins.py | 22 ++++-- armi/settings/caseSettings.py | 47 ----------- 11 files changed, 91 insertions(+), 127 deletions(-) diff --git a/armi/bookkeeping/__init__.py b/armi/bookkeeping/__init__.py index 4cf09980c..97c634a4e 100644 --- a/armi/bookkeeping/__init__.py +++ b/armi/bookkeeping/__init__.py @@ -110,12 +110,13 @@ def getReportContents(r, cs, report, stage, blueprint): """ from armi.cli import reportsEntryPoint + from armi.bookkeeping import newReports as reports from armi.bookkeeping import newReportUtils - if stage == reportsEntryPoint.ReportStage.Begin: + if stage == reports.ReportStage.Begin: newReportUtils.insertGeneralReportContent(cs, r, report, stage) if blueprint is not None: newReportUtils.insertBlueprintContent(r, cs, report, blueprint) - elif stage == reportsEntryPoint.ReportStage.End: + elif stage == reports.ReportStage.End: newReportUtils.insertEndOfLifeContent(r, report) return diff --git a/armi/bookkeeping/newReportUtils.py b/armi/bookkeeping/newReportUtils.py index 6084e3417..7f584d74a 100644 --- a/armi/bookkeeping/newReportUtils.py +++ b/armi/bookkeeping/newReportUtils.py @@ -10,7 +10,6 @@ from armi.utils import plotting from armi.utils import units from armi.utils import iterables -from armi.cli.reportsEntryPoint import ReportStage from armi.materials import custom @@ -37,7 +36,7 @@ def insertGeneralReportContent(cs, r, report, stage): """ # These items only happen once at BOL - if stage == ReportStage.Begin: + if stage == newReports.ReportStage.Begin: comprehensiveBOLContent(cs, r, report) insertDesignContent(r, report) diff --git a/armi/bookkeeping/newReports.py b/armi/bookkeeping/newReports.py index 116205873..74a299658 100644 --- a/armi/bookkeeping/newReports.py +++ b/armi/bookkeeping/newReports.py @@ -1,3 +1,5 @@ +from enum import Enum +from enum import auto import collections import shutil import os @@ -86,7 +88,7 @@ def writeReports(self): ), "report.js", ) - fileurl = doc.renderToFile("ReportContent.html", 0) + fileurl = doc.renderToFile("index.html", 0) return fileurl def get(self, section, default=None): @@ -566,6 +568,12 @@ def render(self, level, idPrefix="") -> htmltree.HtmlElement: ) +class ReportStage(Enum): + Begin = auto() + Standard = auto() + End = auto() + + def encode64(file_path): """Encodes the contents of the file indicated by the path diff --git a/armi/bookkeeping/report/reportInterface.py b/armi/bookkeeping/report/reportInterface.py index bfb64333e..d9f81546f 100644 --- a/armi/bookkeeping/report/reportInterface.py +++ b/armi/bookkeeping/report/reportInterface.py @@ -117,7 +117,6 @@ def generateDesignReport(self, generateFullCoreMap, showBlockAxMesh): def interactEOL(self): """Adds the data to the report, and generates it""" - self.cs.setSettingsReport() b = self.o.r.core.getFirstBlock(Flags.FUEL) b.setAreaFractionsReport() diff --git a/armi/bookkeeping/tests/test_report.py b/armi/bookkeeping/tests/test_report.py index 2d3082232..57213e45f 100644 --- a/armi/bookkeeping/tests/test_report.py +++ b/armi/bookkeeping/tests/test_report.py @@ -26,7 +26,6 @@ from armi.utils import directoryChangers from armi.physics.neutronics.reports import neutronicsPlotting import armi.bookkeeping.newReports -from armi.cli.reportsEntryPoint import ReportStage class TestReportContentCreation(unittest.TestCase): @@ -75,7 +74,7 @@ def testReportContents(self): r=self.r, cs=self.o.cs, report=reportTest, - stage=ReportStage.Begin, + stage=newReports.ReportStage.Begin, blueprint=self.r.blueprints, ) @@ -108,7 +107,7 @@ def testWriteReports(self): reportTest.writeReports() # Want to check that two exists... times = 0 - with open("ReportContent.html") as f: + with open("index.html") as f: for line in f: if "" in line: times = times + 1 diff --git a/armi/cases/case.py b/armi/cases/case.py index f20465cef..4c459cd50 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -45,11 +45,10 @@ from armi import operators from armi import runLog from armi import interfaces +from armi.cli import reportsEntryPoint from armi.reactor import blueprints from armi.reactor import systemLayoutInput from armi.reactor import reactors -from armi.bookkeeping import report -from armi.bookkeeping.report import reportInterface from armi.bookkeeping.db import compareDatabases from armi.utils import pathTools from armi.utils.directoryChangers import DirectoryChanger @@ -491,21 +490,8 @@ def checkInputs(self): def summarizeDesign(self, generateFullCoreMap=True, showBlockAxialMesh=True): """Uses the ReportInterface to create a fancy HTML page describing the design inputs.""" - settings.setMasterCs(self.cs) - o = self.initializeOperator() - with DirectoryChanger(self.cs.inputDirectory, dumpOnException=False): - # There are global variables that are modified when a report is - # generated, so reset it all - six.moves.reload_module(report) # pylint: disable=too-many-function-args - self.cs.setSettingsReport() - rpi = o.getInterface("report") - - if rpi is None: - rpi = reportInterface.ReportInterface(o.r, o.cs) - - rpi.generateDesignReport(generateFullCoreMap, showBlockAxialMesh) - report.DESIGN.writeHTML() - runLog.important("Design report summary was successfully generated") + + _ = reportsEntryPoint.createReportFromSettings(self.cs) def buildCommand(self, python="python"): """ diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index cc5c93b45..eb4dbf654 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -89,7 +89,11 @@ def test_summarizeDesign(self): case = cases.Case(cs) c2 = case.clone() c2.summarizeDesign(True, True) - self.assertTrue(os.path.exists("Core Design Report.html")) + self.assertTrue( + os.path.exists( + os.path.join("{}-reports".format(c2.cs.caseTitle), "index.html") + ) + ) def test_independentVariables(self): """Ensure that independentVariables added to a case move with it.""" diff --git a/armi/cli/reportsEntryPoint.py b/armi/cli/reportsEntryPoint.py index e9663badb..9e270b249 100644 --- a/armi/cli/reportsEntryPoint.py +++ b/armi/cli/reportsEntryPoint.py @@ -1,11 +1,15 @@ -from enum import Enum -from enum import auto +import pathlib import webbrowser import armi from armi.cli import entryPoint -from armi.reactor.reactors import factory +from armi.reactor import reactors from armi.utils import runLog +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 class ReportsEntryPoint(entryPoint.EntryPoint): @@ -23,10 +27,10 @@ def __init__(self): def addOptions(self): self.parser.add_argument("-h5db", help="Input database path", type=str) self.parser.add_argument( - "-bp", help="Input blueprint (optional)", type=str, default=None + "--bp", help="Input blueprint (optional)", type=str, default=None ) self.parser.add_argument( - "-settings", help="Settings File (optional", type=str, default=None + "--settings", help="Settings File (optional)", type=str, default=None ) self.parser.add_argument( "--output-name", @@ -67,45 +71,23 @@ def addOptions(self): # self.createOptionFromSetting("imperialunits", "-i") def invoke(self): - from armi import settings - from armi.reactor import blueprints - from armi.bookkeeping.newReports import ReportContent - from armi.bookkeeping.db import databaseFactory - from armi.utils import directoryChangers nodes = self.args.nodes - report = ReportContent("Overview") - app = armi.getApp() - if app is None: - raise RuntimeError("NEED APP!") - pm = app._pm if self.args.h5db is None: # Just do begining stuff, no database is given... if self.cs is not None: - cs = self.cs - settings.setMasterCs(self.cs) - blueprint = blueprints.loadFromCs(cs) - r = factory(cs, blueprint) - report.title = r.name + site = createReportFromSettings(cs) + if self.args.view: + webbrowser.open(site) else: raise RuntimeError( "No Settings with Blueprint or Database, cannot gerenate a report" ) - with directoryChangers.ForcedCreationDirectoryChanger("reportsOutputFiles"): - _ = pm.hook.getReportContents( - r=r, - cs=cs, - report=report, - stage=ReportStage.Begin, - blueprint=blueprint, - ) - site = report.writeReports() - if self.args.view: - webbrowser.open(site) - else: + report = reports.ReportContent("Overview") + pm = armi.getPluginManagerOrFail() db = databaseFactory(self.args.h5db, "r") if self.args.bp is not None: blueprint = self.args.bp @@ -119,18 +101,18 @@ def invoke(self): cs = db.loadCS() if self.args.bp is None: blueprint = db.loadBlueprints() - r = factory(cs, blueprint) + r = reactors.factory(cs, blueprint) report.title = r.name pluginContent = ( armi.getPluginManagerOrFail().hook.getReportContents( r=r, cs=cs, report=report, - stage=ReportStage.Begin, + stage=reports.ReportStage.Begin, blueprint=blueprint, ) ) - stage = ReportStage.Standard + stage = reports.ReportStage.Standard for cycle, node in dbNodes: if nodes is not None and (cycle, node) not in nodes: continue @@ -153,7 +135,7 @@ def invoke(self): pluginContent = pm.hook.getReportContents( r=r, cs=cs, report=report, stage=stage, blueprint=blueprint ) - stage = ReportStage.End + stage = reports.ReportStage.End pluginContent = pm.hook.getReportContents( r=r, cs=cs, report=report, stage=stage, blueprint=blueprint ) @@ -162,7 +144,35 @@ def invoke(self): webbrowser.open(site) -class ReportStage(Enum): - Begin = auto() - Standard = auto() - End = auto() +def createReportFromSettings(cs): + """ + Create BEGINNING reports, given a settings file. + + 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. + settings.setMasterCs(cs) + + blueprint = blueprints.loadFromCs(cs) + r = reactors.factory(cs, blueprint) + report = reports.ReportContent("Overview") + pm = armi.getPluginManagerOrFail() + report.title = r.name + + with directoryChangers.ForcedCreationDirectoryChanger( + "{}-reports".format(cs.caseTitle) + ): + _ = pm.hook.getReportContents( + r=r, + cs=cs, + report=report, + stage=reports.ReportStage.Begin, + blueprint=blueprint, + ) + site = report.writeReports() + + return site diff --git a/armi/physics/neutronics/reports.py b/armi/physics/neutronics/reports.py index b56c0af79..6de905274 100644 --- a/armi/physics/neutronics/reports.py +++ b/armi/physics/neutronics/reports.py @@ -1,8 +1,5 @@ from collections import defaultdict -# parts of report for neutronics -from armi.cli.reportsEntryPoint import ReportStage - from armi.bookkeeping import newReportUtils from armi.bookkeeping import newReports from armi.reactor.flags import Flags @@ -22,10 +19,12 @@ def insertNeutronicsReport(r, cs, report, stage): collecting contents for. """ - if stage == ReportStage.Begin: + if stage == newReports.ReportStage.Begin: insertNeutronicsBOLContent(r, cs, report) - elif stage == ReportStage.Standard or stage == ReportStage.End: + elif ( + stage == newReports.ReportStage.Standard or stage == newReports.ReportStage.End + ): neutronicsPlotting(r, report, cs) diff --git a/armi/plugins.py b/armi/plugins.py index ef6aa9fa4..3abbcf0ba 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -539,21 +539,27 @@ def mpiActionRequiresReset(cmd) -> bool: @staticmethod @HOOKSPEC - def getReportContents(r, cs, report, stage, blueprint): # ReportContent + def getReportContents(r, cs, report, stage, blueprint) -> None: """ To generate a report. + For more information, see :doc:`/developer/reports`. + Parameters ---------- - r : a reactor - cs : case settings - report : current report object to add to - blueprint : blueprint for a reactor (if None, only partial contents created) - stage : begin/standard/or end (stage of the report for - when inserting BOL vs. EOL content) + r : Reactor + + cs : Settings + + report : ReportContent + Report object to add contents to - For more information, see the documentation at https://terrapower.github.io/armi/developer/reports.html + stage : ReportStage + begin/standard/or end (stage of the report for when inserting BOL vs. EOL + content) + blueprint : Blueprint, optional + for a reactor (if None, only partial contents created) """ diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index 5938c0307..db88fac03 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -332,53 +332,6 @@ def writeToYamlStream(self, stream, style="short"): writer.writeYaml(stream) return writer - def setSettingsReport(self): - """Puts settings into the report manager""" - from armi.bookkeeping import report - - report.setData("caseTitle", self.caseTitle, report.RUN_META) - report.setData( - "outputFileExtension", self["outputFileExtension"], report.RUN_META - ) - - report.setData( - "Total Core Power", "%8.5E MWt" % (self["power"] / 1.0e6), report.RUN_META - ) - if not self["cycleLengths"]: - report.setData( - "Cycle Length", "%8.5f days" % self["cycleLength"], report.RUN_META - ) - report.setData( - "BU Groups", str(self["buGroups"]), report.RUN_META - ) # str to keep the list together in the report - - for key in [ - "nCycles", - "burnSteps", - "skipCycles", - "cycleLength", - "numProcessors", - ]: - report.setData(key, self[key], report.CASE_PARAMETERS) - - for key in self.environmentSettings: - report.setData(key, self[key], report.RUN_META, [report.ENVIRONMENT]) - - for key in ["genXS", "neutronicsKernel"]: - report.setData(key, self[key], report.CASE_CONTROLS, [report.ENVIRONMENT]) - - for key in ["boundaries", "neutronicsKernel", "neutronicsType", "fpModel"]: - report.setData(key, self[key], report.RUN_META, [report.NEUTRONICS]) - - for key in ["reloadDBName", "startCycle", "startNode"]: - report.setData(key, self[key], report.SNAPSHOT) - - for key in ["power", "Tin", "Tout"]: - report.setData(key, self[key], report.REACTOR_PARAMS) - - for key in ["buGroups"]: - report.setData(key, self[key], report.BURNUP_GROUPS) - def updateEnvironmentSettingsFrom(self, otherCs): r"""Updates the environment settings in this object based on some other cs (from the GUI, most likely)