-
Notifications
You must be signed in to change notification settings - Fork 87
/
blocks.py
2465 lines (2075 loc) · 92.7 KB
/
blocks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# 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.
"""
Defines blocks, which are axial chunks of assemblies. They contain
most of the state variables, including power, flux, and homogenized number densities.
Assemblies are made of blocks.
Blocks are made of components.
"""
from typing import Optional, Type, Tuple, ClassVar
import collections
import copy
import math
import numpy
from armi import nuclideBases
from armi import runLog
from armi.bookkeeping import report
from armi.physics.neutronics import GAMMA
from armi.physics.neutronics import NEUTRON
from armi.reactor import blockParameters
from armi.reactor import components
from armi.reactor import composites
from armi.reactor import geometry
from armi.reactor import grids
from armi.reactor import parameters
from armi.reactor.components import basicShapes
from armi.reactor.components.basicShapes import Hexagon, Circle
from armi.reactor.components.complexShapes import Helix
from armi.reactor.flags import Flags
from armi.reactor.parameters import ParamLocation
from armi.utils import densityTools
from armi.utils import hexagon
from armi.utils import units
from armi.utils.plotting import plotBlockFlux
from armi.utils.units import TRACE_NUMBER_DENSITY
PIN_COMPONENTS = [
Flags.CONTROL,
Flags.PLENUM,
Flags.SHIELD,
Flags.FUEL,
Flags.CLAD,
Flags.PIN,
Flags.WIRE,
]
_PitchDefiningComponent = Optional[Tuple[Type[components.Component], ...]]
class Block(composites.Composite):
"""
A homogenized axial slab of material.
Blocks are stacked together to form assemblies.
"""
uniqID = 0
# dimension used to determine which component defines the block's pitch
PITCH_DIMENSION = "op"
# component type that can be considered a candidate for providing pitch
PITCH_COMPONENT_TYPE: ClassVar[_PitchDefiningComponent] = None
pDefs = blockParameters.getBlockParameterDefinitions()
def __init__(self, name: str, height: float = 1.0):
"""
Builds a new ARMI block.
name : str
The name of this block
height : float, optional
The height of the block in cm. Defaults to 1.0 so that
`getVolume` assumes unit height.
"""
composites.Composite.__init__(self, name)
self.p.height = height
self.p.heightBOL = height
self.p.orientation = numpy.array((0.0, 0.0, 0.0))
self.points = []
self.macros = None
# flag to indicated when DerivedShape children must be updated.
self.derivedMustUpdate = False
# which component to use to determine block pitch, along with its 'op'
self._pitchDefiningComponent = (None, 0.0)
# TODO: what's causing these to have wrong values at BOL?
for problemParam in ["THcornTemp", "THedgeTemp"]:
self.p[problemParam] = []
for problemParam in [
"residence",
"bondRemoved",
"fluence",
"fastFluence",
"fastFluencePeak",
"displacementX",
"displacementY",
"fluxAdj",
"buRate",
"eqRegion",
"fissileFraction",
]:
self.p[problemParam] = 0.0
def __repr__(self):
# be warned, changing this might break unit tests on input file generations
return "<{type} {name} at {loc} XS: {xs} BU GP: {bu}>".format(
type=self.getType(),
name=self.getName(),
xs=self.p.xsType,
bu=self.p.buGroup,
loc=self.getLocation(),
)
def __deepcopy__(self, memo):
"""
Custom deepcopy behavior to prevent duplication of macros and _lumpedFissionProducts.
We detach the recursive links to the parent and the reactor to prevent blocks carrying large
independent copies of stale reactors in memory. If you make a new block, you must add it to
an assembly and a reactor.
"""
# add self to memo to prevent child objects from duplicating the parent block
memo[id(self)] = b = self.__class__.__new__(self.__class__)
# use __getstate__ and __setstate__ pickle-methods to initialize
state = self.__getstate__() # __getstate__ removes parent
del state["macros"]
del state["_lumpedFissionProducts"]
b.__setstate__(copy.deepcopy(state, memo))
# assign macros and LFP
b.macros = self.macros
b._lumpedFissionProducts = self._lumpedFissionProducts
return b
def createHomogenizedCopy(self, pinSpatialLocators=False):
"""
Create a copy of a block.
Notes
-----
Used to implement a copy function for specific block types that can
be much faster than a deepcopy by glossing over details that may be
unnecessary in certain contexts.
This base class implementation is just a deepcopy of the block, in full detail
(not homogenized).
"""
return copy.deepcopy(self)
@property
def core(self):
from armi.reactor.reactors import Core
c = self.getAncestor(lambda c: isinstance(c, Core))
return c
@property
def r(self):
"""
Look through the ancestors of the Block to find a Reactor, and return it.
Notes
-----
Typical hierarchy: Reactor <- Core <- Assembly <- Block
A block should only have a reactor through a parent assembly.
It may make sense to try to factor out usage of ``b.r``.
Returns
-------
core.parent : armi.reactor.reactors.Reactor
ARMI reactor object that is an ancestor of the block.
Raises
------
ValueError
If the parent of the block's ``core`` is not an ``armi.reactor.reactors.Reactor``.
"""
from armi.reactor.reactors import Reactor
core = self.core
if core is None:
return self.getAncestor(lambda o: isinstance(o, Reactor))
if not isinstance(core.parent, Reactor):
raise TypeError(
"Parent of Block ({}) core is not a Reactor. Got {} instead".format(
core.parent, type(core.parent)
)
)
return core.parent
def makeName(self, assemNum, axialIndex):
"""
Generate a standard block from assembly number.
This also sets the block-level assembly-num param.
Once, we used a axial-character suffix to represent the axial
index, but this is inherently limited so we switched to a numerical
name. The axial suffix needs can be brought in in plugins that require
them.
Examples
--------
>>> makeName(120, 5)
'B0120-005'
"""
self.p.assemNum = assemNum
return "B{0:04d}-{1:03d}".format(assemNum, axialIndex)
def getSmearDensity(self, cold=True):
"""
Compute the smear density of pins in this block.
Smear density is the area of the fuel divided by the area of the space available
for fuel inside the cladding. Other space filled with solid materials is not
considered available. If all the area is fuel, it has 100% smear density. Lower
smear density allows more room for swelling.
.. warning:: This requires circular fuel and circular cladding. Designs that vary
from this will be wrong. It may make sense in the future to put this somewhere a
bit more design specific.
Notes
-----
This only considers circular objects. If you have a cladding that is not a circle,
it will be ignored.
Negative areas can exist for void gaps in the fuel pin. A negative area in a gap
represents overlap area between two solid components. To account for this
additional space within the pin cladding the abs(negativeArea) is added to the
inner cladding area.
Parameters
----------
cold : bool, optional
If false, returns the smear density at hot temperatures
Returns
-------
smearDensity : float
The smear density as a fraction
"""
fuels = self.getComponents(Flags.FUEL)
if not fuels:
return 0.0 # Smear density is not computed for non-fuel blocks
circles = self.getComponentsOfShape(components.Circle)
if not circles:
raise ValueError(
"Cannot get smear density of {}. There are no circular components.".format(
self
)
)
clads = set(self.getComponents(Flags.CLAD)).intersection(set(circles))
if not clads:
raise ValueError(
"Cannot get smear density of {}. There are no clad components.".format(
self
)
)
# Compute component areas
cladID = numpy.mean([clad.getDimension("id", cold=cold) for clad in clads])
innerCladdingArea = (
math.pi * (cladID**2) / 4.0 * self.getNumComponents(Flags.FUEL)
)
fuelComponentArea = 0.0
unmovableComponentArea = 0.0
negativeArea = 0.0
for c in self.getSortedComponentsInsideOfComponent(clads.pop()):
componentArea = c.getArea(cold=cold)
if c.isFuel():
fuelComponentArea += componentArea
elif c.hasFlags(Flags.SLUG):
# this flag designates that this clad/slug combination isn't fuel and shouldn't be counted in the average
pass
else:
if c.containsSolidMaterial():
unmovableComponentArea += componentArea
elif c.containsVoidMaterial() and componentArea < 0.0:
if cold: # will error out soon
runLog.error(
"{} with id {} and od {} has negative area at cold dimensions".format(
c,
c.getDimension("id", cold=True),
c.getDimension("od", cold=True),
)
)
negativeArea += abs(componentArea)
if cold and negativeArea:
raise ValueError(
"Negative component areas exist on {}. Check that the cold dimensions are properly aligned "
"and no components overlap.".format(self)
)
innerCladdingArea += negativeArea # See note 2
totalMovableArea = innerCladdingArea - unmovableComponentArea
smearDensity = fuelComponentArea / totalMovableArea
return smearDensity
def autoCreateSpatialGrids(self):
"""
Creates a spatialGrid for a Block.
Blocks do not always have a spatialGrid from Blueprints, but, some Blocks can have their
spatialGrids inferred based on the multiplicty of their components.
This would add the ability to create a spatialGrid for a Block and give its children
the corresponding spatialLocators if certain conditions are met.
Raises
------
ValueError
If the multiplicities of the block are not only 1 or N or if generated ringNumber leads to more positions than necessary.
"""
raise NotImplementedError()
def getMgFlux(self, adjoint=False, average=False, volume=None, gamma=False):
"""
Returns the multigroup neutron flux in [n/cm^2/s].
The first entry is the first energy group (fastest neutrons). Each additional
group is the next energy group, as set in the ISOTXS library.
It is stored integrated over volume on self.p.mgFlux
Parameters
----------
adjoint : bool, optional
Return adjoint flux instead of real
average : bool, optional
If true, will return average flux between latest and previous. Doesn't work
for pin detailed yet
volume: float, optional
If average=True, the volume-integrated flux is divided by volume before being returned.
The user may specify a volume here, or the function will obtain the block volume directly.
gamma : bool, optional
Whether to return the neutron flux or the gamma flux.
Returns
-------
flux : multigroup neutron flux in [n/cm^2/s]
"""
flux = composites.ArmiObject.getMgFlux(
self, adjoint=adjoint, average=False, volume=volume, gamma=gamma
)
if average and numpy.any(self.p.lastMgFlux):
volume = volume or self.getVolume()
lastFlux = self.p.lastMgFlux / volume
flux = (flux + lastFlux) / 2.0
return flux
def setPinMgFluxes(self, fluxes, adjoint=False, gamma=False):
"""
Store the pin-detailed multi-group neutron flux.
The [g][i] indexing is transposed to be a list of lists, one for each pin. This makes it
simple to do depletion for each pin, etc.
Parameters
----------
fluxes : 2-D list of floats
The block-level pin multigroup fluxes. fluxes[g][i] represents the flux in group g for pin i.
Flux units are the standard n/cm^2/s.
The "ARMI pin ordering" is used, which is counter-clockwise from 3 o'clock.
adjoint : bool, optional
Whether to set real or adjoint data.
gamma : bool, optional
Whether to set gamma or neutron data.
Outputs
-------
self.p.pinMgFluxes : 2-D array of floats
The block-level pin multigroup fluxes. pinMgFluxes[g][i] represents the flux in group g for pin i.
Flux units are the standard n/cm^2/s.
The "ARMI pin ordering" is used, which is counter-clockwise from 3 o'clock.
"""
pinFluxes = []
G, nPins = fluxes.shape
for pinNum in range(1, nPins + 1):
thisPinFlux = []
if self.hasFlags(Flags.FUEL):
pinLoc = self.p.pinLocation[pinNum - 1]
else:
pinLoc = pinNum
for g in range(G):
thisPinFlux.append(fluxes[g][pinLoc - 1])
pinFluxes.append(thisPinFlux)
pinFluxes = numpy.array(pinFluxes)
if gamma:
if adjoint:
raise ValueError("Adjoint gamma flux is currently unsupported.")
else:
self.p.pinMgFluxesGamma = pinFluxes
else:
if adjoint:
self.p.pinMgFluxesAdj = pinFluxes
else:
self.p.pinMgFluxes = pinFluxes
def getMicroSuffix(self):
"""
Returns the microscopic library suffix (e.g. 'AB') for this block.
DIF3D and MC2 are limited to 6 character nuclide labels. ARMI by convention uses
the first 4 for nuclide name (e.g. U235, PU39, etc.) and then uses the 5th
character for cross-section type and the 6th for burnup group. This allows a
variety of XS sets to be built modeling substantially different blocks.
Notes
-----
The single-letter use for xsType and buGroup limit users to 26 groups of each.
ARMI will allow 2-letter xsType designations if and only if the `buGroups`
setting has length 1 (i.e. no burnup groups are defined). This is useful for
high-fidelity XS modeling of V&V models such as the ZPPRs.
"""
bu = self.p.buGroup
if not bu:
raise RuntimeError(
"Cannot get MicroXS suffix because {0} in {1} does not have a burnup group"
"".format(self, self.parent)
)
xsType = self.p.xsType
if len(xsType) == 1:
return xsType + bu
elif len(xsType) == 2 and ord(bu) > ord("A"):
raise ValueError(
"Use of multiple burnup groups is not allowed with multi-character xs groups!"
)
else:
return xsType
def getHeight(self):
"""Return the block height."""
return self.p.height
def setHeight(self, modifiedHeight, conserveMass=False, adjustList=None):
"""
Set a new height of the block.
Parameters
----------
modifiedHeight : float
The height of the block in cm
conserveMass : bool, optional
Conserve mass of nuclides in ``adjustList``.
adjustList : list, optional
Nuclides that will be conserved in conserving mass in the block. It is recommended to pass a list of
all nuclides in the block.
Notes
-----
There is a coupling between block heights, the parent assembly axial mesh,
and the ztop/zbottom/z params of the sibling blocks. When you set a height,
all those things are invalidated. Thus, this method has to go through and
update them via ``parent.calculateZCoords``. This could be inefficient
though it has not been identified as a bottleneck. Possible improvements
include deriving z/ztop/zbottom on the fly and invalidating the parent mesh
with some kind of flag, signaling it to recompute itself on demand.
Developers can get around some of the O(N^2) scaling of this by setting
``p.height`` directly but they must know to update the dependent objects
after they do that. Use with care.
See Also
--------
armi.reactor.reactors.Core.updateAxialMesh
May need to be called after this.
armi.reactor.assemblies.Assembly.calculateZCoords
Recalculates z-coords, automatically called by this.
"""
originalHeight = self.getHeight() # get before modifying
if modifiedHeight < 0.0:
raise ValueError(
"Cannot set height of block {} to height of {} cm".format(
self, modifiedHeight
)
)
self.p.height = modifiedHeight
self.clearCache()
if conserveMass:
if originalHeight != modifiedHeight:
if not adjustList:
raise ValueError(
"Nuclides in ``adjustList`` must be provided to conserve mass."
)
self.adjustDensity(originalHeight / modifiedHeight, adjustList)
if self.parent:
self.parent.calculateZCoords()
def getWettedPerimeter(self):
raise NotImplementedError
def getFlowAreaPerPin(self):
"""
Return the flowing coolant area of the block in cm^2, normalized to the number of pins in the block.
NumPins looks for max number of fuel, clad, control, etc.
See Also
--------
armi.reactor.blocks.Block.getNumPins
figures out numPins
"""
numPins = self.getNumPins()
try:
return self.getComponent(Flags.COOLANT, exact=True).getArea() / numPins
except ZeroDivisionError:
raise ZeroDivisionError(
"Block {} has 0 pins (fuel, clad, control, shield, etc.). Thus, its flow area "
"per pin is undefined.".format(self)
)
def getHydraulicDiameter(self):
raise NotImplementedError
def adjustUEnrich(self, newEnrich):
"""
Adjust U-235/U-238 mass ratio to a mass enrichment.
Parameters
----------
newEnrich : float
New U-235 enrichment in mass fraction
Notes
-----
completeInitialLoading must be run because adjusting the enrichment actually
changes the mass slightly and you can get negative burnups, which you do not want.
"""
fuels = self.getChildrenWithFlags(Flags.FUEL)
if fuels:
for fuel in fuels:
fuel.adjustMassEnrichment(newEnrich)
else:
# no fuel in this block
tU = self.getNumberDensity("U235") + self.getNumberDensity("U238")
if tU:
self.setNumberDensity("U235", tU * newEnrich)
self.setNumberDensity("U238", tU * (1.0 - newEnrich))
self.completeInitialLoading()
def getLocation(self):
"""Return a string representation of the location.
.. impl:: Location of a block is retrievable.
:id: I_ARMI_BLOCK_POSI0
:implements: R_ARMI_BLOCK_POSI
"""
if self.core and self.parent.spatialGrid and self.spatialLocator:
return self.core.spatialGrid.getLabel(
self.spatialLocator.getCompleteIndices()
)
else:
return "ExCore"
def coords(self, rotationDegreesCCW=0.0):
"""
Returns the coordinates of the block.
.. impl:: Coordinates of a block are queryable.
:id: I_ARMI_BLOCK_POSI1
:implements: R_ARMI_BLOCK_POSI
"""
if rotationDegreesCCW:
raise NotImplementedError("Cannot get coordinates with rotation.")
return self.spatialLocator.getGlobalCoordinates()
def setBuLimitInfo(self):
"""Sets burnup limit based on igniter, feed, etc."""
if self.p.buRate == 0:
# might be cycle 1 or a non-burning block
self.p.timeToLimit = 0.0
else:
timeLimit = (
self.p.buLimit - self.p.percentBu
) / self.p.buRate + self.p.residence
self.p.timeToLimit = (timeLimit - self.p.residence) / units.DAYS_PER_YEAR
def getMaxArea(self):
raise NotImplementedError
def getMaxVolume(self):
"""
The maximum volume of this object if it were totally full.
Returns
-------
vol : float
volume in cm^3.
"""
return self.getMaxArea() * self.getHeight()
def getArea(self, cold=False):
"""
Return the area of a block for a full core or a 1/3 core model.
Area is consistent with the area in the model, so if you have a central
assembly in a 1/3 symmetric model, this will return 1/3 of the total
area of the physical assembly. This way, if you take the sum
of the areas in the core (or count the atoms in the core, etc.),
you will have the proper number after multiplying by the model symmetry.
Parameters
----------
cold : bool
flag to indicate that cold (as input) dimensions are required
Notes
-----
This might not work for a 1/6 core model (due to symmetry line issues).
Returns
-------
area : float (cm^2)
See Also
--------
armi.reactor.blocks.Block.getMaxArea
return the full area of the physical assembly disregarding model symmetry
"""
# this caching requires that you clear the cache every time you adjust anything
# including temperature and dimensions.
area = self._getCached("area")
if area:
return area
a = 0.0
for c in self.getChildren():
myArea = c.getArea(cold=cold)
a += myArea
fullArea = a
# correct the fullHexArea by the symmetry factor
# this factor determines if the hex has been clipped by symmetry lines
area = fullArea / self.getSymmetryFactor()
self._setCache("area", area)
return area
def getVolume(self):
"""
Return the volume of a block.
.. impl:: Volume of block is retrievable.
:id: I_ARMI_BLOCK_DIMS0
:implements: R_ARMI_BLOCK_DIMS
Returns
-------
volume : float
Block or component volume in cm^3
"""
# use symmetryFactor in case the assembly is sitting on a boundary and needs to be cut in half, etc.
vol = sum(c.getVolume() for c in self)
return vol / self.getSymmetryFactor()
def getSymmetryFactor(self):
"""
Return a scaling factor due to symmetry on the area of the block or its components.
Takes into account assemblies that are bisected or trisected by symmetry lines
In 1/3 symmetric cases, the central assembly is 1/3 a full area.
If edge assemblies are included in a model, the symmetry factor along
both edges for overhanging assemblies should be 2.0. However,
ARMI runs in most scenarios with those assemblies on the 120-edge removed,
so the symmetry factor should generally be just 1.0.
See Also
--------
armi.reactor.converters.geometryConverter.EdgeAssemblyChanger.scaleParamsRelatedToSymmetry
"""
return 1.0
def isOnWhichSymmetryLine(self):
"""Block symmetry lines are determined by the reactor, not the parent."""
grid = self.core.spatialGrid
return grid.overlapsWhichSymmetryLine(self.spatialLocator.getCompleteIndices())
def adjustDensity(self, frac, adjustList, returnMass=False):
"""
adjusts the total density of each nuclide in adjustList by frac.
Parameters
----------
frac : float
The fraction of the current density that will remain after this operation
adjustList : list
List of nuclide names that will be adjusted.
returnMass : bool
If true, will return mass difference.
Returns
-------
mass : float
Mass difference in grams. If you subtract mass, mass will be negative.
If returnMass is False (default), this will always be zero.
"""
self._updateDetailedNdens(frac, adjustList)
mass = 0.0
if returnMass:
# do this with a flag to enable faster operation when mass is not needed.
volume = self.getVolume()
numDensities = self.getNuclideNumberDensities(adjustList)
for nuclideName, dens in zip(adjustList, numDensities):
if not dens:
# don't modify zeros.
continue
newDens = dens * frac
# add a little so components remember
self.setNumberDensity(nuclideName, newDens + TRACE_NUMBER_DENSITY)
if returnMass:
mass += densityTools.getMassInGrams(nuclideName, volume, newDens - dens)
return mass
def _updateDetailedNdens(self, frac, adjustList):
"""
Update detailed number density which is used by hi-fi depleters such as ORIGEN.
Notes
-----
This will perturb all number densities so it is assumed that if one of the active densities
is perturbed, all of htem are perturbed.
"""
if self.p.detailedNDens is None:
# BOL assems get expanded to a reference so the first check is needed so it
# won't call .blueprints on None since BOL assems don't have a core/r
return
if any(nuc in self.r.blueprints.activeNuclides for nuc in adjustList):
self.p.detailedNDens *= frac
# Other power densities do not need to be updated as they are calculated in
# the global flux interface, which occurs after axial expansion from crucible
# on the interface stack.
self.p.pdensDecay *= frac
def completeInitialLoading(self, bolBlock=None):
"""
Does some BOL bookkeeping to track things like BOL HM density for burnup tracking.
This should run after this block is loaded up at BOC (called from
Reactor.initialLoading).
The original purpose of this was to get the moles HM at BOC for the moles
Pu/moles HM at BOL calculation.
This also must be called after modifying something like the smear density or zr
fraction in an optimization case. In ECPT cases, a BOL block must be passed or
else the burnup will try to get based on a pre-burned value.
Parameters
----------
bolBlock : Block, optional
A BOL-state block of this block type, required for perturbed equilibrium cases.
Must have the same enrichment as this block!
Returns
-------
hmDens : float
The heavy metal number density of this block.
See Also
--------
Reactor.importGeom
depletion._updateBlockParametersAfterDepletion
"""
if bolBlock is None:
bolBlock = self
hmDens = bolBlock.getHMDens() # total homogenized heavy metal number density
self.p.nHMAtBOL = hmDens
self.p.molesHmBOL = self.getHMMoles()
self.p.puFrac = (
self.getPuMoles() / self.p.molesHmBOL if self.p.molesHmBOL > 0.0 else 0.0
)
try:
# non-pinned reactors (or ones without cladding) will not use smear density
self.p.smearDensity = self.getSmearDensity()
except ValueError:
pass
self.p.enrichmentBOL = self.getFissileMassEnrich()
massHmBOL = 0.0
sf = self.getSymmetryFactor()
for child in self:
hmMass = child.getHMMass() * sf
massHmBOL += hmMass
# Components have a massHmBOL parameter but not every composite will
if isinstance(child, components.Component):
child.p.massHmBOL = hmMass
self.p.massHmBOL = massHmBOL
return hmDens
def setB10VolParam(self, heightHot):
"""
Set the b.p.initialB10ComponentVol param according to the volume of boron-10 containing components.
Parameters
----------
heightHot : Boolean
True if self.height() is cold height
"""
# exclude fuel components since they could have slight B10 impurity and
# this metric is not relevant for fuel.
b10Comps = [c for c in self if c.getNumberDensity("B10") and not c.isFuel()]
if not b10Comps:
return
# get the highest density comp dont want to sum all because some
# comps might have very small impurities of boron and adding this
# volume wont be conservative for captures per cc.
b10Comp = sorted(b10Comps, key=lambda x: x.getNumberDensity("B10"))[-1]
if len(b10Comps) > 1:
runLog.warning(
f"More than one boron10-containing component found in {self.name}. "
f"Only {b10Comp} will be considered for calculation of initialB10ComponentVol "
"Since adding multiple volumes is not conservative for captures/cc."
f"All compos found {b10Comps}",
single=True,
)
if self.isFuel():
runLog.warning(
f"{self.name} has both fuel and initial b10. "
"b10 volume may not be conserved with axial expansion.",
single=True,
)
# calc volume of boron components
coldArea = b10Comp.getArea(cold=True)
coldFactor = b10Comp.getThermalExpansionFactor() if heightHot else 1
coldHeight = self.getHeight() / coldFactor
self.p.initialB10ComponentVol = coldArea * coldHeight
def replaceBlockWithBlock(self, bReplacement):
"""
Replace the current block with the replacementBlock.
Typically used in the insertion of control rods.
"""
paramsToSkip = set(
self.p.paramDefs.inCategory(parameters.Category.retainOnReplacement).names
)
tempBlock = copy.deepcopy(bReplacement)
oldParams = self.p
newParams = self.p = tempBlock.p
for paramName in paramsToSkip:
newParams[paramName] = oldParams[paramName]
# update synchronization information
self.p.assigned = parameters.SINCE_ANYTHING
paramDefs = self.p.paramDefs
for paramName in set(newParams.keys()) - paramsToSkip:
paramDefs[paramName].assigned = parameters.SINCE_ANYTHING
newComponents = tempBlock.getChildren()
self.setChildren(newComponents)
self.clearCache()
@staticmethod
def plotFlux(core, fName=None, bList=None, peak=False, adjoint=False, bList2=[]):
# Block.plotFlux has been moved to utils.plotting as plotBlockFlux, which is a
# better fit.
# We don't want to remove the plotFlux function in the Block namespace yet
# in case client code is depending on this function existing here. This is just
# a simple pass-through function that passes the arguments along to the actual
# implementation in its new location.
plotBlockFlux(core, fName, bList, peak, adjoint, bList2)
def _updatePitchComponent(self, c):
"""
Update the component that defines the pitch.
Given a Component, compare it to the current component that defines the pitch of the Block.
If bigger, replace it.
We need different implementations of this to support different logic for determining the
form of pitch and the concept of "larger".
See Also
--------
CartesianBlock._updatePitchComponent
"""
# Some block types don't have a clearly defined pitch (e.g. ThRZ)
if self.PITCH_COMPONENT_TYPE is None:
return
if not isinstance(c, self.PITCH_COMPONENT_TYPE):
return
try:
componentPitch = c.getDimension(self.PITCH_DIMENSION)
except parameters.UnknownParameterError:
# some components dont have the appropriate parameter
return
if componentPitch and (componentPitch > self._pitchDefiningComponent[1]):
self._pitchDefiningComponent = (c, componentPitch)
def add(self, c):
composites.Composite.add(self, c)
self.derivedMustUpdate = True
self.clearCache()
try:
mult = int(c.getDimension("mult"))
if self.p.percentBuByPin is None or len(self.p.percentBuByPin) < mult:
# this may be a little wasteful, but we can fix it later...
self.p.percentBuByPin = [0.0] * mult
except AttributeError:
# maybe adding a Composite of components rather than a single
pass
self._updatePitchComponent(c)
def removeAll(self, recomputeAreaFractions=True):
for c in self.getChildren():
self.remove(c, recomputeAreaFractions=False)
if recomputeAreaFractions: # only do this once
self.getVolumeFractions()
def remove(self, c, recomputeAreaFractions=True):
composites.Composite.remove(self, c)
self.clearCache()
if c is self._pitchDefiningComponent[0]:
self._pitchDefiningComponent = (None, 0.0)
pc = self.getLargestComponent(self.PITCH_DIMENSION)
if pc is not None:
self._updatePitchComponent(pc)
if recomputeAreaFractions:
self.getVolumeFractions()
def getComponentsThatAreLinkedTo(self, comp, dim):
"""
Determine which dimensions of which components are linked to a specific dimension of a particular component.
Useful for breaking fuel components up into individuals and making sure
anything that was linked to the fuel mult (like the cladding mult) stays correct.
Parameters
----------
comp : Component
The component that the results are linked to
dim : str
The name of the dimension that the results are linked to
Returns
-------
linkedComps : list