Skip to content

Commit

Permalink
Draft: Particle fuel modeling
Browse files Browse the repository at this point in the history
Provides a yaml-interface for
- modeling particle fuel with arbitrary layers
- multiple particle fuel types in the reactor
- assigning particle fuel as children to some parent component

We have been decently successful with these changes internally in that
downstream plugins can see `Component.particleFuel` and perform actions
based on their content. What follows is an overview of the interface,
implementation, and a discussion of where to go next.

Related to terrapower#228 and
would support modeling the MHTGR-350 benchmark
terrapower#224

This patch is submitted to kick-start a discussion on better ways to add
this feature into ARMI, leveraging the domain knowledge of the ARMI
developers and the "particle fuel aware" plugins internally developed at
USNC Tech.

Input interface
---------------

```yaml
particle fuel:
    demo:
        kernel:
            material: UO2
            id: 0
            od: 0.6
            Tinput: 900
            Thot: 900
            flags: DEPLETABLE
        buffer:
            material: SiC
            id: 0.6
            od: 0.61
            Tinput: 900
            Thot: 900
```

```yaml
matrix:
    shape: Circle
    material: Graphite
    Tinput: 1200
    Thot: 1200
    id: 0.0
    od: 2.2
    latticeIDs: [F]
    flags: DEPLETABLE
    particleFuelSpec: demo
    particleFuelPackingFraction: 0.4

```

With this interface it's possible to define several specifications
in the model and assign them to different cylindrical components.

Implementation
--------------

The particle fuel is stored as a child of the parent component, such
that `<Circle: Matrix>.children` is used to dynamically find the particle
fuel spec. We can't store the specification as an attribute because we
have to support potentially dynamic addition and removal of children to this
component. Something like `self.particleFuel = spec` that makes spec a
child would also have to understand what happens if we remove the spec
from the parent, e.g., `self.remove(self.particleFuel)`. What is then the
outcome of `self.particleFuel` unless we always check the children of the
matrix?

By making the particle fuel spec a `Composite` and placing it in the
`.children` of the parent, the spec is able to be written to and read
from the database.

Adds `Component.setParticleMultiplicity`. The method is called during
block construction when the block's height is available to the matrix
component (particle's parent). The multiplicity is determined from the
matrix volume and target packing fraction.

Note: the particle mult is, by design, for a single component.

Unresolved issues
-----------------

- Volume of the parent matrix is not reduced by the volume occupied
by the particles.
- No support for homogenizing regions that contain particle fuel
- Various homogenization properties don't account for particle fuel
  (e.g., `Core.getHM*`)
- Particle fuel is not included in some text-reporting, leading to
statements like

```
[info] Nuclide categorization for cross section temperature assignments:
       ------------------  ------------------------------------------------------
       Nuclide Category    Nuclides
       ------------------  ------------------------------------------------------
       Fuel
```
and
```
[warn] The system has no heavy metal and therefore is not a nuclear reactor. Please make sure that this is intended and not a input error.
```

- No provided routines for packing the particles into their parent. This
  could be facilitated with a dedicated packing plugin and an unstructured
  3-D `SpatialGrid` class. It's burdensome to expect the user to define
  the exact location of _every_ particle every time. But, if some `Plugin`
  performs the packing and creates this spatial grid, the multiplicity is
  tackled, and you potentially avoid adding or removing particles as their
  parent expands or contracts.
- Unsure if material modifications make their way down to the materials
  in the particle fuel spec. The implementation suggests it as the `matMods`
  argument is passed into the particle fuel YAML object constructor. But
  we have yet to stress test that

Next steps
----------

This patch is submitted because we continue to find places where this
approach does not play well with the rest of the ARMI stack. While Blocks
that contain particle fuel are correctly able to compute their heavy
metal mass by iterating over their children, which in turn finds the
particle fuel. However, higher-level actions like `Core.getHM*` do not
go down to the sub-block level, instead asking to homogenize Blocks.
The homogenization methods are not yet aware of the particle fuel because
they rely on `Component.getNuclides` which reports the nuclides for it's
`Material`, and does not include the children.

This approach is sensible because if I'm writing a neutronic input file
and I can exactly model the matrix and it's particle fuel, I would expect
`matrix.getNuclides` to return the nuclides for _just the matrix_. Then,
being informed of the particle fuel, I can write those materials and geometry
uniquely. However, codes that cannot handle particle fuel and/or work
with homogenized regions (e.g., nodal diffusion) would need this
homogenized data. Allowing `Component.getNuclides` to return the nuclides
on the child particle fuel would support this case, but not the previous case.

My speculation is that the optimal strategy lies somewhere in making the
matrix object not a `Component` but a `Block` and having the ARMI Composite
tree accept blocks that potentially contain blocks. I think this is valid
using the API but the user interface would need some work. Having the matrix
be a block would provide a better interface for homogenization and exact
representation of the particle fuel. I think...

Other related changes
---------------------

Added `Sphere.getBoundingCircleOuterDiameter` so that the particle fuel
composites can be properly added to the database. They need to be
"sortable" other wise `Database3._createLayout` breaks trying to sort
components. This enables the particle fuel spec to be added to and read
from the HDF data file

The material constructor is a more public function as it is needed both
in creating `Components` but also in creating the particle fuel spec.

Added `MATRIX` flag

Signed-off-by: Andrew Johnson <a.johnson@usnc-tech.com>
  • Loading branch information
drewj-usnctech committed Nov 17, 2021
1 parent 47d3cc7 commit 86702a5
Show file tree
Hide file tree
Showing 15 changed files with 963 additions and 41 deletions.
3 changes: 3 additions & 0 deletions armi/bookkeeping/report/reportInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def interactBOL(self):
runLog.important("Beginning of BOL Reports")
reportingUtils.makeCoreAndAssemblyMaps(self.r, self.cs)
reportingUtils.writeAssemblyMassSummary(self.r)
reportingUtils.makeParticleFuelDesignReport(self.r)

if self.cs["summarizeAssemDesign"]:
reportingUtils.summarizePinDesign(self.r.core)
Expand Down Expand Up @@ -115,6 +116,8 @@ def generateDesignReport(self, generateFullCoreMap, showBlockAxMesh):
)
reportingUtils.makeBlockDesignReport(self.r)

reportingUtils.makeParticleFuelDesignReport(self.r)

def interactEOL(self):
"""Adds the data to the report, and generates it"""
b = self.o.r.core.getFirstBlock(Flags.FUEL)
Expand Down
85 changes: 85 additions & 0 deletions armi/bookkeeping/report/reportingUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import time
import tabulate
from copy import copy
from math import nan

import numpy

Expand Down Expand Up @@ -56,6 +57,8 @@
Operator_MasterMachine = "Master Machine:"
Operator_Date = "Date and Time:"
Operator_CaseDescription = "Case Description:"
# Convert a value in centimeters to micrometers
CM_TO_MICRO_METER = 10000


def writeWelcomeHeaders(o, cs):
Expand Down Expand Up @@ -1002,3 +1005,85 @@ def makeCoreAndAssemblyMaps(r, cs, generateFullCoreMap=False, showBlockAxMesh=Tr


COMPONENT_INFO = "Component Information"


def makeParticleFuelDesignReport(r):
"""Add reports for particle fuel specification and usage
Parameters
----------
r : Reactor
Reactor containing particle fuel designs. If none are found, no
work is done.
"""
if not r.blueprints.particleFuelDesigns:
return

descriptions = {}

for name, design in r.blueprints.particleFuelDesigns.items():
rows = _particleFuelSpecToTable(design)
descriptions[name] = {
"spec": rows,
"usage": [],
}

# Find all usages of each specification
for blockDesign in r.blueprints.blockDesigns.values():
for componentDesign in blockDesign.values():
thisParticleFuel = componentDesign.particleFuelSpec
if thisParticleFuel is None:
continue
descriptions[thisParticleFuel]["usage"].append(
(
f"{blockDesign.name} {componentDesign.name}",
f"{componentDesign.particleFuelPackingFraction}",
)
)

for name, subdata in descriptions.items():
grp = report.data.Table(f"{name} Specification")
report.setData(
"Layer",
"Material, Outer diameter (μm), Thickness (μm)",
group=grp,
reports=report.DESIGN,
)
for layerName, layerData in subdata["spec"]:
report.setData(layerName, layerData, grp, report.DESIGN)

grp = report.data.Table(f"{name} Usage")
if subdata["usage"]:
report.setData("Block and Component name", "Packing fraction")
for desc, pf in subdata["usage"]:
report.setData(desc, pf, grp, report.DESIGN)
else:
msg = f"Particle fuel specification {name} not used"
report.setData("WARNING", msg, grp, report.DESIGN)
runLog.warning(msg)

return


def _particleFuelSpecToTable(spec) -> list:
rows = []
names = []
prevOD = None
for layer in sorted(spec.values(), key=lambda ring: ring.od):
od = layer.od
if prevOD is not None:
thickness = round(CM_TO_MICRO_METER * 0.5 * (od - prevOD))
else:
thickness = "-"
names.append(layer.name)
rows.append(
"{}, {}, {}".format(
layer.material,
round(od * CM_TO_MICRO_METER, 6),
thickness,
)
)
prevOD = od

return list(zip(names, rows))
4 changes: 4 additions & 0 deletions armi/reactor/blueprints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
from armi.reactor.blueprints.blockBlueprint import BlockKeyedList
from armi.reactor.blueprints import isotopicOptions
from armi.reactor.blueprints.gridBlueprint import Grids, Triplet
from armi.reactor.blueprints.componentBlueprint import ParticleFuelKeyedList

context.BLUEPRINTS_IMPORTED = True
context.BLUEPRINTS_IMPORT_CONTEXT = "".join(traceback.format_stack())
Expand Down Expand Up @@ -185,6 +186,9 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
)
systemDesigns = yamlize.Attribute(key="systems", type=Systems, default=None)
gridDesigns = yamlize.Attribute(key="grids", type=Grids, default=None)
particleFuelDesigns = yamlize.Attribute(
key="particle fuel", type=ParticleFuelKeyedList, default=None
)

# These are used to set up new attributes that come from plugins. Defining its
# initial state here to make pylint happy
Expand Down
8 changes: 8 additions & 0 deletions armi/reactor/blueprints/blockBlueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ def construct(
b.autoCreateSpatialGrids()
except (ValueError, NotImplementedError) as e:
runLog.warning(str(e), single=True)
# check if particle fuel exists and set particle mult
# Note: these changes occur here during block construction instead of during
# component contruction because component parent parameters (i.e., height)
# are needed for volume calculations
for component in b.getChildren():
if component.particleFuel:
component.setParticleMultiplicity()

return b

def _getGridDesign(self, blueprint):
Expand Down
Loading

0 comments on commit 86702a5

Please sign in to comment.