Source code for armi.nuclearDataIO.cccc.nhflux

# 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.
"""
NHFLUX is a CCCC interface file that stores flux moments and partial currents from
DIF3D-Nodal and DIF3D-VARIANT.

Examples
--------
>>> nhfluxData = NfluxStream.readBinary("NHFLUX")
>>> NhfluxStream.writeAscii(nhfluxData, "nhflux.ascii")

"""
import numpy as np

from armi.nuclearDataIO import cccc

FILE_SPEC_1D_KEYS = (
    "ndim",
    "ngroup",
    "ninti",
    "nintj",
    "nintk",
    "iter",
    "effk",
    "power",
    "nSurf",
    "nMom",
    "nintxy",
    "npcxy",
    "nscoef",
    "itrord",
    "iaprx",
    "ileak",
    "iaprxz",
    "ileakz",
    "iorder",
)

FILE_SPEC_1D_KEYS_VARIANT11 = (
    "npcbdy",
    "npcsym",
    "npcsec",
    "iwnhfl",
    "nMoms",
)


[docs]class NHFLUX(cccc.DataContainer): """ An abstraction of a NHFLUX file. This format is defined in the DIF3D manual. Note that the format for DIF3D-Nodal and DIF3D-VARIANT are not the same. The VARIANT NHFLUX format has recently changed, so this reader is only compatible with files produced by v11.0 of the solver. .. warning:: DIF3D outputs NHFLUX at every time node, but REBUS outputs NHFLUX only at every cycle. See also [VARIANT-95]_ and [VARIANT-2014]_. .. [VARIANT-95] G. Palmiotti, E. E. Lewis, and C. B. Carrico, VARIANT: VARIational Anisotropic Nodal Transport for Multidimensional Cartesian and Hexagonal Geometry Calculation, ANL-95/40, Argonne National Laboratory, Argonne, IL (October 1995). .. [VARIANT-2014] Smith, M. A., Lewis, E. E., and Shemon, E. R. DIF3D-VARIANT 11.0: A Decade of Updates. United States: N. p., 2014. Web. doi:10.2172/1127298. https://publications.anl.gov/anlpubs/2014/04/78313.pdf Attributes ---------- metadata : file control The NHFLUX file control info (sort of global for this library). This is the contents of the 1D data block on the file. incomingPointersToAllAssemblies: 2-D list of floats This is an index map for the "internal surfaces" between DIF3D nodal indexing and DIF3D GEODST indexing. It can be used to process incoming partial currents. This uses the same ordering as the geodstCoordMap attribute. externalCurrentPointers : list of ints This is an index map for the "external surfaces" between DIF3D nodal indexing and DIF3D GEODST indexing. "External surfaces" are important because they contain the INCOMING partial currents from the outer reactor boundary. This uses the same ordering as geodstCoordMap, except that each assembly now has multiple subsequent indices. For example, for a hexagonal core, if hex of index n (0 to N-1) has a surface of index k (0 to 5) that lies on the vacuum boundary, then the index of that surface is N*6 + k + 1. geodstCoordMap : list of ints This is an index map between DIF3D nodal and DIF3D GEODST. It is necessary for interpreting the ordering of flux and partial current data in the NHFLUX file. Note that this mapping between DIF3D-Nodal and DIF3D-VARIANT is not the same. outgoingPCSymSeCPointers: list of ints This is an index map for the outpgoing partial currents on the symmetric and sector lateral boundary. It is only present for DIF3D-VARIANT for hexagonal cores. ingoingPCSymSeCPointers: list of ints This is an index map for the ingoing (or incoming) partial currents on the symmetric and sector lateral boundary. It is only present for DIF3D-VARIANT for hexagonal cores. fluxMomentsAll : 4-D list of floats This contains all the flux moments for all core assemblies. The jth planar flux moment of assembly i in group g in axial node k is fluxMoments[i][k][j][g]. The assemblies are ordered according to the geodstCoordMap attribute. For DIF3D-VARIANT, this includes both even and odd parity moments. partialCurrentsHexAll : 5-D list of floats This contains all the OUTGOING partial currents for all core assemblies. The OUTGOING partial current on surface j in assembly i in axial node k in group g is partialCurrentsHex[i][k][j][g][m], where m=0. The assemblies are ordered according to the geodstCoordMap attribute. For DIF3D-VARIANT, higher-order data is available for the m axis. partialCurrentsHex_extAll : 4-D list of floats This contains all the INCOMING partial currents on "external surfaces", which are adjacent to the reactor outer boundary (usually vacuum). Internal reflective surfaces are NOT included in this! These "external surfaces" are ordered according to externalCurrentPointers. For DIF3D-VARIANT, higher-order data is available for the last axis. partialCurrentsZAll : 5-D list of floats This contains all the upward and downward partial currents for all core assemblies The assemblies are ordered according to the geodstCoordMap attribute. For DIF3D-VARIANT, higher-order data is available for the last axis. """ def __init__(self, fName="NHFLUX", variant=False, numDataSetsToRead=1): """ Initialize the NHFLUX or NAFLUX reader object. Parameters ---------- fName : str, optional Filename of the NHFLUX binary file to be read. variant : bool, optional Whether or not this NHFLUX/NAFLUX file has the DIF3D-VARIANT output format, which is different than the DIF3D-Nodal format. """ cccc.DataContainer.__init__(self) self.metadata["variantFlag"] = variant self.metadata["numDataSetsToRead"] = numDataSetsToRead # Initialize instance array variables self.incomingPointersToAllAssemblies: np.ndarray = np.array([]) self.externalCurrentPointers: np.ndarray = np.array([]) self.geodstCoordMap: np.ndarray = np.array([]) if self.metadata["variantFlag"]: self.outgoingPCSymSecPointers: np.ndarray = np.array([]) self.ingoingPCSymSecPointers: np.ndarray = np.array([]) self.fluxMomentsAll: np.ndarray = np.array([]) self.partialCurrentsHexAll: np.ndarray = np.array([]) self.partialCurrentsHex_extAll: np.ndarray = np.array([]) self.partialCurrentsZAll: np.ndarray = np.array([]) @property def fluxMoments(self): """ For DIF3D-Nodal, this property is equivalent to the attribute `fluxMomentsAll`. For DIF3D-VARIANT, this property represents the even-parity flux moments. Read-only property (there is no setter). """ nMom = self.metadata["nMom"] return self.fluxMomentsAll[..., :nMom, :] @property def partialCurrentsHex(self): """ For DIF3D-Nodal, this property is almost always equivalent to the attribute `partialCurrentsHex`. For DIF3D-VARIANT, this property returns the zeroth-order moment of the outgoing radial currents. Read-only property (there is no setter). """ return self.partialCurrentsHexAll[..., 0] @property def partialCurrentsHex_ext(self): """ For DIF3D-Nodal, this property is almost always equivalent to the attribute `partialCurrentsHex_ext`. For DIF3D-VARIANT, this property returns the zeroth-order moment of the incoming/ingoing radial currents. Read-only property (there is no setter). """ return self.partialCurrentsHex_extAll[..., 0] @property def partialCurrentsZ(self): """ For DIF3D-Nodal, this property is almost always equivalent to the attribute `partialCurrentsZ`. For DIF3D-VARIANT, this property returns the zeroth-order moment of the axial currents. Read-only property (there is no setter). """ return self.partialCurrentsZAll[..., 0]
[docs]class NhfluxStream(cccc.StreamWithDataContainer): @staticmethod def _getDataContainer() -> NHFLUX: return NHFLUX()
[docs] def readWrite(self): r""" Read everything from the DIF3D binary file NHFLUX. Read all surface-averaged partial currents, all planar moments, and the DIF3D nodal coordinate mapping system. Notes ----- This method should be private but conflicts with ``_readWrite`` so we need a better name. Parameters ---------- numDataSetsToRead : int, optional The number of whole-core flux data sets included in this NHFLUX/NAFLUX file that one wishes to be read. Some NHFLUX/NAFLUX files, such as NAFLUX files written by SASSYS/DIF3D-K, contain more than one flux data set. Each data set overwrites the previous one on the NHFLUX class object, which will contain only the numDataSetsToRead-th data set. The first numDataSetsToRead-1 data sets are essentially skipped over. """ self._rwFileID() self._rwBasicFileData1D() # This control info only exists for VARIANT. We can only process entries with 0 # or 1. if self._metadata["variantFlag"] and self._metadata["iwnhfl"] == 2: msg = ( "This reader can only read VARIANT NHFLUX files where 'iwnhfl'=0 (both " "fluxes and currents are present) or 'iwnhfl'=1 (only fluxes are present). " ) raise ValueError(msg) # Read the hex ordering map between DIF3D nodal and DIF3D GEODST. Also read index # pointers to incoming partial currents on outer reactor surface (these don't # belong to any assembly). Incoming partial currents are non-zero due to flux # extrapolation self._rwGeodstCoordMap2D() # Number of energy groups ng = self._metadata["ngroup"] # Number of axial nodes (same for each assembly in DIF3D) nz = self._metadata["nintk"] # Number of XY partial currents on the boundary. Note that for the same model, this # number is not the same between Nodal and VARIANT; VARIANT has more. numPartialCurrentsHex_ext = ( self._metadata["npcxy"] - self._metadata["nintxy"] * self._metadata["nSurf"] ) # Typically, flux and current data has units of n/cm^2/s. However, when reading # an NHFLUX file produced by VARPOW (where 'iwnhfl'=1), the flux-only data has units # of W/cc (there is no current data written to the file). if self._data.fluxMomentsAll.size == 0: # Initialize using metadata info for reading totalMoments = ( self._metadata["nMom"] if not self._metadata["variantFlag"] else (self._metadata["nMom"] + self._metadata["nMoms"]) ) self._data.fluxMomentsAll = np.zeros( (self._metadata["nintxy"], nz, totalMoments, ng) ) if self._metadata["iwnhfl"] != 1: self._data.partialCurrentsHexAll = np.zeros( ( self._metadata["nintxy"], nz, self._metadata["nSurf"], ng, self._metadata["nscoef"], ) ) self._data.partialCurrentsHex_extAll = np.zeros( (numPartialCurrentsHex_ext, nz, ng, self._metadata["nscoef"]) ) self._data.partialCurrentsZAll = np.zeros( (self._metadata["nintxy"], nz + 1, 2, ng, self._metadata["nscoef"]) ) for _n in range(self._metadata["numDataSetsToRead"]): # Each record contains nodal data for ONE energy group in ONE axial core slice. # Must loop through all energy groups and all axial core slices. # The axial surface partial currents are indexed by axial surface (NOT by axial node), # so there are nz+1 records for z-surface currents # Loop through all energy groups: high-to-low for forward flux, low-to-high for # adjoint flux for g in range(ng): gEff = self._getEnergyGroupIndex(g) # Loop through axial nodes for z in range(nz): # Process flux moments self._data.fluxMomentsAll[:, z, :, gEff] = self._rwFluxMoments3D( self._data.fluxMomentsAll[:, z, :, gEff] ) # Process currents if self._metadata["iwnhfl"] != 1: # Loop through axial nodes for z in range(nz): ( self._data.partialCurrentsHexAll[:, z, :, gEff, :], self._data.partialCurrentsHex_extAll[:, z, gEff, :], ) = self._rwHexPartialCurrents4D( self._data.partialCurrentsHexAll[:, z, :, gEff, :], self._data.partialCurrentsHex_extAll[:, z, gEff, :], ) # Loop through axial surfaces (NOT axial nodes, because there is a "+1") for z in range(nz + 1): self._data.partialCurrentsZAll[ :, z, :, gEff, : ] = self._rwZPartialCurrents5D( self._data.partialCurrentsZAll[:, z, :, gEff, :] )
def _getNumOuterSurfacesHex(self): """ The word "outer" in the method name means along the outside of the core. Thus, this is the number of lateral hex surfaces on the outer core boundary (usually vacuum...internal reflective boundaries do NOT count). """ # Both Nodal and VARIANT files should return the same number, but they are calculated # differently between the two codes if self._metadata["variantFlag"]: numOuterSurfacesHex = self._metadata["npcbdy"] else: # Nodal does not have an "npcbdy" metadata parameter, so numOuterSurfacesHex # must be calculated differently. Performing the same calculation below in VARIANT, # which is possible to do, can return a different number, so that is why # we cannot use the same calculation for both codes. numOuterSurfacesHex = ( self._metadata["npcxy"] - self._metadata["nintxy"] * self._metadata["nSurf"] ) return numOuterSurfacesHex def _rwFileID(self): """ Read/write file id record. Notes ----- The username, version, etc are embedded in this string but it's usually blank. """ with self.createRecord() as record: self._metadata["label"] = record.rwString(self._metadata["label"], 28) def _rwBasicFileData1D(self): """ Read basic data parameters (number of energy groups, assemblies, axial nodes, etc.). """ # Dummy values are stored because sometimes they get assigned # unexpected values anyway, and so we still want to preserve those values anyway if self._metadata["variantFlag"]: keys = ( FILE_SPEC_1D_KEYS + FILE_SPEC_1D_KEYS_VARIANT11 + tuple(f"IDUM{e:>02}" for e in range(1, 7)) ) else: keys = FILE_SPEC_1D_KEYS + tuple( tuple(f"IDUM{e:>02}" for e in range(1, 12)) ) with self.createRecord() as record: self._metadata.update(record.rwImplicitlyTypedMap(keys, self._metadata)) def _rwGeodstCoordMap2D(self): """ Read/write core geometry indexing from the NHFLUX 2D block. This reads the 2-D (x,y) indexing for assemblies. geodstCoordMap maps DIF3D nodal hex indexing to DIF3D GEODST indexing. This DIF3D GEODST indexing is different than (but similar to) the MCNP GEODST ordering. For Nodal, let N be the number of assemblies. Let M be the number of "external hex surfaces" exposed to the outer reactor boundary (usually vacuum). M does NOT include reflective surfaces! N = self._metadata['nintxy'] M = self._metadata['npcxy'] - self._metadata['nintxy']*6 N*6 + M = self._metadata['npcxy'] For VARIANT in hexagonal geometry, there are two additional datasets for outgoing and ingoing partial currents on the symmetric and sector xy-plane boundary. Examples -------- geodstCoordMap[NodalIndex] = geodstIndex See Also -------- nuclearDataIO.NHFLUX.__init__ nuclearDataIO.NHFLUX._rwHexPartialCurrents4D nuclearDataIO.ISOTXS.read2D nuclearDataIO.SPECTR.read2D """ with self.createRecord() as record: # Number of unique assemblies - this is N in the comments above nAssem = self._metadata["nintxy"] # Number of lateral surfaces per assembly (this is 6 for hexagonal cores) nSurf = self._metadata["nSurf"] numExternalSurfaces = self._getNumOuterSurfacesHex() # Initialize np arrays to store all node ordering (and node surface ordering) # data. We don't actually use incomingPointersToAllAssemblies (basically # equivalent to nearest neighbors indices), but it's here in case someone # needs it in the future. # Initialize data size when reading if self._data.incomingPointersToAllAssemblies.size == 0: # Index pointers to INCOMING partial currents on assemblies self._data.incomingPointersToAllAssemblies = np.zeros( (nSurf, nAssem), dtype=int ) # Index pointers to OUTGOING partial currents on core outer boundary self._data.externalCurrentPointers = np.zeros( (numExternalSurfaces), dtype=int ) # Index pointers to DIF3D GEODST ordering of each assembly self._data.geodstCoordMap = np.zeros(nAssem, dtype=int) self._data.incomingPointersToAllAssemblies = record.rwIntMatrix( self._data.incomingPointersToAllAssemblies, nAssem, nSurf ) self._data.externalCurrentPointers = record.rwList( self._data.externalCurrentPointers, "int", numExternalSurfaces ) self._data.geodstCoordMap = record.rwList( self._data.geodstCoordMap, "int", nAssem ) # There is additional data to process for VARIANT if self._metadata["variantFlag"]: # Number of symmetry and sector surface pointers npcsto = self._metadata["npcsym"] + self._metadata["npcsec"] if self._data.outgoingPCSymSecPointers.size == 0: self._data.outgoingPCSymSecPointers = np.zeros(npcsto, dtype=int) self._data.ingoingPCSymSecPointers = np.zeros(npcsto, dtype=int) self._data.outgoingPCSymSecPointers = record.rwList( self._data.outgoingPCSymSecPointers, "int", npcsto ) self._data.ingoingPCSymSecPointers = record.rwList( self._data.ingoingPCSymSecPointers, "int", npcsto ) def _rwFluxMoments3D(self, contents): r""" Read/write multigroup flux moments from the NHFLUX 3D block. This reads/writes the planar moments for each DIF3D node on ONE x,y plane. The planar moments for DIF3D nodes on different x,y planes (different axial slices) are in a different 3D record, so this method must be repeatedly executed in order to process them all. Format is ``((FLUX(I,J),I=1,NMOM),J=1,NINTXY)`` so we must pass in ``NINTXY`` as the first item in the shape. However, the caller of this method wants the shape to be (nintxy, nMom) so we actually have to transpose it on the way in/out. nMom can also be nMoms when reading/writing for VARIANT. """ nMom = self._metadata["nMom"] with self.createRecord() as record: result = record.rwDoubleMatrix( contents[:, :nMom].T, self._metadata["nintxy"], nMom, ) contents[:, :nMom] = result.T # If we have VARIANT data, then we also need to process the odd-parity moments. if self._metadata["variantFlag"] and self._metadata["nMoms"] > 0: result = record.rwDoubleMatrix( contents[:, nMom:].T, self._metadata["nintxy"], self._metadata["nMoms"], ) contents[:, nMom:] = result.T return contents def _rwHexPartialCurrents4D(self, surfCurrents, externalSurfCurrents): r""" Read/write multigroup lateral partial OUTGOING currents from the NHFLUX 4D block. This reads all OUTGOING partial currents for all assembly block lateral surfaces at a fixed axial position. For a hexagonal core, there are 6 surfaces per assembly axial block. The data for the 2 axial surfaces of each block are in the 5D records. Each 4D record contains all the surface partial currents on ONE x,y plane. The surface data on different x,y planes (different axial slices) are in a different 4D record, so this method must be repeatedly executed in order to process them all. If the reactor contains N assemblies and M exterior surfaces (surfaces adjacent to vacuum boundary), this record will contain N*6 + M partial currents. The N*6 assembly OUTGOING partial currents are listed first, followed by the M INCOMING partial currents from the outer reactor edge. N = self._metadata['nintxy'] M = self._metadata['npcxy'] - self._metadata['nintxy']*6 N*6 + M = self._metadata['npcxy'] Notes ----- These data are harder to read with rwMatrix, though it could be done if we discarded the unwanted data at another level if that is much faster. """ with self.createRecord() as record: nAssem = self._metadata["nintxy"] nSurf = self._metadata["nSurf"] # This is equal to one for Nodal diffusion theory, but greater than one for # VARIANT. nscoef = self._metadata["nscoef"] numPartialCurrentsHex_ext = ( self._metadata["npcxy"] - self._metadata["nintxy"] * self._metadata["nSurf"] ) # Loop through all lateral surfaces of all assemblies for i in range(nAssem): for j in range(nSurf): for m in range(nscoef): # OUTGOING partial currents on each lateral surface in each assembly. # If m > 0, other NSCOEF options (i.e., half-angle integrated # flux when reading DIF3D-Nodal data, and higher current moments # when reading DIF3D-VARIANT data) are processed. surfCurrents[i, j, m] = record.rwDouble(surfCurrents[i, j, m]) for j in range(numPartialCurrentsHex_ext): for m in range(nscoef): # INCOMING current at each surface of outer core boundary. If m > 0, # other NSCOEF options (i.e., half-angle integrated flux when # reading DIF3D-Nodal data, and higher current moments when reading # DIF3D-VARIANT data) are processed. externalSurfCurrents[j, m] = record.rwDouble( externalSurfCurrents[j, m] ) return surfCurrents, externalSurfCurrents def _rwZPartialCurrents5D(self, surfCurrents): """ Read/write multigroup axial partial currents from the NHFLUX 5D block. Most other NHFLUX data is indexed by DIF3D node (each axial core slice in its own record). HOWEVER, "top" and "bottom" surfaces of each DIF3D node are instead indexed by axial surface. If there are Z axial nodes, then there are Z+1 axial surfaces. Thus, there are Z+1 5D records, while there are only Z 3D and Z 4D records. Each 5D record (each axial surface) contains two partial currents for each assembly position. The first is the UPWARD partial current, while the second is the DOWNWARD partial current. Returns ------- surfCurrents : 3-D list of floats This contains all the upward and downward partial currents in all assemblies on ONE whole-core axial slice. The assemblies are ordered according to self.geodstCoordMap. See Also -------- nuclearDataIO.NHFLUX._rwBasicFileData1D nuclearDataIO.NHFLUX._rwGeodstCoordMap2D """ with self.createRecord() as record: nAssem = self._metadata["nintxy"] nSurf = 2 nscoef = self._metadata["nscoef"] # Loop through all (up and down) partial currents on all hexes # These loops are in a different order than in the 4D record above!!! # Here we loop through surface FIRST and assemblies SECOND!!! for j in range(nSurf): for i in range(nAssem): for m in range(nscoef): # Outward partial current. For m > 0, other NSCOEF options # (i.e., half-angle integrated flux when reading DIF3D-Nodal # data, and higher current moments when reading DIF3D-VARIANT # data) are processed. surfCurrents[i, j, m] = record.rwDouble(surfCurrents[i, j, m]) return surfCurrents def _getEnergyGroupIndex(self, g): r""" Real fluxes stored in NHFLUX have "normal" (or "forward") energy groups. Also see the subclass method NAFLUX.getEnergyGroupIndex(). """ return g
[docs]class NafluxStream(NhfluxStream): """ NAFLUX is similar in format to the NHFLUX, but contains adjoint flux. It has reversed energy group ordering. """ def _getEnergyGroupIndex(self, g): r""" Adjoint fluxes stored in NAFLUX have "reversed" (or "backward") energy groups. """ ng = self._metadata["ngroup"] return ng - g - 1
[docs]class NhfluxStreamVariant(NhfluxStream): """ Stream for VARIANT version of NHFLUX. Notes ----- Can be deleted after have the NHFLUX data container be the public interface """ @staticmethod def _getDataContainer() -> NHFLUX: return NHFLUX(variant=True)
[docs]class NafluxStreamVariant(NafluxStream): """ Stream for VARIANT version of NAFLUX. Notes ----- Can be deleted after have the NHFLUX data container be the public interface """ @staticmethod def _getDataContainer() -> NHFLUX: return NHFLUX(variant=True)
[docs]def getNhfluxReader(adjointFlag, variantFlag): r""" Returns the appropriate DIF3D nodal flux binary file reader class, either NHFLUX (real) or NAFLUX (adjoint). """ if adjointFlag: reader = NafluxStreamVariant if variantFlag else NafluxStream else: reader = NhfluxStreamVariant if variantFlag else NhfluxStream return reader