/
operator.py
1072 lines (919 loc) · 41.1 KB
/
operator.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.
"""
The standard ARMI operator.
This builds and maintains the interface stack and loops through it for a
certain number of cycles with a certain number of timenodes per cycle.
This is analogous to a real reactor operating over some period of time,
often from initial startup, through the various cycles, and out to
the end of plant life.
.. impl:: ARMI controls the time flow of the reactor, by running a sequence of Interfaces at each time step.
:id: IMPL_EVOLVING_STATE_0
:links: REQ_EVOLVING_STATE
"""
import os
import re
import shutil
import time
from armi import context
from armi import interfaces
from armi import runLog
from armi import settings
from armi.bookkeeping import memoryProfiler
from armi.bookkeeping.report import reportingUtils
from armi.operators import settingsValidation
from armi.operators.runTypes import RunTypes
from armi.utils import codeTiming
from armi.utils import (
pathTools,
getPowerFractions,
getAvailabilityFactors,
getStepLengths,
getCycleLengths,
getBurnSteps,
getMaxBurnSteps,
getCycleNames,
)
class Operator: # pylint: disable=too-many-public-methods
"""
Orchestrates an ARMI run, building all the pieces, looping through the interfaces, and manipulating the reactor.
This Standard Operator loops over a user-input number of cycles, each with a
user-input number of subcycles (called time nodes). It calls a series of
interaction hooks on each of the
:py:class:`~armi.interfaces.Interface` in the Interface Stack.
.. figure:: /.static/armi_general_flowchart.png
:align: center
**Figure 1.** The computational flow of the interface hooks in a Standard Operator
.. note:: The :doc:`/developer/guide` has some additional narrative on this topic.
Attributes
----------
cs : CaseSettings object
Global settings that define the run.
cycleNames : list of str
The name of each cycle. Cycles without a name are `None`.
stepLengths : list of list of float
A two-tiered list, where primary indices correspond to cycle and
secondary indices correspond to the length of each intra-cycle step (in days).
cycleLengths : list of float
The duration of each individual cycle in a run (in days). This is the entire cycle,
from startup to startup and includes outage time.
burnSteps : list of int
The number of sub-cycles in each cycle.
availabilityFactors : list of float
The fraction of time in a cycle that the plant is producing power. Note that capacity factor
is always less than or equal to this, depending on the power fraction achieved during each cycle.
Note that this is not a two-tiered list like stepLengths or powerFractions,
because each cycle can have only one availabilityFactor.
powerFractions : list of list of float
A two-tiered list, where primary indices correspond to cycles and secondary
indices correspond to the fraction of full rated capacity that the plant achieves
during that step of the cycle.
Zero power fraction can indicate decay-only cycles.
interfaces : list
The Interface objects that will operate upon the reactor
"""
inspector = settingsValidation.Inspector
def __init__(self, cs):
"""
Constructor for operator.
Parameters
----------
cs : CaseSettings object
Global settings that define the run.
Raises
------
OSError
If unable to create the FAST_PATH directory.
"""
self.r = None
self.cs = cs
runLog.LOG.startLog(self.cs.caseTitle)
self.timer = codeTiming.getMasterTimer()
self.interfaces = []
self.restartData = []
self.loadedRestartData = []
self._cycleNames = None
self._stepLengths = None
self._cycleLengths = None
self._burnSteps = None
self._maxBurnSteps = None
self._powerFractions = None
self._availabilityFactors = None
# Create the welcome headers for the case (case, input, machine, and some basic reactor information)
reportingUtils.writeWelcomeHeaders(self, cs)
self._initFastPath()
@property
def burnSteps(self):
if not self._burnSteps:
self._burnSteps = getBurnSteps(self.cs)
if self._burnSteps == [] and self.cs["nCycles"] == 1:
# it is possible for there to be one cycle with zero burn up,
# in which case burnSteps is an empty list
pass
else:
self._checkReactorCycleAttrs({"burnSteps": self._burnSteps})
return self._burnSteps
@property
def maxBurnSteps(self):
if not self._maxBurnSteps:
self._maxBurnSteps = getMaxBurnSteps(self.cs)
return self._maxBurnSteps
@property
def stepLengths(self):
if not self._stepLengths:
self._stepLengths = getStepLengths(self.cs)
if self._stepLengths == [] and self.cs["nCycles"] == 1:
# it is possible for there to be one cycle with zero burn up,
# in which case stepLengths is an empty list
pass
else:
self._checkReactorCycleAttrs({"Step lengths": self._stepLengths})
self._consistentPowerFractionsAndStepLengths()
return self._stepLengths
@property
def cycleLengths(self):
if not self._cycleLengths:
self._cycleLengths = getCycleLengths(self.cs)
self._checkReactorCycleAttrs({"cycleLengths": self._cycleLengths})
return self._cycleLengths
@property
def powerFractions(self):
if not self._powerFractions:
self._powerFractions = getPowerFractions(self.cs)
self._checkReactorCycleAttrs({"powerFractions": self._powerFractions})
self._consistentPowerFractionsAndStepLengths()
return self._powerFractions
@property
def availabilityFactors(self):
if not self._availabilityFactors:
self._availabilityFactors = getAvailabilityFactors(self.cs)
self._checkReactorCycleAttrs(
{"availabilityFactors": self._availabilityFactors}
)
return self._availabilityFactors
@property
def cycleNames(self):
if not self._cycleNames:
self._cycleNames = getCycleNames(self.cs)
self._checkReactorCycleAttrs({"Cycle names": self._cycleNames})
return self._cycleNames
@staticmethod
def _initFastPath():
"""
Create the FAST_PATH directory for fast local operations
Notes
-----
The FAST_PATH was once created at import-time in order to support modules
that use FAST_PATH without operators (e.g. Database). However, we decided
to leave FAST_PATH as the CWD in INTERACTIVE mode, so this should not
be a problem anymore, and we can safely move FAST_PATH creation
back into the Operator.
If the operator is being used interactively (e.g. at a prompt) we will still
use a temporary local fast path (in case the user is working on a slow network path).
"""
context.activateLocalFastPath()
try:
os.makedirs(context.getFastPath())
except OSError:
# If FAST_PATH exists already that generally should be an error because
# different processes will be stepping on each other.
# The exception to this rule is in cases that instantiate multiple operators in one
# process (e.g. unit tests that loadTestReactor). Since the FAST_PATH is set at
# import, these will use the same path multiple times. We pass here for that reason.
if not os.path.exists(context.getFastPath()):
# if it actually doesn't exist, that's an actual error. Raise
raise
def _checkReactorCycleAttrs(self, attrsDict):
"""Check that the list has nCycles number of elements."""
for name, param in attrsDict.items():
if len(param) != self.cs["nCycles"]:
raise ValueError(
"The `{}` setting did not have a length consistent with the number of cycles.\n"
"Expected {} value(s), but only had {} defined.\n"
"Current input: {}".format(
name, self.cs["nCycles"], len(param), param
)
)
def _consistentPowerFractionsAndStepLengths(self):
"""
Check that the internally-resolved _powerFractions and _stepLengths have
consistent shapes, if they both exist.
"""
if self._powerFractions and self._stepLengths:
for cycleIdx in range(len(self._powerFractions)):
if len(self._powerFractions[cycleIdx]) != len(
self._stepLengths[cycleIdx]
):
raise ValueError(
"The number of entries in lists for subcycle power "
f"fractions and sub-steps are inconsistent in cycle {cycleIdx}"
)
@property
def atEOL(self):
"""
Return whether we are approaching EOL.
For the standard operator, this will return true when the current cycle
is the last cycle (cs["nCycles"] - 1). Other operators may need to
impose different logic.
"""
return self.r.p.cycle == self.cs["nCycles"] - 1
def initializeInterfaces(self, r):
"""
Attach the reactor to the operator and initialize all interfaces.
This does not occur in `__init__` so that the ARMI operator can be initialized before a
reactor is created, which is useful for summarizing the case information quickly.
Parameters
----------
r : Reactor
The Reactor object to attach to this Operator.
"""
self.r = r
r.o = self # TODO: this is only necessary for fuel-handler hacking
with self.timer.getTimer("Interface Creation"):
self.createInterfaces()
self._processInterfaceDependencies()
if context.MPI_RANK == 0:
runLog.header("=========== Interface Stack Summary ===========")
runLog.info(reportingUtils.getInterfaceStackSummary(self))
self.interactAllInit()
else:
self._attachInterfaces()
self._loadRestartData()
def __repr__(self):
return "<{} {} {}>".format(self.__class__.__name__, self.cs["runType"], self.cs)
def __enter__(self):
"""Context manager to enable interface-level error handling hooks."""
return self
def __exit__(self, exception_type, exception_value, stacktrace):
if any([exception_type, exception_value, stacktrace]):
runLog.error(
r"{}\n{}\{}".format(exception_type, exception_value, stacktrace)
)
self.interactAllError()
def operate(self):
"""
Run the operation loop.
See Also
--------
mainOperator : run the operator loop on the primary MPI node (for parallel runs)
workerOperate : run the operator loop for the worker MPI nodes
"""
self._mainOperate()
def _mainOperate(self):
"""
Main loop for a standard ARMI run. Steps through time interacting with the interfaces.
"""
self.interactAllBOL()
startingCycle = self.r.p.cycle # may be starting at t != 0 in restarts
for cycle in range(startingCycle, self.cs["nCycles"]):
keepGoing = self._cycleLoop(cycle, startingCycle)
if not keepGoing:
break
self.interactAllEOL()
def _cycleLoop(self, cycle, startingCycle):
"""Run the portion of the main loop that happens each cycle."""
self.r.p.cycleLength = self.cycleLengths[cycle]
self.r.p.availabilityFactor = self.availabilityFactors[cycle]
self.r.p.cycle = cycle
self.r.core.p.coupledIteration = 0
if cycle == startingCycle:
startingNode = self.r.p.timeNode
else:
startingNode = 0
self.r.p.timeNode = startingNode
halt = self.interactAllBOC(self.r.p.cycle)
if halt:
return False
for timeNode in range(startingNode, int(self.burnSteps[cycle])):
self.r.core.p.power = (
self.powerFractions[cycle][timeNode] * self.cs["power"]
)
self.r.p.capacityFactor = (
self.r.p.availabilityFactor * self.powerFractions[cycle][timeNode]
)
self.r.p.stepLength = self.stepLengths[cycle][timeNode]
self._timeNodeLoop(cycle, timeNode)
else: # do one last node at the end using the same power as the previous node
timeNode = self.burnSteps[cycle]
self._timeNodeLoop(cycle, timeNode)
self.interactAllEOC(self.r.p.cycle)
return True
def _timeNodeLoop(self, cycle, timeNode):
"""Run the portion of the main loop that happens each subcycle."""
self.r.p.timeNode = timeNode
self.interactAllEveryNode(cycle, timeNode)
# perform tight coupling if requested
if self.cs["numCoupledIterations"]:
for coupledIteration in range(self.cs["numCoupledIterations"]):
self.r.core.p.coupledIteration = coupledIteration + 1
self.interactAllCoupled(coupledIteration)
def _interactAll(self, interactionName, activeInterfaces, *args):
"""
Loop over the supplied activeInterfaces and perform the supplied interaction on each.
Notes
-----
This is the base method for the other ``interactAll`` methods.
"""
interactMethodName = "interact{}".format(interactionName)
printMemUsage = self.cs["verbosity"] == "debug" and self.cs["debugMem"]
if self.cs["debugDB"]:
self._debugDB(interactionName, "start", 0)
halt = False
cycleNodeTag = self._expandCycleAndTimeNodeArgs(*args)
runLog.header(
"=========== Triggering {} Event ===========".format(
interactionName + cycleNodeTag
)
)
for statePointIndex, interface in enumerate(activeInterfaces, start=1):
self.printInterfaceSummary(
interface, interactionName, statePointIndex, *args
)
# maybe make this a context manager
if printMemUsage:
memBefore = memoryProfiler.PrintSystemMemoryUsageAction()
memBefore.broadcast()
memBefore.invoke(self, self.r, self.cs)
interactionMessage = " {} interacting with {} ".format(
interactionName, interface.name
)
with self.timer.getTimer(interactionMessage):
interactMethod = getattr(interface, interactMethodName)
halt = halt or interactMethod(*args)
if self.cs["debugDB"]:
self._debugDB(interactionName, interface.name, statePointIndex)
if printMemUsage:
memAfter = memoryProfiler.PrintSystemMemoryUsageAction()
memAfter.broadcast()
memAfter.invoke(self, self.r, self.cs)
memAfter -= memBefore
memAfter.printUsage(
"after {:25s} {:15s} interaction".format(
interface.name, interactionName
)
)
self._checkCsConsistency()
runLog.header(
"=========== Completed {} Event ===========\n".format(
interactionName + cycleNodeTag
)
)
return halt
def printInterfaceSummary(self, interface, interactionName, statePointIndex, *args):
"""
Log which interaction point is about to be executed.
This looks better as multiple lines but it's a lot easier to grep as one line.
We leverage newlines instead of long banners to save disk space.
"""
nodeInfo = self._expandCycleAndTimeNodeArgs(*args)
line = "=========== {:02d} - {:30s} {:15s} ===========".format(
statePointIndex, interface.name, interactionName + nodeInfo
)
runLog.header(line)
@staticmethod
def _expandCycleAndTimeNodeArgs(*args):
"""Return text annotating the (cycle, time node) args for each that are present."""
cycleNodeInfo = ""
for label, step in zip((" - cycle {}", ", node {}"), args):
cycleNodeInfo += label.format(step)
return cycleNodeInfo
def _debugDB(self, interactionName, interfaceName, statePointIndex=0):
"""
Write state to DB with a unique "statePointName", or label.
Notes
-----
Used within _interactAll to write details between each physics interaction when cs['debugDB'] is enabled.
Parameters
----------
interactionName : str
name of the interaction (e.g. BOL, BOC, EveryNode)
interfaceName : str
name of the interface that is interacting (e.g. globalflux, lattice, th)
statePointIndex : int (optional)
used as a counter to make labels that increment throughout an _interactAll call. The result should be fed
into the next call to ensure labels increment.
"""
dbiForDetailedWrite = self.getInterface("database")
db = dbiForDetailedWrite.database if dbiForDetailedWrite is not None else None
if db is not None and db.isOpen():
# looks something like "c00t00-BOL-01-main"
statePointName = "c{:2<0}t{:2<0}-{}-{:2<0}-{}".format(
self.r.p.cycle,
self.r.p.timeNode,
interactionName,
statePointIndex,
interfaceName,
)
db.writeStateToDB(self.r, statePointName=statePointName)
def _checkCsConsistency(self):
"""Debugging check to verify that CS objects are not unexpectedly multiplying."""
cs = settings.getMasterCs()
wrong = (self.cs is not cs) or any((i.cs is not cs) for i in self.interfaces)
if wrong:
msg = ["Master cs ID is {}".format(id(cs))]
for i in self.interfaces:
msg.append("{:30s} has cs ID: {:12d}".format(str(i), id(i.cs)))
msg.append("{:30s} has cs ID: {:12d}".format(str(self), id(self.cs)))
raise RuntimeError("\n".join(msg))
runLog.debug(
"Reactors, operators, and interfaces all share master cs: {}".format(id(cs))
)
def interactAllInit(self):
"""Call interactInit on all interfaces in the stack after they are initialized."""
allInterfaces = self.interfaces[:] # copy just in case
self._interactAll("Init", allInterfaces)
def interactAllBOL(self, excludedInterfaceNames=()):
"""
Call interactBOL for all interfaces in the interface stack at beginning-of-life.
All enabled or bolForce interfaces will be called excluding interfaces with excludedInterfaceNames.
"""
activeInterfaces = [
ii
for ii in self.interfaces
if (ii.enabled() or ii.bolForce()) and not ii.name in excludedInterfaceNames
]
activeInterfaces = [
ii
for ii in activeInterfaces
if ii.name not in self.cs["deferredInterfaceNames"]
]
self._interactAll("BOL", activeInterfaces)
def interactAllBOC(self, cycle):
"""Interact at beginning of cycle of all enabled interfaces."""
activeInterfaces = [ii for ii in self.interfaces if ii.enabled()]
if cycle < self.cs["deferredInterfacesCycle"]:
activeInterfaces = [
ii
for ii in activeInterfaces
if ii.name not in self.cs["deferredInterfaceNames"]
]
return self._interactAll("BOC", activeInterfaces, cycle)
def interactAllEveryNode(self, cycle, tn, excludedInterfaceNames=None):
"""
Call the interactEveryNode hook for all enabled interfaces.
All enabled interfaces will be called excluding interfaces with excludedInterfaceNames.
Parameters
----------
cycle : int
The cycle that is currently being run. Starts at 0
tn : int
The time node that is currently being run (0 for BOC, etc.)
excludedInterfaceNames : list, optional
Names of interface names that will not be interacted with.
"""
excludedInterfaceNames = excludedInterfaceNames or ()
activeInterfaces = [
ii
for ii in self.interfaces
if ii.enabled() and ii.name not in excludedInterfaceNames
]
self._interactAll("EveryNode", activeInterfaces, cycle, tn)
def interactAllEOC(self, cycle, excludedInterfaceNames=None):
"""Interact end of cycle for all enabled interfaces."""
excludedInterfaceNames = excludedInterfaceNames or ()
activeInterfaces = [
ii
for ii in self.interfaces
if ii.enabled() and ii.name not in excludedInterfaceNames
]
self._interactAll("EOC", activeInterfaces, cycle)
def interactAllEOL(self):
"""
Run interactEOL for all enabled interfaces.
Notes
-----
If the interfaces are flagged to be reversed at EOL, they are separated from the main stack and appended
at the end in reverse order. This allows, for example, an interface that must run first to also run last.
"""
activeInterfaces = [ii for ii in self.interfaces if ii.enabled()]
interfacesAtEOL = [ii for ii in activeInterfaces if not ii.reverseAtEOL]
activeReverseInterfaces = [ii for ii in activeInterfaces if ii.reverseAtEOL]
interfacesAtEOL.extend(reversed(activeReverseInterfaces))
self._interactAll("EOL", interfacesAtEOL)
def interactAllCoupled(self, coupledIteration):
"""
Interact for tight physics coupling over all enabled interfaces.
Tight coupling implies operator-split iterations between two or more physics solvers at the same solution
point in time. For example, a flux solution might be computed, then a temperature solution, and then
another flux solution based on updated temperatures (which updated densities, dimensions, and Doppler).
This is distinct from loose coupling, which would simply uses the temperature values from the previous timestep
in the current flux solution. It's also distinct from full coupling where all fields are solved simultaneously.
ARMI supports tight and loose coupling.
"""
activeInterfaces = [ii for ii in self.interfaces if ii.enabled()]
self._interactAll("Coupled", activeInterfaces, coupledIteration)
def interactAllError(self):
"""Interact when an error is raised by any other interface. Provides a wrap-up option on the way to a crash."""
for i in self.interfaces:
runLog.extra("Error-interacting with {0}".format(i.name))
i.interactError()
def createInterfaces(self):
"""
Dynamically discover all available interfaces and call their factories, potentially adding
them to the stack.
An operator contains an ordered list of interfaces. These communicate between
the core ARMI structure and auxiliary computational modules and/or external codes.
At specified interaction points in a run, the list of interfaces is executed.
Each interface optionally defines interaction "hooks" for each of the interaction points.
The normal interaction points are BOL, BOC, every node, EOC, and EOL. If an interface defines
an interactBOL method, that will run at BOL, and so on.
The majority of ARMI capabilities lie within interfaces, and this architecture provides
much of the flexibility of ARMI.
See Also
--------
addInterface : Adds a particular interface to the interface stack.
armi.interfaces.STACK_ORDER : A system to determine the required order of interfaces.
armi.interfaces.getActiveInterfaceInfo : Collects the interface classes from relevant
packages.
"""
interfaceList = interfaces.getActiveInterfaceInfo(self.cs)
for klass, kwargs in interfaceList:
self.addInterface(klass(self.r, self.cs), **kwargs)
def addInterface(
self,
interface,
index=None,
reverseAtEOL=False,
enabled=True,
bolForce=False,
):
"""
Attach an interface to this operator.
Notes
-----
Order matters.
Parameters
----------
interface : Interface
the interface to add
index : int, optional. Will insert the interface at this index rather than appending it to the end of
the list
reverseAtEOL : bool, optional.
The interactEOL hooks will run in reverse order if True. All interfaces with this flag will be run
as a group after all other interfaces.
This allows something to run first at BOL and last at EOL, etc.
enabled : bool, optional
If enabled, will run at all hooks. If not, won't run any (with possible exception at BOL, see bolForce).
Whenever possible, Interfaces that are needed during runtime for some peripheral
operation but not during the main loop should be instantiated by the
part of the code that actually needs the interface.
bolForce: bool, optional
If true, will run at BOL hook even if disabled. This is often a sign
that the interface in question should be ephemerally instantiated on demand
rather than added to the interface stack at all.
Raises
------
RuntimeError
If an interface of the same name or function is already attached to the
Operator.
"""
if self.getInterface(interface.name):
raise RuntimeError(
"An interface with name {0} is already attached.".format(interface.name)
)
iFunc = self.getInterface(function=interface.function)
if iFunc:
if issubclass(type(iFunc), type(interface)):
runLog.info(
"Ignoring Interface {newFunc} because existing interface {old} already "
" more specific".format(newFunc=interface, old=iFunc)
)
return
elif issubclass(type(interface), type(iFunc)):
self.removeInterface(iFunc)
runLog.info(
"Will Insert Interface {newFunc} because it is a subclass of {old} interface and "
" more derived".format(newFunc=interface, old=iFunc)
)
else:
raise RuntimeError(
"Cannot add {0}; the {1} already is designated "
"as the {2} interface. Multiple interfaces of the same "
"function is not supported.".format(
interface, iFunc, interface.function
)
)
runLog.debug("Adding {0}".format(interface))
if index is None:
self.interfaces.append(interface)
else:
self.interfaces.insert(index, interface)
if reverseAtEOL:
interface.reverseAtEOL = True
if not enabled:
interface.enabled(False)
interface.bolForce(bolForce)
interface.attachReactor(self, self.r)
def _processInterfaceDependencies(self):
"""
Check all interfaces' dependencies and adds missing ones.
Notes
-----
Order does not matter here because the interfaces added here are disabled and playing supporting
role so it is not intended to run on the interface stack. They will be called by other interfaces.
As mentioned in :py:meth:`addInterface`, it may be better to just insantiate utility code
when its needed rather than rely on this system.
"""
# Make multiple passes in case there's one added that depends on another.
for _dependencyPass in range(5):
numInterfaces = len(self.interfaces)
# manipulation friendly, so it's ok to add additional things to the stack
for i in self.getInterfaces():
for dependency in i.getDependencies(self.cs):
name = dependency.name
function = dependency.function
klass = dependency
if not self.getInterface(name, function=function):
runLog.extra(
"Attaching {} interface (disabled, BOL forced) due to dependency in {}".format(
klass.name, i.name
)
)
self.addInterface(
klass(r=self.r, cs=self.cs), enabled=False, bolForce=True
)
if len(self.interfaces) == numInterfaces:
break
else:
raise RuntimeError("Interface dependency resolution did not converge.")
def removeAllInterfaces(self):
"""Removes all of the interfaces"""
for interface in self.interfaces:
interface.detachReactor()
self.interfaces = []
def removeInterface(self, interface=None, interfaceName=None):
"""
Remove a single interface from the interface stack.
Parameters
----------
interface : Interface, optional
An actual interface object to remove.
interfaceName : str, optional
The name of the interface to remove.
Returns
-------
success : boolean
True if the interface was removed
False if it was not (because it wasn't there to be removed)
"""
if interfaceName:
interface = self.getInterface(interfaceName)
if interface and interface in self.interfaces:
self.interfaces.remove(interface)
interface.detachReactor()
return True
else:
runLog.warning(
"Cannot remove interface {0} because it is not in the interface stack.".format(
interface
)
)
return False
def getInterface(self, name=None, function=None):
"""
Returns a specific interface from the stack by its name or more generic function.
Parameters
----------
name : str, optional
Interface name
function : str
Interface function (general, like 'globalFlux','th',etc.). This is useful when you need
the ___ solver (e.g. globalFlux) but don't care which particular one is active (e.g. SERPENT vs. DIF3D)
Raises
------
RuntimeError
If there are more than one interfaces of the given name or function.
"""
candidateI = None
for i in self.interfaces:
if (name and i.name == name) or (function and i.function == function):
if candidateI is None:
candidateI = i
else:
raise RuntimeError(
"Cannot retrieve a single interface as there are multiple "
"interfaces with name {} or function {} attached. ".format(
name, function
)
)
return candidateI
def interfaceIsActive(self, name):
"""True if named interface exists and is active."""
i = self.getInterface(name)
return i and i.enabled()
def getInterfaces(self):
"""
Get list of interfaces in interface stack.
Notes
-----
Returns a copy so you can manipulate the list in an interface, like dependencies.
"""
return self.interfaces[:]
def reattach(self, r, cs=None):
"""Add links to globally-shared objects to this operator and all interfaces.
Notes
-----
Could be a good opportunity for weakrefs.
"""
self.r = r
self.r.o = self
if cs is not None:
self.cs = cs
for i in self.interfaces:
i.r = r
i.o = self
if cs is not None:
i.cs = cs
def detach(self):
"""
Break links to globally-shared objects to this operator and all interfaces.
May be required prior to copying these objects over the network.
Notes
-----
Could be a good opportunity for weakrefs.
"""
if self.r:
self.r.o = None
self.r = None
for i in self.interfaces:
i.o = None
i.r = None
i.cs = None
def _attachInterfaces(self):
"""
Links all the interfaces in the interface stack to the operator, reactor, and cs.
See Also
--------
createInterfaces : creates all interfaces
addInterface : adds a single interface to the stack
"""
for i in self.interfaces:
i.attachReactor(self, self.r)
def dumpRestartData(self, cycle, time_, factorList):
"""
Write some information about the cycle and shuffling to a auxiliary file for potential restarting.
Notes
-----
This is old and can be deprecated now that the database contains
the entire state. This was historically needed to have complete information regarding
shuffling when figuring out ideal fuel management operations.
"""
if cycle >= len(self.restartData):
self.restartData.append((cycle, time_, factorList))
else:
# try to preserve loaded restartdata so we don't lose it in restarts.
self.restartData[cycle] = (cycle, time_, factorList)
with open(self.cs.caseTitle + ".restart.dat", "w") as restart:
for info in self.restartData:
restart.write("cycle=%d time=%10.6E factorList=%s\n" % info)
def _loadRestartData(self):
"""
Read a restart.dat file which contains all the fuel management factorLists and cycle lengths.
Notes
-----
This allows the ARMI to do the same shuffles that it did last time, assuming fuel management logic
has not changed. Note, it would be better if the moves were just read from a table in the database.
"""
restartName = self.cs.caseTitle + ".restart.dat"
if not os.path.exists(restartName):
return
else:
runLog.info("Loading restart data from {}".format(restartName))
with open(restartName, "r") as restart:
for line in restart:
match = re.search(
r"cycle=(\d+)\s+time=(\d+\.\d+[Ee+-]+\d+)\s+factorList=[\[\{](.+?)[\]\}]",
line,
)
if match:
newStyle = re.findall(r"'(\w+)':\s*(\d*\.?\d*)", line)
if newStyle:
# key-based factorList. load a dictionary.
factorList = {}
for key, val in newStyle:
factorList[key] = float(val)
else:
# list based factorList. Load a list. (old style, backward compat)
try:
factorList = [
float(item) for item in match.group(3).split(",")
]
except ValueError:
factorList = match.group(3).split(",")
runLog.debug(
"loaded restart data for cycle %d" % float(match.group(1))
)
self.restartData.append(
(float(match.group(1)), float(match.group(2)), factorList)
)
runLog.info("loaded restart data for {0} cycles".format(len(self.restartData)))
def loadState(
self, cycle, timeNode, timeStepName="", fileName=None, updateMassFractions=None
):
"""
Convenience method reroute to the database interface state reload method
See also
--------
armi.bookeeping.db.loadOperator:
A method for loading an operator given a database. loadOperator does not
require an operator prior to loading the state of the reactor. loadState
does, and therefore armi.init must be called which requires access to the
blueprints, settings, and geometry files. These files are stored implicitly
on the database, so loadOperator creates the reactor first, and then attaches
it to the operator. loadState should be used if you are in the middle
of an ARMI calculation and need load a different time step. If you are
loading from a fresh ARMI session, either method is sufficient if you have
access to all the input files.
"""
dbi = self.getInterface("database")
if not dbi:
raise RuntimeError("Cannot load from snapshot without a database interface")
if updateMassFractions is not None:
runLog.warning(
"deprecated: updateMassFractions is no longer a valid option for loadState"
)
dbi.loadState(cycle, timeNode, timeStepName, fileName)
def snapshotRequest(self, cycle, node):
"""
Process a snapshot request at this time.
This copies various physics input and output files to a special folder that
follow-on analysis be executed upon later.
Notes
-----