Skip to content

Commit

Permalink
Add DIF3D Reader to CCCC (#1309)
Browse files Browse the repository at this point in the history
  • Loading branch information
opotowsky authored Jun 17, 2023
1 parent f08b13b commit 20be875
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 0 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions armi/nuclearDataIO/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# though prefer full imports in new code
from .cccc import (
compxs,
dif3d,
dlayxs,
fixsrc,
gamiso,
Expand Down
215 changes: 215 additions & 0 deletions armi/nuclearDataIO/cccc/dif3d.py
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
162 changes: 162 additions & 0 deletions armi/nuclearDataIO/cccc/tests/test_dif3d.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 20be875

Please sign in to comment.