terrapower / armi

An open-source nuclear reactor analysis automation framework that helps design teams increase efficiency and quality
https://terrapower.github.io/armi/
Apache License 2.0
222 stars 87 forks source link

Support specifying depletion schedule with burnup (MWd/kgHM) #1364

Open drewj-usnctech opened 1 year ago

drewj-usnctech commented 1 year ago

An alternative time schedule for depletion that is commonly used in reactor analysis is burnup in units of (typically) power * time / mass. Common variants are MWd/kgHM or GWd/tHM (same units if you assume metric tonne = 1000 kg). The heavy metal mass is often taken as the initial loading, so you may see a MWd/kgHMi just to be more clear as heavy metal mass can change through operation.

This ticket proposes allowing specifying a depletion schedule with step burnup or cumulative burnup units, mirroring the step days and cumulative days supported in the cycles arrays. An example input could look like

settings:
  power: 1000000000 # 1 GW power
  nCycles: 2
  cycles:
    - name: A
      step burnup: [0.5, 0.5, 1, 2]
    - name: B
      cumulative burnup: [5, 10, 15 ,20]

wherein cycle A would deplete to 0.5, 1, 2, and 4 MWd/kgHM, and cycle B would deplete out to 5, 10, 15, and 20 MWd/kgHM.

It makes sense to disallow including burnup and day values in the same input, as the following would make no sense

...
  cycles:
    - name: A
      step burnup: [0.5, 0.5, 1, 2]
      step days: [1, 2, 3, 4]

The same power fraction and availability factors fields should still be supported as if the schedule had been specified with days.

Given the total reactor power (known from settings file) and the heavy metal mass of the reactor (through Reactor.getHMMass()), it is possible to convert burnup steps to day steps, and provide the day stepping that ARMI uses internally.

keckler commented 1 year ago

I agree that this should be possible and could also be valuable.

It would have to be made very clear what exactly corresponds to the "initial" HM mass. For instance, is it recalculated at the beginning of each cycle after fuel is shuffled? If so, that could potentially pose some challenges from an architectural standpoint because right now the cycles setting is resolved at the very start of a run. So since it won't be known what the BOC HM mass is at later cycles, resolving the cycle lengths out until the end of a run might be a problem.

The only request I'd have is just to keep the docs up to date.

drewj-usnctech commented 1 year ago

It would have to be made very clear what exactly corresponds to the "initial" HM mass

Great point. My gut would be BOL based on Reactor.getHMMass, but if you have a Core and SFP child (or grandchild) of the reactor, do you want spent fuel or to-be-added-later fuel that technically exists at BOL to count? Maybe Core.getHMMass?

jakehader commented 1 year ago

I am not sure, but this might be a good place to consider subclassing the Standard Operator? I'd need look at the Operator definitions but we should write some requirements around how much math and how to do the math on an Operator if we extend in this way.

(I like the suggestion by the way - sometimes cross sections are better functionalized with burn-up versus time so maybe there is a good reason to know or control burnup steps intentionally)

drewj-usnctech commented 1 year ago

Taking insight from @jakehader

I am not sure, but this might be a good place to consider subclassing the Standard Operator

I dug around.

Supporting burn steps as a cycle parameter

There's no easy way around this: we would need to modify the schema for cycles to support step burnup and cumulative burnup. Otherwise we can't even create a Settings attribute with step burnup

Custom operator

One could expose their own Operator to the App with the getOperatorClassFromRunType hook

https://github.com/terrapower/armi/blob/48f543859e84428a3edbae5bc0b0b38b7f3110af/armi/plugins.py#L486-L488

but you would need a unique runType string to get around how the hook is used

https://github.com/terrapower/armi/blob/48f543859e84428a3edbae5bc0b0b38b7f3110af/armi/operators/__init__.py#L73-L85

A workaround would be to switch the order of operations here: iterate over hooks first, then fallback to standard operator / mpi operator / snapshot operator

Updating step lengths

Okay so let's assume we've gotten a custom operator subclass, and our burnup-based depletion schedule is attached to the Settings. How do we modify depletion schedule to provide step days that the rest of ARMI supports?

The first time, as far as I can see, that a reactor is near the settings is Operator.initializeInterfaces which feels like a good place to make our change. Before we start giving settings to interfaces that do stuff with settings.

So, we could override initializeInterfaces to take the reactor power and core heavy metal mass, and convert all the step burnup entries into step days with days = burnup * hmMass / power (taking correct units into consideration)

Unfortunately, we can't even make it through Operator.__init__ because the welcome headers fails try to write the step lengths for each cycle

https://github.com/terrapower/armi/blob/48f543859e84428a3edbae5bc0b0b38b7f3110af/armi/operators/operator.py#L148-L149

https://github.com/terrapower/armi/blob/48f543859e84428a3edbae5bc0b0b38b7f3110af/armi/bookkeeping/report/reportingUtils.py#L198-L203

https://github.com/terrapower/armi/blob/48f543859e84428a3edbae5bc0b0b38b7f3110af/armi/operators/operator.py#L171-L182

During the first fetching of Operator.stepLengths, _getStepAndCycleLengths fails because there aren't any step days or other "known" time step data in our cycle.

https://github.com/terrapower/armi/blob/48f543859e84428a3edbae5bc0b0b38b7f3110af/armi/utils/__init__.py#L196-L216

Possible solutions

Now, if the printing of the welcome headers can be delayed until the operator can update the settings with the reactor, then the step lengths can still be printed. If it's not necessary to do the welcome header printing every time an operator is constructed, this might be easier. Something like

o = armi.operators.factory(settings)
reportingUtils.writeWelcomeHeaders(o, settings)
jakehader commented 1 year ago

This sounds like a great refactor to support application based operators and to give more power to end users on how they want work to be performed and looks like you're on the right track. What do you need from us? Need us to propose a PR change?

keckler commented 1 year ago

Thanks for your thinking about this @drewj-usnctech . Looking at reportingUtils.writeWelcomeHeaders(), it seems to me that it might be more appropriate if that were a method on the of the operator itself. I don't really see why it sits as a function in a different module, as it is intimately tied to the initialization of an operator.

If you moved writeWelcomeHeaders() to be an Operator method, then you could just make custom implementations for each operator. This could potentially solve your issue of getting beyond Operator.__init__(). And it seems more natural anyway, IMO.

drewj-usnctech commented 1 year ago

Thanks @jakehader and @keckler

I think there are three independent changes to bring this to fruition

  1. Change ordering in armi.operators.getOperatorClassFromSettings to allow plugin hooks to provide their own operator subclass type. If no plugin hooks provide an operator given the run type, then the previous pre-hook behavior of standard / mpi / snapshot could be used
  2. Remove reportingUtils.writeWelcomeHeaders from Operator.__init__. Where to put that functionality is worth some discussion*
  3. Support step burnup (and concurrently or later cumulative burnup) in cs["cycles"] array**

Post script

welcome headers

reportingUtils.writeWelcomeHeaders boils down to four functions, only one of which is really tied to the operator: writing the operational conditions. So something like Operator.summarizeOperationalConditions() -> dict would probably play nice w/ reportingUtils.writeWelcomeHeaders

I would lean towards moving the call to reportingUtils.writeWelcomeHeaders to after the operator has been given a reactor. I think during or after armi.cases.case.Case.initializeOperator?

cs["cycles"]

If step burnup is added to cycles array, how should armi.utils.getStepLengths and getCycleLengths be updated? They both rely on armi.utils._getStepLengthsAndCycleLengths which would need some logic to handle burnup steps. Maybe just error out because both need some length of time in days to be meaningful to people asking step and cycle lengths.

Same with armi.utils.hasBurnup. This could probably support step burnup logic because we just want to know if the Settings produce a run with depletion. But it also relies on getStepLengths so maybe a carve out if getStepLengths fails?

design qs

There is a weird partially constructed problem where if I make an operator with burn steps, I can't do things like Operator.stepLengths because I don't have a reactor to convert burnup steps to day steps.

Following this subclassing thread, it feels weird that this leads to two different operators, that differ only in their depletion schedule. I don't have more thoughts on how to remedy this, but it feels worth mentioning

drewj-usnctech commented 1 year ago

@john-science similar discussions to power vs. power density discussions for #1365