Repository: terrapower/armi
Pull request #2115 (2025-04-08 02:37:29)

Adding Reactor construction hook to Database.load()
## What is the change? Why is it being made?

Currently the `beforeReactorConstruction` plugin hook is only called during a case settings based load but not during a database load. Without this hook, there is no plugin hook prior to loading a database. In particular, this hook is used by materials plugins to load materials data related to the reactor about to be constructed. Without this hook being called for database loads, the database loads just use whatever material data happened to be loaded prior to that point. 

## SCR Information

<!-- MANDATORY: uncomment one-and-only-one of these -->
<!-- Change Type: features -->
Change Type: fixes
<!-- Change Type: trivial -->
<!-- Change Type: docs -->

<!-- MANDATORY: Describe the change in one sentence -->
One-Sentence Description: Add `beforeReactorConstruction` hook to database loads.

<!-- MANDATORY: Describe any impact on the requirements, all on one line -->
One-line Impact on Requirements: NA


---

## Checklist

<!--
    The pull request author should check the box if the condition is met OR if it does not apply.
-->

- [x] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr).
- [x] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify any new/changed code.
- [x] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder.
- [x] The code style follows [good practices](https://terrapower.github.io/armi/developer/standards_and_practices.html).
- [x] The dependencies are still up-to-date in `pyproject.toml`.


alexhjames: @opotowsky This is likely why Nala has to have a mat_props load_all call in the `configure()`. I think you should be able to remove that after this PR.
opotowsky: > @opotowsky This is likely why Nala has to have a mat_props load_all call in the `configure()`. I think you should be able to remove that after this PR.

Correct it will remove that one call, but not all the others in every other repo (for unit testing). 

Patch:
diff --git a/armi/bookkeeping/db/__init__.py b/armi/bookkeeping/db/__init__.py
index 9ece8498..b00d0f7e 100644
--- a/armi/bookkeeping/db/__init__.py
+++ b/armi/bookkeeping/db/__init__.py
@@ -84,6 +84,7 @@ def loadOperator(
     statePointName=None,
     allowMissing=False,
     handleInvalids=True,
+    callReactorConstructionHook=False,
 ):
     """
     Return an operator given the path to a database.
@@ -104,6 +105,8 @@ def loadOperator(
         with undefined parameters. Default False.
     handleInvalids : bool
         Whether to check for invalid settings. Default True.
+    callReactorConstructionHook : bool
+        Flag for whether the beforeReactorConstruction plugin hook should be executed. Default is False.
 
     See Also
     --------
@@ -147,13 +150,14 @@ def loadOperator(
         # so creating a reactor from the database should be included.
         cs = db.loadCS(handleInvalids=handleInvalids)
         thisCase = cases.Case(cs)
-
         r = db.load(
             loadCycle,
             loadNode,
+            cs=cs,
             statePointName=statePointName,
             allowMissing=allowMissing,
             handleInvalids=handleInvalids,
+            callReactorConstructionHook=callReactorConstructionHook,
         )
 
     o = thisCase.initializeOperator(r=r)
diff --git a/armi/bookkeeping/db/database.py b/armi/bookkeeping/db/database.py
index c402603f..0d372c37 100644
--- a/armi/bookkeeping/db/database.py
+++ b/armi/bookkeeping/db/database.py
@@ -55,7 +55,7 @@ from typing import (
 import h5py
 import numpy as np
 
-from armi import context, getApp, meta, runLog, settings
+from armi import context, getApp, getPluginManagerOrFail, meta, runLog, settings
 from armi.bookkeeping.db.jaggedArray import JaggedArray
 from armi.bookkeeping.db.layout import (
     DB_VERSION,
@@ -689,6 +689,7 @@ class Database:
         statePointName=None,
         allowMissing=False,
         handleInvalids=True,
+        callReactorConstructionHook=False,
     ):
         """Load a new reactor from a DB at (cycle, node).
 
@@ -725,6 +726,8 @@ class Database:
             with undefined parameters. Default False.
         handleInvalids : bool
             Whether to check for invalid settings. Default True.
+        callReactorConstructionHook : bool
+            Flag for whether the beforeReactorConstruction plugin hook should be executed. Default is False.
 
         Returns
         -------
@@ -736,6 +739,9 @@ class Database:
         cs = cs or self.loadCS(handleInvalids=handleInvalids)
         bp = bp or self.loadBlueprints()
 
+        if callReactorConstructionHook:
+            getPluginManagerOrFail().hook.beforeReactorConstruction(cs=cs)
+
         if node < 0:
             numNodes = getNodesPerCycle(cs)[cycle]
             if (node + numNodes) < 0:


Test patch:
diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py
index e1498060..ea8eba61 100644
--- a/armi/tests/test_plugins.py
+++ b/armi/tests/test_plugins.py
@@ -28,12 +28,15 @@ from armi import (
     settings,
     utils,
 )
+from armi.bookkeeping.db import loadOperator
+from armi.bookkeeping.db.databaseInterface import DatabaseInterface
 from armi.physics.neutronics import NeutronicsPlugin
 from armi.reactor.blocks import Block
 from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger
 from armi.reactor.flags import Flags
 from armi.testing import loadTestReactor
 from armi.tests import TEST_ROOT
+from armi.utils.directoryChangers import TemporaryDirectoryChanger
 
 
 class PluginFlags1(plugins.ArmiPlugin):
@@ -126,18 +129,36 @@ class TestPluginRegistration(unittest.TestCase):
         # By default, make sure we get the armi-shipped expansion class
         self.assertIs(first, AxialExpansionChanger)
         pm.register(SillyAxialPlugin)
-        second = pm.hook.getAxialExpansionChanger()
-        # Registering a plugin that implements the hook means we get that plugin's axial expander
-        self.assertIs(second, SillyAxialExpansionChanger)
+        try:
+            second = pm.hook.getAxialExpansionChanger()
+            # Registering a plugin that implements the hook means we get that plugin's axial expander
+            self.assertIs(second, SillyAxialExpansionChanger)
+        finally:
+            pm.unregister(SillyAxialPlugin)
 
     def test_beforeReactorConstructionHook(self):
         """Test that plugin hook successfully injects code before reactor initialization."""
         pm = getPluginManagerOrFail()
         pm.register(BeforeReactorPlugin)
-        o = loadTestReactor(
-            TEST_ROOT, inputFileName="smallestTestReactor/armiRunSmallest.yaml"
-        )[0]
-        self.assertTrue(o.cs.beforeReactorConstructionFlag)
+        try:
+            o, r = loadTestReactor(
+                TEST_ROOT, inputFileName="smallestTestReactor/armiRunSmallest.yaml"
+            )
+            self.assertTrue(o.cs.beforeReactorConstructionFlag)
+
+            # Check that hook is called for database loading
+            with TemporaryDirectoryChanger():
+                dbi = DatabaseInterface(r, o.cs)
+                dbi.initDB(fName=self._testMethodName + ".h5")
+                db = dbi.database
+                db.writeToDB(r)
+                db.close()
+                o = loadOperator(
+                    self._testMethodName + ".h5", 0, 0, callReactorConstructionHook=True
+                )
+            self.assertTrue(o.cs.beforeReactorConstructionFlag)
+        finally:
+            pm.unregister(BeforeReactorPlugin)
 
 
 class TestPluginBasics(unittest.TestCase):