03 - Derivations#
Derivations in ReMKiT1D represent function wrappers taking in variables as arguments. They are used primarily to define derived variables, but can also be used wherever some functional dependence on variable values needs to be specified.
In this tutorial we cover:
Derivation properties (
__call__magic method on derivations) and creating derived variables directly using derivation objectsCommonly used derivations (
NodeDerivationandSimpleDerivation)Composite derivations and
DerivationClosuresTextbooks, built-in derivations
[1]:
from RMK_support import Grid, Variable, node
import RMK_support.derivations as dv
import numpy as np
Many derivations are available, encapsulating various functional dependencies in ReMKiT1D. The reader is encouraged to explore examples and documentation to see how these are used.
The most basic is the SimpleDerivation, which represents $ c:nbsphinx-math:prod_i v_i^{p_i} $, where \(c\) is a scalar, \(v_i\) are different variables and \(p_i\) are powers associated with each.
[2]:
deriv = dv.SimpleDerivation("d", # name of the derivation
multConst = 2.0, # multiplicative constant
varPowers = [1.0,-2.0] # Powers associated with the variables
# we have 2 powers so expect 2 variables
)
We can interrogate the derivation to see that it requires 2 (free) arguments.
[3]:
print(deriv.name)
print(deriv.numArgs)
d
2
Let’s create some dummy variables on a grid to showcase more derivation features
[4]:
grid = Grid(xGrid = 0.1 * np.ones(16),
interpretXGridAsWidths = True,
vGrid = 0.1 * np.ones(8),
interpretVGridAsWidths = True,
lMax = 3,
)
a,b,c = (Variable(name,grid,data=(i+1)*np.ones(16)) for i,name in enumerate(["a","b","c"]))
We can then pass the derivation to a new derived variable constructor
[5]:
derivedVar = Variable("derived",grid,
derivation=deriv, # The derivation object
derivationArgs=["a","b"] # The argument list - we need two arguments
)
print(derivedVar.isDerived)
print(derivedVar.derivation.name)
print(derivedVar.derivationArgs)
True
d
['a', 'b']
Derivations support automatic generation of derived variables by application to the correct arguments:
[6]:
derivedVar = deriv(a,b)
print(derivedVar.name)
print(derivedVar.isDerived)
print(derivedVar.derivation.name)
print(derivedVar.derivationArgs)
d
True
d
['a', 'b']
As we can see, the derivation passes its name on to the variable, we can use rename() to change this, or we can use other VariableContainer approaches when registering the variable (see previous tutorial).
Some derivations have evaluate() methods defined, these accept numpy arrays, and also overload the __call__ method.
[7]:
print(deriv(np.array([1.0, 2.0]),np.array([-1.0,-0.2])))
print(deriv.evaluate(np.array([1.0, 2.0]),np.array([-1.0,-0.2])))
[ 2. 100.]
[ 2. 100.]
We’ve seen in the previous tutorial that Node objects can be used to define derived variables. Formally, they do so via NodeDerivation objects.
[8]:
nodeDeriv = dv.NodeDerivation("node",node(a)+node(b)**2)
print(nodeDeriv.name)
print(nodeDeriv.numArgs)
node
0
Note that the number of arguments for the derivation in 0. This is because the derivation itself absorbs the Node and has 0 free arguments. We can see this by asking the derivation for it’s number of enclosed (or total) arguments:
[9]:
print(nodeDeriv.enclosedArgs)
print(nodeDeriv.totNumArgs)
2
2
We can get the argument list of a NodeDerivation by calling fillArgs(), which would normally require passing the argument list:
[10]:
print(nodeDeriv.fillArgs()) # Arguments for a NodeDerivation are enclosed
print(deriv.fillArgs("b","a")) # Arguments for a SimpleDerivation are explicit
['a', 'b']
['b', 'a']
Applying a derivation with 0 arguments is unfortunately ambiguous, so one must pass at least one argument of the correct type:
[11]:
nodeVar = nodeDeriv(c) # c will only be used to get some variable properties, but won't be an argument
print(nodeVar.name)
print(nodeVar.isDerived)
print(nodeVar.derivation.name)
print(nodeVar.derivationArgs)
node
True
node
['a', 'b']
/home/stefan/.local/lib/python3.8/site-packages/RMK_support/variable_container.py:235: UserWarning: derivationArgs set for variable node which is produced by a NodeDerivation. Ignoring in favour of node leaf variables.
warnings.warn(
The warning above can be ignored when applying a NodeDerivation.
For evaluation, the derivation still expects the correct number of total arguments.
[12]:
print(nodeDeriv(np.ones(1),2*np.ones(1)))
[5.]
Derivation Closures#
As seen above, some derivations contain enclosed arguments. While derivations such as NodeDerivation or RangeFilterDerivation (see docstrings) contain enclosed arguments by default, arguments can be enclosed explicitly using the DerivationClosure construct.
[13]:
derivClosure1 = dv.DerivationClosure(deriv,a) # Enclosing a as the first argument of deriv
derivClosure2 = dv.DerivationClosure(deriv,a,argPositions=(1,)) # Enclosing a as the second argument of deriv
print(derivClosure1.enclosedArgs)
print(derivClosure1.numArgs)
print(derivClosure2.enclosedArgs)
print(derivClosure2.numArgs)
1
1
1
1
When using fillArgs() to interrogate a Derivation with enclosed arguments, those arguments will be combined with any passed free arguments in the correct order. This is how the Python interfaces ensures that the Fortran code get the expected argument orders.
[14]:
print(derivClosure1.fillArgs("b")) # Fill missing arguments (second)
print(derivClosure2.fillArgs("b")) # Fill missing argument (first)
['a', 'b']
['b', 'a']
[15]:
derivVar = derivClosure1(b) # Acting only on b
print(derivedVar.derivationArgs)
print(derivedVar.name) # Name of the derivation whose closure was taken
['a', 'b']
d
Complete closure arithmetic#
If a DerivationClosure is complete, i.e. has 0 free arguments, it can be added to/multiplied by other complete closures to produce composite derivations.
NOTE: These are automatically named, and can quickly go over the allowed maximum ReMKiT1D derivation name lengths. It is thus advisable to rename them.
NOTE: Derivation closure arithmetic is less efficient than NodeDerivations, so whenever possible these should be used for simple calculations. The closure example in this tutorial is a good case for using nodes, and the user is encouraged to play around and try to implement it using a NodeDerivation.
[16]:
deriv1 = dv.DerivationClosure(deriv,a,b)
deriv2 = dv.DerivationClosure(deriv,b,c)
compositeDeriv = (deriv1*deriv2 + 2*deriv1**2).rename("composite")
print(compositeDeriv.name)
print(compositeDeriv.enclosedArgs)
print(compositeDeriv.fillArgs()) # a,b for deriv1, b,c for deriv 2, and a,b again for deriv1
composite
6
['a', 'b', 'b', 'c', 'a', 'b']
Unlike NodeDerivation, evaluating a DerivationClosure doesn’t require passing argument values. Note that all used derivations must have the evaluate() method define, otherwise they can only be evaluated in the Fortran code.
[17]:
print(compositeDeriv.evaluate()) # len 16 array since all variables were set as living on x only
[0.72222222 0.72222222 0.72222222 0.72222222 0.72222222 0.72222222
0.72222222 0.72222222 0.72222222 0.72222222 0.72222222 0.72222222
0.72222222 0.72222222 0.72222222 0.72222222]
We can also apply Fortran functions (see MultiplicativeDerivation docstring) to complete closures to produce new closures.
[18]:
compositeDeriv2 = dv.funApply("exp",deriv1)
print(compositeDeriv2.name) # Auto-generated
print(compositeDeriv2.enclosedArgs)
print(compositeDeriv2.fillArgs())
print(compositeDeriv2.evaluate())
exp_d
2
['a', 'b']
[1.64872127 1.64872127 1.64872127 1.64872127 1.64872127 1.64872127
1.64872127 1.64872127 1.64872127 1.64872127 1.64872127 1.64872127
1.64872127 1.64872127 1.64872127 1.64872127]
Textbooks and built-in derivations#
Derivations in ReMKiT1D are stored in the Textbook object, which also provides access to various built-in derivations.
For a list of built-in derivations and their explanations the user is referred to the Textbook docstring.
Textbooks refer to species ID’s which will be covered in a later tutorial. For now it’s safe to assume these are unique integer tags linking to species data such as mass and charge.
[19]:
textbook = dv.Textbook(grid)
# A built-in derivation
deriv = textbook["gradDeriv"] # We access derivations registered in a textbook by name
print(deriv.name)
print(deriv.numArgs)
gradDeriv
1
The user is encouraged to explore the documentation and examples for uses of built-in derivations.
NOTE: All built-in derivations can be recreated using Python-level derivation objects, but are provided for convenience and backwards-compatibility.
All derivations can be registered in a textbook, and higher-level interfaces exist that perform this automatically.
Here we demonstrate registering a derivation and getting all the registered derivation names (not including species-specific derivations)
[20]:
textbook.register(compositeDeriv) # This will register all of the derivations appearing in the composite
print(textbook.registeredDerivs)
['flowSpeedFromFlux', 'leftElectronGamma', 'rightElectronGamma', 'densityMoment', 'energyMoment', 'cclDragCoeff', 'cclDiffusionCoeff', 'cclWeight', 'fluxMoment', 'heatFluxMoment', 'viscosityTensorxxMoment', 'gridToDual', 'dualToGrid', 'distributionInterp', 'gradDeriv', 'logLee', 'maxwellianDistribution', 'd', 'dXd', 'd_pow_rmul', 'composite']
Note that composite is registered, but so are also all the individual derivations appearing in the composite:
d- the baseSimpleDerivationdXd- multiplicative derivation (thederiv1*deriv2term)d_pow_rmul- the2*deriv1**2term
Here we see more auto-generated derivation names, and why we should be careful when using closure arithmetic. It can be very powerful, but requires careful derivation renaming to avoid unwieldy or illegal auto-generated names. A future update is likely to address this in an automated way, but users are currently warned to be careful with these auto-generated names.