@@ -375,11 +375,20 @@ def applyMaterialMassFracsToNumberDensities(self):
375375 # `density` is 3D density
376376 # call getProperty to cache and improve speed
377377 density = self .material .getProperty ("pseudoDensity" , Tc = self .temperatureInC )
378-
379378 self .p .numberDensities = densityTools .getNDensFromMasses (
380379 density , self .material .massFrac
381380 )
382381
382+ # Sometimes the material thermal expansion depends on its parents composition (eg Pu frac) so
383+ # setting number densities can sometimes change thermal expansion behavior.
384+ # so call again so that the material has access to its parents comp when providing the reference initial density.
385+ densityBasedOnParentComposition = self .material .getProperty (
386+ "pseudoDensity" , Tc = self .temperatureInC
387+ )
388+ self .p .numberDensities = densityTools .getNDensFromMasses (
389+ densityBasedOnParentComposition , self .material .massFrac
390+ )
391+
383392 # material needs to be expanded from the material's cold temp to hot,
384393 # not components cold temp, so we don't use mat.linearExpansionFactor or
385394 # component.getThermalExpansionFactor.
@@ -721,12 +730,7 @@ def setNumberDensity(self, nucName, val):
721730 val : float
722731 Number density to set in atoms/bn-cm (heterogeneous)
723732 """
724- self .p .numberDensities [nucName ] = val
725- self .p .assigned = parameters .SINCE_ANYTHING
726- # necessary for syncMpiState
727- parameters .ALL_DEFINITIONS [
728- "numberDensities"
729- ].assigned = parameters .SINCE_ANYTHING
733+ self .updateNumberDensities ({nucName : val })
730734
731735 def setNumberDensities (self , numberDensities ):
732736 """
@@ -747,12 +751,19 @@ def setNumberDensities(self, numberDensities):
747751
748752 Notes
749753 -----
750- We don't just call setNumberDensity for each nuclide because we don't want to call ``getVolumeFractions``
751- for each nuclide (it's inefficient).
754+ Note that sometimes volume/dimensions can change due to the number density change when the material thermal
755+ expansion depends on the component's composition (eg its plutonium fraction). In this case, changing the
756+ density will implicitly change the area/volume. Since it its very difficult to predict the new dims ahead of time,
757+ and perturbation/depletion calculations are almost exclusively done assuming constant volume,
758+ the densities sent are automatically perturbed to conserve mass with the original dimensions.
759+ That is, the components densities are not exactly as passed, but whatever they would need to be to preserve volume
760+ integrated number densities (moles) from the pre-perturbed components volume/dims.
761+ Note this has no effect if the material thermal expansion has no dependence on component composition fracs.
762+ If this is not desired, `self.p.numberDensities` can be set directly.
752763 """
753- self .p . numberDensities = numberDensities
764+ self .updateNumberDensities ( numberDensities , wipe = True )
754765
755- def updateNumberDensities (self , numberDensities ):
766+ def updateNumberDensities (self , numberDensities , wipe = False ):
756767 """
757768 Set one or more multiple number densities. Leaves unlisted number densities alone.
758769
@@ -761,12 +772,61 @@ def updateNumberDensities(self, numberDensities):
761772 numberDensities : dict
762773 nucName: ndens pairs.
763774
775+ Notes
776+ -----
777+ Note that sometimes volume/dimensions can change due to the number density change when the material thermal
778+ expansion depends on the component's composition (eg its plutonium fraction). In this case, changing the
779+ density will implicitly change the area/volume. Since it its very difficult to predict the new dims ahead of time,
780+ and perturbation/depletion calculations are almost exclusively done assuming constant volume,
781+ the densities sent are automatically perturbed to conserve mass with the original dimensions.
782+ That is, the components densities are not exactly as passed, but whatever they would need to be to preserve volume
783+ integrated number densities (moles) from the pre-perturbed components volume/dims.
784+ Note this has no effect if the material thermal expansion has no dependence on component composition fracs.
785+ If this is not desired, `self.p.numberDensities` can be set directly.
764786 """
787+ # prepare to change the densities with knowledge that dims could change due to
788+ # material thermal expansion dependence on composition
789+ if len (self .p .numberDensities ) > 0 :
790+ dLLprev = (
791+ self .material .linearExpansionPercent (Tc = self .temperatureInC ) / 100.0
792+ )
793+ else :
794+ dLLprev = 0.0
795+ try :
796+ vol = self .getVolume ()
797+ except :
798+ # either no parent to get height or parent's height is None
799+ # which would be AttributeError and TypeError respectively, but other errors could be possible
800+ vol = None
801+ area = self .getArea ()
802+
803+ # change the densities
804+ if wipe :
805+ self .p .numberDensities = {}
765806 self .p .numberDensities .update (numberDensities )
766- # since we're updating the object the param points to but not the param itself, we have to inform
767- # the param system to flag it as modified so it properly syncs during ``syncMpiState``.
768- self .p .assigned = parameters .SINCE_ANYTHING
769- self .p .paramDefs ["numberDensities" ].assigned = parameters .SINCE_ANYTHING
807+
808+ # check if thermal expansion changed
809+ dLLnew = self .material .linearExpansionPercent (Tc = self .temperatureInC ) / 100.0
810+ if dLLprev != dLLnew and dLLprev != 0.0 :
811+ # the thermal expansion changed so the volume change is happening at same time as
812+ # density change was requested. Attempt to make mass consistent with old dims
813+ # (since the density change was for the old volume and otherwise mass wouldn't be conserved)
814+
815+ # enable recalculation of volume, otherwise it uses stored.
816+ self .clearLinkedCache ()
817+ if vol is not None :
818+ factor = vol / self .getVolume ()
819+ else :
820+ factor = area / self .getArea ()
821+ self .changeNDensByFactor (factor )
822+
823+ def changeNDensByFactor (self , factor ):
824+ """Change the number density of all nuclides within the object by a multiplicative factor."""
825+ newDensities = {
826+ nuc : dens * factor for nuc , dens in self .p .numberDensities .items ()
827+ }
828+ self .p .numberDensities = newDensities
829+ self ._changeOtherDensParamsByFactor (factor )
770830
771831 def getEnrichment (self ):
772832 """Get the mass enrichment of this component, as defined by the material."""
@@ -1247,7 +1307,9 @@ def getIntegratedMgFlux(self, adjoint=False, gamma=False):
12471307 if not self .parent :
12481308 return np .zeros (1 )
12491309
1250- volumeFraction = (self .getVolume () / self .parent .getSymmetryFactor () ) / self .parent .getVolume ()
1310+ volumeFraction = (
1311+ self .getVolume () / self .parent .getSymmetryFactor ()
1312+ ) / self .parent .getVolume ()
12511313 return volumeFraction * self .parent .getIntegratedMgFlux (adjoint , gamma )
12521314
12531315 # pin-level flux is available. Note that it is NOT integrated on the param level.
@@ -1262,7 +1324,11 @@ def getIntegratedMgFlux(self, adjoint=False, gamma=False):
12621324 else :
12631325 pinFluxes = self .parent .p .pinMgFluxes
12641326
1265- return pinFluxes [self .p .pinNum - 1 ] * self .getVolume () / self .parent .getSymmetryFactor ()
1327+ return (
1328+ pinFluxes [self .p .pinNum - 1 ]
1329+ * self .getVolume ()
1330+ / self .parent .getSymmetryFactor ()
1331+ )
12661332
12671333 def getPinMgFluxes (
12681334 self , adjoint : Optional [bool ] = False , gamma : Optional [bool ] = False
0 commit comments