The ConvergenceScan
class provides a simple way of quickly generating
parameter scans, particularly in the resolution parameters. A
ConvergenceScan
object holds a reference to a baseline DREAMSettings
object, as well as a list of input and output parameters. Each input and output
parameter has a name associate with it, as well as a function which modifies a
given DREAMSettings object (input) or extracts data from a
DREAMOutput object (output).
Once the ConvergenceScan
has been successfully executed, the result can be
conveniently plotted using the ConvergenceScanPlot class.
The most basic use case is illustrated by the following example (although far more complicated things can be achieved with the class):
import DREAM
# ... or
# from DREAM.ConvergenceScan import ConvergenceScan
# ... and use as just "ConvergenceScan()"
# First, create a DREAMSettings object as you normally would...
ds = DREAM.DREAMSettings()
# set up ds...
...
# Create convergence scan
cs = DREAM.ConvergenceScan(settings=ds,
inparams=['nt', 'hottail.np', 'hottail.nxi'],
outparams=['other.fluid.runawayRate'])
# Run convergence scan
cs.run()
# Save results to file
cs.save('convergence.h5')
Note
DREAM path
In order for the convergence scan to find DREAM, you must first properly set
up your environment before using the ConvergenceScan
object. Primarily,
the environment variable DREAMPATH
variable should be defined and point
to the DREAM source directory. You can read more about how to do this on the
page runiface.
Input and output parameters can be specified by name. Except for a few special
parameters, the name is the full name, including parent class names in the
owning DREAMSettings/DREAMOutput object. For example, to vary the
radial grid resolution, which is accessed in the DREAMSettings object
using DREAMSettings.radialgrid.nr
, one would generally specify the name of
the parameter as radialgrid.nr
, that is, everything after the the
DREAMSettings.
. The same applies to DREAMOutput, so that the name
eqsys.I_p
would access the final plasma current.
Parameters can be added either when the ConvergenceScan
object is created,
or afterwards (or both):
ds = DREAMSettings()
...
cs = ConvergenceScan(ds, inparams=['nt'], outparams=['other.fluid.runawayRate'])
cs.addScanParameter('radialgrid.nr')
cs.addScanParameter('hottailgrid.pgrid.np')
cs.addScanParameter('hottailgrid.pgrid.nxi')
Some input parameters are special and have aliases which allow for easier access. These are listed in the table below:
Full name |
Alias |
Description |
---|---|---|
|
|
Momentum resolution on hottail grid. |
|
|
Pitch resolution on hottail grid. |
|
|
Momentum AND pitch resolution on hottail grid (adds two input parameters). |
|
|
Radial resolution. |
|
|
Time resolution. |
|
|
Momentum resolution on runaway grid. |
|
|
Pitch resolution on runaway grid. |
|
|
Momentum AND pitch resolution on runaway grid. |
Output parameter data is accessed by index, meaning that any UnknownQuantity or OtherQuantity may be used as the output parameter. When the output parameter is given by name, the code will access only the very last element of the data. This means that, for
ScalarQuantity’s (such as I_p
), the final value is used.
FluidQuantity’s (such as E_field
), the final value at the outermost radius is used.
KineticQuantity’s (such as f_hot
), the final value at the outermost radius in \(\xi=1\) and \(p=p_{\rm max}\) is used.
When specifying output parameters by name, the very last element of the parameter data will generally be accessed.
Sometimes the parameter to scan is more complicated to set than just
increasing/decreasing it by a constant float value. In this case, one can
instead define a custom function which modifies the settings object in a
successive fashion. The function can only be provided via a call to
addScanParameter()
and must thus be added after the ConvergenceScan
object has been constructed.
The addScanParameter()
should be called in the following way:
cs.addScanParameter(name='inparam', f=customFunction, baselineValue=baseval)
These are the required parameters; the other parameters can also be specified if desired.
The name of the parameter is essentially arbitrary. It is only used by the
ConvergenceScan
object to identify the baseline value if the
baselineValue
parameter is not specified. If this is the case, the
baseline value is taken from the variable in the previously given
DREAMSettings object which has the given name.
The custom function customFunction()
should have the following signature:
customInputFunction
(index, settings, baseline)¶index (int) – Index of simulation to set up (0
means the baseline case; negative values are possible)
settings (DREAMSettings) – Settings object to modify. On input, this object is a copy of the baseline settings object specified when constructing the ConvergenceScan
object.
baseline – Baseline value for this parameter.
Tuple consisting of the modified settings object and a float
representing the value set.
Note
Lambda expressions
The use of lambda functions is often appropriate when passing functions
to addScanParameter()
and can provide more compact code
(see, for example,
https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions).
The function is supposed to modify the parameter settings
, which is a copy
of the DREAMSettings object given to the ConvergenceScan
when
constructed, and return the modified settings object along with a numerical
value representing the assigned setting (even if the value is not numerical
itself; then it could for example be index
). An example implementation is:
def _CS_getiNt(index: int, settings: DREAMSettings, baseline):
# Calculate new value to set
val = max(1,int(np.round(baseline * np.float_power(2, index))))
# Modify settings object
settings.timestep.setNt(val)
# Return modified object and new parameter value
return settings.val
The index
parameter accepted by the custom function indicates the stage of
the convergence scan to set up. Indices work such that 0
correspond to the
baseline case, while positive values indicate “higher resolution” and negative
values indicate “lower resolution” (of course, users are welcome to
reinterpret the distinction between positive and negative indices however they
desire). The scanner expects the function to modify the object in a
deterministic way so that a call with a specific index always results in the
same settings being applied. In general, the baseline case is only run once,
instead of once for each scan parameter.
By default, the starting index is -1
, which is then gradually increased
until the upper index limit is reach, which is set to 1
(inclusive) by
default.
As with complicated input parameters, more complicated output parameters can
also be accessed via a custom function. The custom function has to be added
separately via the addOutputParameter()
function in the following way:
cs.addOutputParameter(name='outparam', f=customFunction)
Optionally, a relative tolerance used for the continuous convergence scan mode can also be provided, but is not required.
If a custom function is provided, the name of the output parameter is only used when communicating with the user and has no internal significance.
The custom function should have the signature
customOutputFunction
(do: DREAMOutput)¶do (DREAMOutput) – Output object to extract parameter value from.
The value of the output parameter.
float
Note
Lambda expressions
The use of lambda functions is often appropriate when passing functions
to addOutputParameter()
and can provide more compact code
(see, for example,
https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions).
The purpose of the function is to process the given DREAMOutput object
in order to obtain the value of the output parameter resulting from the
simulation. The returned value must be a float
, although the actual value
returned generally is of little or no interest to the ConvergenceScan
object (it could for example be a binary value, varying discretely between
0 and 1). The only time the ConvergenceScan
object makes a decision based
on the value of the parameter is when scanUntilConvergence
is set to
True
for an input parameter.
An example implementation of the custom output parameter function is the following:
def customFunction(do: DREAMOutput) -> float:
# Calculate kinetic energy carried by hot electrons
Wk = do.eqsys.f_hot.kineticEnergy()[-1,:]
# Turn into a FluidQuantity
Wk = DREAM.Output.FluidQuantity('Wk', Wk, do.grid, do)
# Return total final kinetic energy
return Wk.integral(t=-1)
If you would like to find the point of convergence without manually tweaking
the scan, you can set the parameter scanUntilConvergence=True
when calling
addScanParameter()
. This will cause the ConvergenceScan
to increase the
simulation index by one until the output parameters vary by less than the
relative tolerance specified for each output parameter.
Warning
Note that there is a maximum number of permitted iterations in the
ConvergenceScan
object, called NMAX
. It defaults to 10
, meaning
that if convergence has not been reached on the tenth iteration
(corresponding to index 8
with the default starting index -1
), then
the scan will stop with an error.