Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove reliance on Composite.iterComponents #1211

Closed
drewj-usnctech opened this issue Mar 9, 2023 · 2 comments · Fixed by #1255
Closed

Remove reliance on Composite.iterComponents #1211

drewj-usnctech opened this issue Mar 9, 2023 · 2 comments · Fixed by #1255

Comments

@drewj-usnctech
Copy link
Contributor

drewj-usnctech commented Mar 9, 2023

Potentially hot take here but I'll explain my motivation

Claim

There are a collection of places in ARMI where a composite does something like

def thing(self):
    return sum([c.thing() for c in self.iterComponents()])

when they really should do

def thing(self):
    return sum((c.thing() for c in self))

(minor tweak to use a generator over a list but that's not the point of this ticket)

This works well and good with the current ARMI tree of Assembly -> Block -> Component. The problem comes when something comes between blocks and components that acts a little different to how ARMI expects despite still trying to be good Composite children.

Motivation

I found these cases working on the particle fuel related issues (#1055 #1134). There, we have the following design pattern

With the understanding of mult to be the number of a thing in it's parent, the multiplicity of one of the spherical components in the particle fuel is one. There is one kernel in a standard TRISO. But the ParticleFuel has a multiplicity equal to the number of particles in the matrix (on order of thousands). The matrix child has a multiplicity of 1 - packingFraction to adjust it's mass correctly by accounting for the region occupied by particle fuel. And finally, the multiplicity of the Compact is the number of compacts in the block.

When we do various summation routines, like Block.getHmMass for a Block that also contains a Compact, the mass of the kernel is counted as just the mass of a single layer in one particle because we jump straight down to the spherical components with Block.iterComponents. We don't capture Sphere.getHmMass() * nParticlesPerCompact * nCompactsPerBlock

However, if Compact and ParticleFuel provide their own getters that account for their multiplicity, and Composite methods act more like aggregators of child.thing, we can correct hit masses and volumes and densities.

Occurrences

We've developed the compact feature fairly completely and it's able to do mass accounting etc. after we made changes to the following locations. In order to get this compact in, we had to remove calls to iterComponents in the following places

Block.completeInitialLoading

for child in self.iterComponents():

Block.getLargestComponent

for c in self.iterComponents():

Might need a new name? But also Composite.getDimension isn't abstract so it's not required that composites implement it

Composite.getMass

[c.getMass(nuclideNames=nuclideNames) for c in self.iterComponents()]

Composite.getNuclideNumberDensities

def getNuclideNumberDensities(self, nucNames):
"""Return a list of number densities in atoms/barn-cm for the nuc names requested."""
volumes = numpy.array(
[
c.getVolume() / (c.parent.getSymmetryFactor() if c.parent else 1.0)
for c in self.iterComponents()
]
) # c x 1
totalVol = volumes.sum()
if totalVol == 0.0:
# there are no children so no volume or number density
return [0.0] * len(nucNames)
densListForEachComp = []
for c in self.iterComponents():
numberDensityDict = c.getNumberDensities()
densListForEachComp.append(
[numberDensityDict.get(nuc, 0.0) for nuc in nucNames]
)
nucDensForEachComp = numpy.array(densListForEachComp) # c x n
return volumes.dot(nucDensForEachComp) / totalVol

Little more complex because of the need for volumes but we've got a solution that might be acceptable

Composite.getFuelMass

return sum([fuel.getMass() for fuel in self.iterComponents(Flags.FUEL)], 0.0)

Would require Component.getFuelMass to be defined, but it's essentially trivial

def getFuelMass(self) -> float:
    if self.hasFlags(Flags.FUEL):
        return self.getMass()

Composite.getBoundingCircleOuterDiameter

def getBoundingCircleOuterDiameter(self, Tc=None, cold=False):
"""
Get sum circle bound.
Used to roughly approximate relative size vs. other objects
"""
return sum(
[
comp.getBoundingCircleOuterDiameter(Tc, cold)
for comp in self.iterComponents()
]
)

Other

There are more places where Composite.itercomponents is called, and in some places it's probably okay. Calls like Composite.getComponentNames. These are just the cases we found needed to be changed to get a non-Component child of a block to play nice with mass reporting and the run sequence.

Thankfully, because many of the most important actions are funneled through basically getMass and getNuclideNumberDensities, a lot of other great functionality is provided for free

Follow on

My hope is that we can send the Compact up for review end of next week. I can also send the patches we've made to places listed in a separate PR before putting the Compact PR up.

@drewj-usnctech
Copy link
Contributor Author

Related find in crossSectionGroupManager : expecting all children of Block to be Component or at least have a numberDensities parameter

def getBlockNuclideTemperatureAvgTerms(block, allNucNames):
"""
Compute terms (numerator, denominator) of average for this block.
This volume-weights the densities by component volume fraction.
It's important to count zero-density nuclides (i.e. ones like AM242 that are expected to build up)
as trace values at the proper component temperatures.
"""
def getNumberDensityWithTrace(component, nucName):
# needed to make sure temperature of 0-density nuclides in fuel get fuel temperature
try:
dens = component.p.numberDensities[nucName] or TRACE_NUMBER_DENSITY
except KeyError:
dens = 0.0
return dens

In the Compact design, there is no need to have number densities stored directly on the Compact. Rather, there are aggregated from the child matrix Component and "grandchildren" spheres in the ParticleFuel class

Here it's a little trickier because you'd want to pick up temperatures of nuclides in each of those components. Otherwise if you just used the matrix temperature when finding the volume-weighted temperature, it would likely be lower for U235 as matrix temperature is usually less than fuel temperature

@drewj-usnctech
Copy link
Contributor Author

@john-science @jakehader @opotowsky @ntouran your feedbacks would be appreciated here. I have some patches ready to send off if you buy in to this design change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant