diff --git a/MANIFEST.in b/MANIFEST.in index b95de935e..25454e9b0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include armi/nuclearDataIO/cccc/tests/fixtures/mc2v3.dlayxs include armi/nuclearDataIO/cccc/tests/fixtures/simple_cartesian.pwdint include armi/nuclearDataIO/cccc/tests/fixtures/simple_cartesian.rtflux include armi/nuclearDataIO/cccc/tests/fixtures/simple_cartesian.rzflux +include armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.dif3d include armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.geodst include armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.nhflux include armi/nuclearDataIO/tests/fixtures/AA.gamiso diff --git a/armi/nuclearDataIO/__init__.py b/armi/nuclearDataIO/__init__.py index bcc7bc9db..885e44a6d 100644 --- a/armi/nuclearDataIO/__init__.py +++ b/armi/nuclearDataIO/__init__.py @@ -23,6 +23,7 @@ # though prefer full imports in new code from .cccc import ( compxs, + dif3d, dlayxs, fixsrc, gamiso, diff --git a/armi/nuclearDataIO/cccc/dif3d.py b/armi/nuclearDataIO/cccc/dif3d.py new file mode 100644 index 000000000..2625fddc2 --- /dev/null +++ b/armi/nuclearDataIO/cccc/dif3d.py @@ -0,0 +1,215 @@ +# Copyright 2023 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. + +""" +Module for reading from and writing to DIF3D files, which are module dependent +binary inputs for the DIF3D code. +""" + +from armi import runLog +from armi.nuclearDataIO import cccc + + +FILE_SPEC_2D_PARAMS = ( + [ + "IPROBT", + "ISOLNT", + "IXTRAP", + "MINBSZ", + "NOUTMX", + "IRSTRT", + "LIMTIM", + "NUPMAX", + "IOSAVE", + "IOMEG1", + "INRMAX", + "NUMORP", + "IRETRN", + ] + + [f"IEDF{e}" for e in range(1, 11)] + + [ + "NOUTBQ", + "I0FLUX", + "NOEDIT", + "NOD3ED", + "ISRHED", + "NSN", + "NSWMAX", + "NAPRX", + "NAPRXZ", + "NFMCMX", + "NXYSWP", + "NZSWP", + "ISYMF", + "NCMRZS", + "ISEXTR", + "NPNO", + "NXTR", + "IOMEG2", + "IFULL", + "NVFLAG", + "ISIMPL", + "IWNHFL", + "IPERT", + "IHARM", + ] +) + +FILE_SPEC_3D_PARAMS = [ + "EPS1", + "EPS2", + "EPS3", + "EFFK", + "FISMIN", + "PSINRM", + "POWIN", + "SIGBAR", + "EFFKQ", + "EPSWP", +] + [f"DUM{e}" for e in range(1, 21)] + +TITLE_RANGE = 11 + + +class Dif3dData(cccc.DataContainer): + def __init__(self): + cccc.DataContainer.__init__(self) + + self.twoD = {e: None for e in FILE_SPEC_2D_PARAMS} + self.threeD = {e: None for e in FILE_SPEC_3D_PARAMS} + self.fourD = None + self.fiveD = None + + +class Dif3dStream(cccc.StreamWithDataContainer): + @staticmethod + def _getDataContainer() -> Dif3dData: + return Dif3dData() + + def _rwFileID(self) -> None: + """ + Record for file identification information. + + The parameters are stored as a dictionary under the attribute `metadata`. + """ + with self.createRecord() as record: + for param in ["HNAME", "HUSE1", "HUSE2"]: + self._metadata[param] = record.rwString(self._metadata[param], 8) + self._metadata["VERSION"] = record.rwInt(self._metadata["VERSION"]) + + def _rw1DRecord(self) -> None: + """ + Record for problem title, storage, and dump specifications. + + The parameters are stored as a dictionary under the attribute `metadata`. + """ + with self.createRecord() as record: + for i in range(TITLE_RANGE): + param = f"TITLE{i}" + self._metadata[param] = record.rwString(self._metadata[param], 8) + self._metadata["MAXSIZ"] = record.rwInt(self._metadata["MAXSIZ"]) + self._metadata["MAXBLK"] = record.rwInt(self._metadata["MAXBLK"]) + self._metadata["IPRINT"] = record.rwInt(self._metadata["IPRINT"]) + + def _rw2DRecord(self) -> None: + """ + Record for DIF3D integer control parameters. + + The parameters are stored as a dictionary under the attribute `twoD`. + """ + with self.createRecord() as record: + for param in FILE_SPEC_2D_PARAMS: + self._data.twoD[param] = record.rwInt(self._data.twoD[param]) + + def _rw3DRecord(self) -> None: + """ + Record for convergence criteria and other sundry floating point data (such as + k-effective). + + The parameters are stored as a dictionary under the attribute `threeD`. + """ + with self.createRecord() as record: + for param in FILE_SPEC_3D_PARAMS: + self._data.threeD[param] = record.rwDouble(self._data.threeD[param]) + + def _rw4DRecord(self) -> None: + """ + Record for the optimum overrelaxation factors. This record is only present when + using DIF3D-FD and if `NUMORP` is greater than 0. + + The parameters are stored as a dictionary under the attribute `fourD`. This + could be changed into a list in the future since this record represents groupwise + data. + """ + if self._data.twoD["NUMORP"] != 0: + omegaParams = [f"OMEGA{e}" for e in range(1, self._data.twoD["NUMORP"] + 1)] + + with self.createRecord() as record: + # Initialize the record if we're reading + if self._data.fourD is None: + self._data.fourD = {omegaParam: None for omegaParam in omegaParams} + + for omegaParam in omegaParams: + self._data.fourD[omegaParam] = record.rwDouble( + self._data.fourD[omegaParam] + ) + + def _rw5DRecord(self) -> None: + """ + Record for the axial coarse mesh rebalancing boundaries. Coarse mesh balancing is + disabled in DIF3D-VARIANT, so this record is only relevant for DIF3D-Nodal. This + record is only present if `NCMRZS` is greater than 0. + + The parameters are stored as a dictionary under the attribute `fiveD`. + """ + if self._data.twoD["NCMRZS"] != 0: + zcmrcParams = [f"ZCMRC{e}" for e in range(1, self._data.twoD["NCMRZS"] + 1)] + nzintsParams = [ + f"NZINTS{e}" for e in range(1, self._data.twoD["NCMRZS"] + 1) + ] + + with self.createRecord() as record: + # Initialize the record if we're reading + if self._data.fiveD is None: + self._data.fiveD = {zcmrcParam: None for zcmrcParam in zcmrcParams} + self._data.fiveD.update( + {nzintsParam: None for nzintsParam in nzintsParams} + ) + + for zcmrcParam in zcmrcParams: + self._data.fiveD[zcmrcParam] = record.rwDouble( + self._data.fiveD[zcmrcParam] + ) + for nzintsParam in nzintsParams: + self._data.fiveD[nzintsParam] = record.rwInt( + self._data.fiveD[nzintsParam] + ) + + def readWrite(self): + """Reads or writes metadata and data from 5 records""" + msg = f"{'Reading' if 'r' in self._fileMode else 'Writing'} DIF3D binary data {self}" + runLog.info(msg) + + self._rwFileID() + self._rw1DRecord() + self._rw2DRecord() + self._rw3DRecord() + self._rw4DRecord() + self._rw5DRecord() + + +readBinary = Dif3dStream.readBinary +readAscii = Dif3dStream.readAscii +writeBinary = Dif3dStream.writeBinary +writeAscii = Dif3dStream.writeAscii diff --git a/armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.dif3d b/armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.dif3d new file mode 100644 index 000000000..0e3df7dd9 Binary files /dev/null and b/armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.dif3d differ diff --git a/armi/nuclearDataIO/cccc/tests/test_dif3d.py b/armi/nuclearDataIO/cccc/tests/test_dif3d.py new file mode 100644 index 000000000..7be5f55a4 --- /dev/null +++ b/armi/nuclearDataIO/cccc/tests/test_dif3d.py @@ -0,0 +1,162 @@ +# Copyright 2023 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. + +"""Test reading/writing of DIF3D binary input.""" + +import os +import unittest + +from armi.nuclearDataIO.cccc import dif3d +from armi.utils.directoryChangers import TemporaryDirectoryChanger + +THIS_DIR = os.path.dirname(__file__) + +SIMPLE_HEXZ_INP = os.path.join(THIS_DIR, "../../tests", "simple_hexz.inp") +SIMPLE_HEXZ_DIF3D = os.path.join(THIS_DIR, "fixtures", "simple_hexz.dif3d") + + +class TestDif3dSimpleHexz(unittest.TestCase): + @classmethod + def setUpClass(cls): + """ + Load DIF3D data from binary file. This binary file was generated by running + dif3d.exe v11.0r3284 on the SIMPLE_HEXZ_INP file above (and renaming the DIF3D + binary file to simple_hexz.dif3d). + """ + cls.df = dif3d.Dif3dStream.readBinary(SIMPLE_HEXZ_DIF3D) + + def test__rwFileID(self): + """Verify the file identification info.""" + self.assertEqual(self.df.metadata["HNAME"], "DIF3D") + self.assertEqual(self.df.metadata["HUSE1"], "") + self.assertEqual(self.df.metadata["HUSE2"], "") + self.assertEqual(self.df.metadata["VERSION"], 1) + + def test__rwFile1DRecord(self): + """Verify the rest of the metadata""" + TITLE_A6 = ["3D Hex", "-Z to", "genera", "te NHF", "LUX fi", "le"] + EXPECTED_TITLE = TITLE_A6 + [""] * 5 + for i in range(dif3d.TITLE_RANGE): + self.assertEqual(self.df.metadata[f"TITLE{i}"], EXPECTED_TITLE[i]) + self.assertEqual(self.df.metadata["MAXSIZ"], 10000) + self.assertEqual(self.df.metadata["MAXBLK"], 1800000) + self.assertEqual(self.df.metadata["IPRINT"], 0) + + def test__rw2DRecord(self): + """Verify the control parameters""" + EXPECTED_2D = [ + 0, + 0, + 0, + 10000, + 30, + 0, + 1000000000, + 5, + 0, + 0, + 50, + 0, + 1, + 1, + 0, + 0, + 0, + 110, + 10, + 100, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 10, + 40, + 32, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + for i, param in enumerate(dif3d.FILE_SPEC_2D_PARAMS): + self.assertEqual(self.df.twoD[param], EXPECTED_2D[i]) + + def test__rw3DRecord(self): + """Verify the convergence criteria and other floating point data""" + EXPECTED_3D = [ + 1e-7, + 1e-5, + 1e-5, + 3.823807613470224e-01, + 1e-3, + 4e-2, + 1e0, + 0e0, + 0e0, + 9.999999747378752e-05, + ] + [0.0 for i in range(1, 21)] + for i, param in enumerate(dif3d.FILE_SPEC_3D_PARAMS): + self.assertEqual(self.df.threeD[param], EXPECTED_3D[i]) + + def test__rw4DRecord(self): + """Verify the optimum overrelaxation factors""" + self.assertEqual(self.df.fourD, None) + + def test__rw5DRecord(self): + """Verify the axial coarse-mesh rebalance boundaries""" + self.assertEqual(self.df.fiveD, None) + + def test_writeBinary(self): + """Verify binary equivalence of written DIF3D file.""" + with TemporaryDirectoryChanger(): + dif3d.Dif3dStream.writeBinary(self.df, "DIF3D2") + with open(SIMPLE_HEXZ_DIF3D, "rb") as f1, open("DIF3D2", "rb") as f2: + expectedData = f1.read() + actualData = f2.read() + for expected, actual in zip(expectedData, actualData): + self.assertEqual(expected, actual) + + +class TestDif3dEmptyRecords(unittest.TestCase): + def test_empty4and5Records(self): + """Since the inputs results in these being None, get test coverage another way""" + df = dif3d.Dif3dStream.readBinary(SIMPLE_HEXZ_DIF3D) + # Hack some values that allow 4 and 5 records to be populated \ + # and then populate them + df.twoD["NUMORP"] = 1 + df.twoD["NCMRZS"] = 1 + df.fourD = {"OMEGA1": 1.0} + df.fiveD = {"ZCMRC1": 1.0, "NZINTS1": 10} + with TemporaryDirectoryChanger(): + # Write then read a new one + dif3d.Dif3dStream.writeBinary(df, "DIF3D2") + df2 = dif3d.Dif3dStream.readBinary("DIF3D2") + # Kinda a null test, but this coverage caught some code mistakes! + self.assertEqual(df2.fourD, df.fourD) + self.assertEqual(df2.fiveD, df.fiveD) diff --git a/armi/physics/neutronics/__init__.py b/armi/physics/neutronics/__init__.py index e4b35155a..89c1e6ed9 100644 --- a/armi/physics/neutronics/__init__.py +++ b/armi/physics/neutronics/__init__.py @@ -141,6 +141,7 @@ def getReportContents(r, cs, report, stage, blueprint): PMATRX_EXT = "pmatrx" GAMISO_EXT = "gamiso" ISOTXS = "ISOTXS" +DIF3D = "DIF3D" # Constants for neutronics calculation types ADJOINT_CALC = "adjoint"