@@ -375,11 +375,20 @@ def applyMaterialMassFracsToNumberDensities(self):
375
375
# `density` is 3D density
376
376
# call getProperty to cache and improve speed
377
377
density = self .material .getProperty ("pseudoDensity" , Tc = self .temperatureInC )
378
-
379
378
self .p .numberDensities = densityTools .getNDensFromMasses (
380
379
density , self .material .massFrac
381
380
)
382
381
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
+
383
392
# material needs to be expanded from the material's cold temp to hot,
384
393
# not components cold temp, so we don't use mat.linearExpansionFactor or
385
394
# component.getThermalExpansionFactor.
@@ -721,12 +730,7 @@ def setNumberDensity(self, nucName, val):
721
730
val : float
722
731
Number density to set in atoms/bn-cm (heterogeneous)
723
732
"""
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 })
730
734
731
735
def setNumberDensities (self , numberDensities ):
732
736
"""
@@ -747,12 +751,19 @@ def setNumberDensities(self, numberDensities):
747
751
748
752
Notes
749
753
-----
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.
752
763
"""
753
- self .p . numberDensities = numberDensities
764
+ self .updateNumberDensities ( numberDensities , wipe = True )
754
765
755
- def updateNumberDensities (self , numberDensities ):
766
+ def updateNumberDensities (self , numberDensities , wipe = False ):
756
767
"""
757
768
Set one or more multiple number densities. Leaves unlisted number densities alone.
758
769
@@ -761,12 +772,61 @@ def updateNumberDensities(self, numberDensities):
761
772
numberDensities : dict
762
773
nucName: ndens pairs.
763
774
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.
764
786
"""
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 = {}
765
806
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 )
770
830
771
831
def getEnrichment (self ):
772
832
"""Get the mass enrichment of this component, as defined by the material."""
@@ -1247,7 +1307,9 @@ def getIntegratedMgFlux(self, adjoint=False, gamma=False):
1247
1307
if not self .parent :
1248
1308
return np .zeros (1 )
1249
1309
1250
- volumeFraction = (self .getVolume () / self .parent .getSymmetryFactor () ) / self .parent .getVolume ()
1310
+ volumeFraction = (
1311
+ self .getVolume () / self .parent .getSymmetryFactor ()
1312
+ ) / self .parent .getVolume ()
1251
1313
return volumeFraction * self .parent .getIntegratedMgFlux (adjoint , gamma )
1252
1314
1253
1315
# 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):
1262
1324
else :
1263
1325
pinFluxes = self .parent .p .pinMgFluxes
1264
1326
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
+ )
1266
1332
1267
1333
def getPinMgFluxes (
1268
1334
self , adjoint : Optional [bool ] = False , gamma : Optional [bool ] = False
0 commit comments