Module and function reference¶
Main module pyadi¶
The main module contains the most important function for users of
PyADi: DiffFor()
, which evaluates derivatives.
It also contains the runtime function decorators D()
and
Dc()
, which are the main workhorse of PyADi. The module also
contains the mechanism by which these decorators invoke the configured
rule modules, which by default is just forwardad
. When the
rule modules at last ask the core to handle the function, source
transformation is used to produce a differentiated function. The main
module also contains the main AST visistor that performs the source
code differentiation, while many tools related to that task are also
in astvisitor
.
- class pyadi.pyadi.ASTVisitorFMAD[source]¶
- __call__(tree)[source]¶
Process the tree. Calls dispatch, which will catch only FunctionDefs and enter ddispatch traversal when the function is designated as active. Calls methods self._XYZ for individual node XYZ handling, there is only _FunctionDef.
- active_fields = []¶
- active_methods = []¶
- ddispatch(tree)[source]¶
The main workhorse, the differentiation traversal Calls methods self._DXYZ for individual node XYZ handling
- localvars = []¶
- nodiffExpr = ['Raise', 'Assert']¶
- nodiffFunctions = []¶
- nonder_builtins = ['next']¶
- nondercall_builtins = ['next']¶
- tupleDiff = ['Call', 'List', 'ListComp', 'Dict', 'DictComp', 'DictComp', 'IfExp', 'GeneratorExp', 'Starred']¶
- verbose = 0¶
- pyadi.pyadi.D(function, **opts)¶
An alias for
DiffFunction()
so the generated code can be shorter.
- pyadi.pyadi.Dc(tpl, **opts)¶
An alias for
DiffFunctionObj()
so the generated code can be shorter.
- pyadi.pyadi.DiffFD(f, *args, active=[], seed=1, h=1e-08, f_kw={}, **opts)[source]¶
Evaluate derivatves using central finite differences.
The function f is called two times for each derivative direction provided by seed, to evaluate a central finite difference with step size h. The function f is called once more to compute the original result.
- Parameters:
f (function) – Function to differentiate.
args (list) – Function arguments.
h (float) – Step size, default 1e-8
active (list or str) – Active arguments, like [0,1], [‘x’, ‘y’], or a comma-separated string like ‘x,y’. The empty list or string means all arguments. What actually happens is that a local function of only the active
args
, callingfunction
, is generated bymkActArgFunction()
and that is differentiated instead.seed (1 or list) – Seed, derivative directions. When seed == 1, all derivative directions are computed. When seed is a list, then each entry must be a list or array of the same size as the total length of the active arguments. The function
nvars()
can compute that value.f_kw (dict) – Further keyword arguments that will be passed to
f
as**f_kw
.opts (dict) – Options, not used.
- Returns:
A tuple of the derivative and the function result. The derivative is a list with as many entries as there were seed directions.
- Return type:
- pyadi.pyadi.DiffFDNP(f, *args, active=[0], seed=1, h=1e-08, f_kw={}, **opts)[source]¶
An optimized version of
DiffFD()
with some restrictions:there can be only one active argument, considering opts[‘active’].
the only active argument, the seeds, and the function result must all by
numpy
arrays.
- Parameters:
f (function) – Function to differentiate.
args (list) – Function arguments.
h (float) – Step size.
active (list or str) – Active arguments, like [0,1], [‘x’, ‘y’], or a comma-separated string like ‘x,y’. What actually happens is that a local function of only the active
args
, callingfunction
, is generated bymkActArgFunction()
and that is differentiated instead.seed (1 or list) – Seed, derivative directions. When seed == 1, all derivative directions are computed. When seed is a list, then each entry must be an array of the same size as the active argument.
f_kw (dict) – Further keyword arguments that will be passed to
f
as**f_kw
.opts (dict) – Options, of which this funciton uses:
- Returns:
A tuple of the derivative and the function result. The derivative is a list with as many entries as there were seed directions.
- Return type:
- pyadi.pyadi.DiffFor(function, *args, seed=1, active=[], f_kw=None, timings=True, verbose=0, dump=0, dumpdir='dump', **opts)[source]¶
Differentiate
function
and compute first-order derivatives evaluated at*args
and**f_kw
, w.r.t. all floats inargs
, possibly restricted byàctive
.Differentiate function
function(*args)
with forward mode AD to produceadfun
. This function is the main entry point to start the differentiation process. This function basically does the following:Differentiate
function
withDiffFunction()
aliasD()
Create one set of derivative arguments
dx = dzeros(args)
usingdzeros()
For each seeddir in seed, initialize
dx
with seeddir usingfill()
and calladfun
withdx
andargs
appropriately.
The result is a tuple of the list of the derivative results thus produced, and the function result.
Although PyADi supports almost the full set of Python language features including keyword arguments, lambda functions, etc. the
function
given here must adhere to some restrictions:function
must be a regular Python function, defined withdef
, not a lambda expression.
This function only processes the positional arguments
args
and considers all keyword arguments as options to the process, additional keyword arguments can be passed usingf_kw
.function
can have parameter default values,function
can be a local function returned by whatever other function. This setup processs will not be differentiated.function
can also have a decorator, which will be differentiated.
It may in some cases be required, and it is no problem, to create additional toplevel functions that can be given to this function, for example to wrap a lambda expression.
It should not be required to build extra toplevel functions to inject global variables into the scope, since
function
can be a local function already. It will have access to the parent scopes as usual, but the values in it are treated as global values with zero derivative.However, when a function returning a function is called within
function
, then this entire process, including possible calls to the result later, will be differentiated.- Parameters:
function (function) – Function to differentiate. Must be a regular function, defined with
def
, can be a local function.args (list) – Function arguments.
function
will be differentiated with respect to all arguments or to those listed byactive
.active (list or str) – Active arguments, like [0,1], [‘x’, ‘y’], or a comma-separated string like ‘x,y’. The empty list or string means all arguments. What actually happens is that a local function of only the active
args
, callingfunction
, is generated bymkActArgFunction()
and that is differentiated instead.seed (1 or list) – Seed, derivative directions. When seed == 1, all derivative directions are computed. When seed is a list, then each entry must be a list or array of the same size as the total length of the active arguments. The function
nvars()
can compute that value.f_kw (dict) – Further keyword arguments that will be passed to
function
as**f_kw
. This wrapsfunction
withmkKwFunction()
.opts (dict) – Further options
opts
including also verbose, dump and dumpdir are stored in a global variabletransformOpts
. These global options are added to the options ofdoDiffFunction()
by each call toD()
in the subsequent process.
- Returns:
A tuple of the derivative and the function result. The derivative is a list with as many entries as there were seed directions.
- Return type:
- pyadi.pyadi.DiffFunction(function, **opts)[source]¶
Runtime decorator to handle function calls.
This function merely caches the calls to
doDiffFunction()
, which does the actual work when no entry is found for function.Use
clear()
to clear this cache, which should be necessary only when the processing is redefined at runtime usinginitRules()
.
- pyadi.pyadi.DiffFunctionObj(tpl, **opts)[source]¶
Runtime decorator to handle calls to local variables.
Calls to local variables like
obj.meth
are differentiated to an expression invoking this function asDc((d_obj.meth, obj,meth))
, that is, with a tuple of the “differentiated” function and the original function. Differentiated is in quotes because different cases can happen.Let the tuple tpl be expanded to dfunc, func.
This function will usually call DiffFunction (aka. D) with func after handling the following cases:
When dfunc != func:
A method is being called, dfunc and func are bound methods. Extract the two self pointers from both and substitute func with the actual class function T.meth, where T is the type. T is not necessarily the type of obj but may also be a parent class when an inherited method is being called using super().
At runtime, inject the two self pointers to the front of the argument list.
When func is not a function, then an object is being called, dfunc is the derivative object. Substitute func by T.__call__, where T is the type of obj.
At runtime, inject the two self pointers (that is, dfunc and the original func) to the front of the argument list.
Otherwise, a local function inner is being called, which will have been differentiated in source code already, the call to this decorator then being Dc(d_inner, inner). Hence, dfunc is already the differentiated function of func, it can be called directly. In this case D() is not called in the following.
When dfunc == func: A function alias has been called, that is, a global function was assigned to a local variable like myf = math.sin. The differentiated variable d_myf then has dzeros(math.sin), which is also math.sin. Do nothing.
- . Finally call DiffFunction(func) and return that result, unless
the runtime arguments need patching, then return a local function doing that.
- class pyadi.pyadi.FillHelper(seed)[source]¶
A simple iterator used to source floats one by one from either a list or a
numpy
array, used byfill()
.
- pyadi.pyadi.differentiate(intree, activef=None, active=None, modules=None, filter=False, prefix=None, **kw)[source]¶
- pyadi.pyadi.doDiffFunction(function, **opts)[source]¶
Produce differentiated functions.
This function is called to produce a derivative function for
function
, that is, a function that is called with tuples (dx, x) for each original argument x in the original code, and that returns a tuple (dr, r) where r is the function result and dr is the derivative.This function will call
processRules()
, which calls the installed rule modules.The default rule module
forwardad
will for example catch calls toprint()
, which is a builtin function, and callmkRule()
withD_builtins_print()
to produce a suitable result, namely, it will print the differentiated arguments in an additional line to the original print, which in the case of a formatted f-string would be the same f-string, but with differentiated expressions..When no rule module catchs the call and returns a suitable function, finally the source differentiation
doSourceDiff()
is invoked. It will produce a suitable result by retrieving the AST of function usinggetast()
and differentiating that.A few special cases need to by handled as follows:
When function is a type, then a constructor is being called.
set _C = function
When _C.__init__ is not builtin, set function = _C.__init__, that is the constructor class method.
When function has a closure and the last closure entry fdec is a function this might be a call to a decorated function, that is, the result of deco(fdec), which is a local function that captured fdec. So when fdec has a decorator list (getting the ast of fdec), this might be the right thing. TODO: We should check if this is really the right function. However, substitute function by fdec. This will thus in the following get the AST of fdec, which is something like:
@mydeco2(1.23) def gdeco2(l): return gl_sum(l)
This gets differentiated to an expression decorating the regular D(fdec) with the differentiated decorator expression:
@(lambda f: D(mydeco2)((0, 1.23))[0](f, gdeco2)[0]) def d_gdeco2(d_l, l): return D(gl_sum)((d_l, l))
Which when loaded produces the differentiated inner function that the differentiated decorator, that D(mydeco2) returns, creates when called with d_gdeco2 alias f, and gdeco2. Thus, whatever happens in these decorators and the inner functions that they produce is getting differentiated regulary.
The differentiated decorator expression is one of the few cases where we have to throw away the second part of the tuples that differentiated functions produce, because we only need the differentiated result, and even twice in this case.
Then the differentiated function
adfun
is produced by callingprocessRules()
. This function returns however a local functiondef theADFun(*args, **kw):
that does the following:first flatten the argument list
args
of N tuples to a list of 2*N, alternating derivative and regular arguments. This is because the source differentiation differentiatesdef f(x, y):
todef d_f(d_x, x, d_y, y):
. Hence, the builtin rules will also be called with the flattened list of arguments. This step also forces the evaluation of potentially lazy zip and other iterators that the arguments may be.when
_C
is not None, a type, that is, a constructor has been called. Initialize two objects d_o and o withinitType()
. Prepend(d_o, o)
to the list of arguments. This requires that _C.__init__ accepts being called with no arguments. Both objects will then be reinitialized again with the provided arguments to the constructor when the differentiated __init__ methodadres
is invoked in the next step. TODO: check if we can somehow produce unitialized objects, that is, do what Python does before it calls __init__?Another corner case is when
function
is not a function but a bound method. This can only happen with global objects being called, like the methodget
ofos.environ
. Then the self pointero
is extracted and a copyd_o
to bedzeros()
-ed must be created somehow on the fly. Prepend(d_o, o)
to the list of arguments.Finally call
adres = adfun(*args, **kw)
when
adres
is None, which often happens with methods, return(None, None)
, unless_C
is not None, then a constructor has been called, return(d_o, o)
.when
adres
is an object of the builtin typegenerator
,function
was a generator function andadfun
is too, with differentiated yield statements that produce tuples. Create two coupled iteratorsd_it =
GenIter
(adres
) andit =
GenIter2
(d_it
) that in tandem iterateadres
, one returningr[0]
and the otherr[1]
of the tuplesr
produced, and return(d_it, it)
.otherwise, return
adres
, which is a tuple.
- pyadi.pyadi.execompile(source, fglobals={}, flocals={}, imports=['math', 'sys', 'os', {'pyadi': 'D'}], vars=['x'], fname='', **kw)[source]¶
- pyadi.pyadi.fill(arg, seed)[source]¶
Fill arg with values from seed.
Fill arguments arg with values from seed. Lists, tuples, dicts and objects are deep-copied and each generic value, as per
astunparse.astnode.isgeneric()
is filled with one value from seed usingFillHelper
.numpy
arrays are batch-filled in-place usingFillHelper.get()
, sodzeros()
should be used before if it is desired that arrays are cloned and the original arrays not modified
- pyadi.pyadi.getHandle(alias)[source]¶
Return handle to a rule module.
Return the second item of the result of a rule module’s decorator function, or None. This second item is meant to be a function that can manipulate or read the local scope of the decorator, for an example see the tracing rule module
trace
.The result returned becomes invalid whenever
initRules()
is called again.- Parameters:
alias (str) – The module name or alias used with
initRules()
.- Returns:
The second item that the rule module’s decorator returned, usally a second inner function.
- Return type:
object, usually function or None
- pyadi.pyadi.initRules(rules='ad=pyadi.forwardad', **opts)[source]¶
Initialize the rule processing mechanism for
processRules()
that performs the mapping of functions to differentiated functions.The
decorator()
of a rule module may return two function handles instead of one. In this case the second one can be retrieved usinggetHandle()
, possibly to manipulate the scope of the returned differentiated functions at runtime, as demonstrated by thedecorator()
of the rule moduletrace
.When the same module shall be used several times in the chain, an alias can be defined, for example:
pyadi.initRules( rules='pyadi.trace,pyadi.forwardad,tr2=pyadi.trace', tracecalls=True, verbose=True, verboseargs=True)
Then,
getHandle('tr2')
retrieves the handle to the second instance of the decorator that the trace module installed, andgetHandle('pyadi.trace')
gets that of the first, whilegetHandle('pyadi.forwardad')
is None because it does not provide a handler function.
Runtime rule modules¶
The rule modules are initialized by initRules()
, which takes
a list of python module names and imports them. PyADi provides some
builtin rule modules which are documented in this section.
Any python module may be used as a rule module, as long as it contains
a function decorator()
.
The rules modules come in two categories: Modules that do the actual
thing, of which there are forwardad
and dummyad
,
and modules which do nothing, inserting themselves into the chain
without changing anything to the arguments and results. They may
choose to intercept all the function calls, but then again they do not
change the result and call the given function. The No-Op modules are
trace
and timing
,
Module forwardad¶
The default rule module for PyADi.
It resolves functions for which it has a handler registered to a corresponding function compatible with our AD process, that is, function that takes the flattened list of 2*N arguments and the differentiated keywords dictionary and returns a tuple of the derivative and the function result.
When some function is not handled by this module, then source
transformation will be invoked. This in turn fails with the exception
NoSource
when the source can not be obtained. Then a
handler must be added to this module in order to make the process
work.
Defining handlers, how they are resolved and their semantics is entirely local to this module.
Handlers for any Python function function
can be set, retrieved,
and deleted with setrule()
, getrule()
, and
delrule()
, respectively.
Note the mode parameter of these functions, which can be ‘D’, ‘Dkw’ or ‘E’. This controls how the generic calling pattern is mapped to the rules:
- “D”)
mkRule()
is used to build the AD function, and therule must have the signature
rule(r, *args, **kw)
, or compatible. The wrapper calls the original function first asr = function(*args[1::2], **kw)
and then calls the rule passing the resultr
as the first parameter. The rule must return just the derivatived_r
, the wrapper returns(d_r, r)
.- “Dkw”) similar to “D”,
mkRule2()
is used to build the ADfunction, which additionally unpacks the keywords into the derivative keywords and the original keywords with
unjnd()
intod_kw, kw
. The rule must have the signaturerule(r, d_kw, *args, **kw)
. The wrapper in addition to the function result passes the derivative keywords as the second parameterd_kw
whilekw
will only receive the original keyword params.- “E”) No wrapper is produced, the rule will be called directly, which
accordingy must return a tuple.
Note that the rule modes have the precedence ‘D’, ‘Dkw’, and ‘E’. So
if you wanted to replace an existing rule with mode ‘D’ with a new
rule with mode ‘E’, make sure to delrule()
the handler with
mode ‘D’ first, or else your new rule would be shadowed by the still
extant rule with mode ‘D’.
Mode “D” is useful in that vast majority of cases. For example, the
handler for sin()
is:
def D_math_sin(r, dx, x):
return dx * cos(x)
The handler for print()
calls print again, this time with all
the differentiated arguments:
def D_builtins_print(r, *args):
print('D ', *args[0::2])
return 0
The function result that is provided by the wrapper can be put to good
use in several functions, for example the exponentials like
exp()
and sqrt()
, but also
numpy.linalg.inv()
:
def D_math_exp(r, dx, x):
return r * dx
def D_math_sqrt(r, dx, x):
return 0.5 * dx / r
def D_numpy_linalg_inv(r, dx, x):
return r @ -dx @ r
Common trivial cases are type conversions, which should be applied to the derivative arguments too:
def D_builtins_list(r, dx, x):
return list(dx)
def D_builtins_tuple(r, dx, x):
return tuple(dx)
def D_builtins_float(r, dx, x):
return float(dx)
def D_builtins_complex(r, dx, x, dy, y):
return complex(dx, dy)
Another generic case is when data is shuffled around in whatever way, then apply the same shuffling to the derivatives:
def D_builtins_ndarray_reshape(r, *args, **kw):
# args[0]: the derivative array
# args[1]: the original array
# args[3::2]: the remainder of the original arguments, i.e. the sizes
return args[0].reshape(*args[3::2])
def D_numpy_hstack(r, dx, x):
return np.hstack(dx)
def D_numpy_diag(r, dx, x):
return np.diag(dx)
def D_numpy_real(r, dx, x):
return np.real(dx)
def D_numpy_imag(r, dx, x):
return np.imag(dx)
Another trivial case, but which can be a pitfall, is where values are created that do not depend on the inputs, in which case it is paramount that the derivative values are always zero:
def D_builtins_int(r, dx, x):
return 0
def D_builtins_len(r, dx, x):
return 0
def D_builtins_Random_random(r):
return 0
When a sequence is created, we must provide a sequence of zeros:
def D_builtins_range(r, *args):
return [0]*len(r)
def D_builtins_enumerate(r, dx, x):
return zip([0]*len(x), dx)
And likewise for arrays:
def D_numpy_zeros(r, *args):
return np.zeros(r.shape, dtype=r.dtype)
D_numpy_eye = D_numpy_ones = D_numpy_random_rand = D_numpy_zeros
The intention of the mode “Dkw” is to save the call to
unjnd()
when it is not needed. The flipside is that rules
must announce when they do want the split keyword arguments, but so
far these are quite a small number, for example, dict()
itself:
def Dkw_builtins_dict(r, d_kw, *args, **kw):
return dict(**d_kw)
Another example is numpy.sum()
, which on past occasions has
it made pretty clear that it does not want to be bothered with a
keyword d_axis
or any other keyword starting with d_
for that
matter, so we give it what it needs but not what is does not want:
def Dkw_numpy_sum(r, d_kw, dx, x, **kw):
return np.sum(dx, **kw)
- pyadi.forwardad.D_builtins_RandomState_rand(r, *args)¶
- pyadi.forwardad.D_numpy_eye(r, *args)¶
- pyadi.forwardad.D_numpy_ones(r, *args)¶
- pyadi.forwardad.D_numpy_random_rand(r, *args)¶
- pyadi.forwardad.delrule(func, mode='D')[source]¶
Delete the runtime handler for
func
withmode
. Seesetrule()
for details.- Returns:
The currently installed handler.
- Return type:
function
- pyadi.forwardad.getrule(func, mode='D')[source]¶
Return the runtime handler
adfunc
forfunc
withmode
. Seesetrule()
for details.- Returns:
The currently installed handler.
- Return type:
function
- pyadi.forwardad.setrule(func, adfunc, mode='D')[source]¶
Install a runtime handler
adfunc
forfunc
withmode
.This is a function that gets called with the current original and derivative parameters in the form
adfunc(*args, **kw)
, whereargs
is a list of 2*N arguments, derivative and original arguments interleaved.Depending on
mode
theadfunc
will be handled differently, in the first two cases a wrapper function is produced:When
mode='D'
, the handler will be called asadfunc(r, *args, **kw)
, wherer
is the function result, and has to return just the derivatived_r
.When
mode='Dkw'
, the handler will be called asadfunc(r, d_kw, *args, **kw)
, where in additiond_kw, kw
is the result ofunjnd()
.When
mode='E'
, then the handler will be called directly asadfunc(*args, **kw)
, and has to return a tuple.
Module dummyad¶
This rule module is an attempt to provide a set of rules that makes the differentiated code run, with no regard for the derivative result.
Module trace¶
This rule module adds a call to any function call that monitors for certain tasks, like printing the function name and possibly arguments, and for other tasks such as even interrupting the program.
Module timing¶
This rule module adds a call to any function call that monitors
certain function calls. When a matching function is caught its call
and its descendants for a configurable height are timed with
Timer
.
- pyadi.timing.decorator(catch=[], height=1, **opts)[source]¶
This function produces a decorator
inner()
that always install another layer of function calls around the result (a function) it receives from inner layers.In addtion to the parameters, there are two closure variables, stack and found. The function
timing()
that the decorator produces maintains the current stack height in stack. When the function name matches an entry in catch, height is set to stack.When found + height > stack, then the call to the function produced by the preceding layer is sent through a timing with
Timer
.
AST utility modules¶
Module astvisitor¶
- pyadi.astvisitor.getmoddict(mod, **opts)[source]¶
Return dictionary with ASTs of module classes and functions. Resolve
from
imports withresolveImports()
.
- pyadi.astvisitor.op_revname(op)¶
- pyadi.astvisitor.op_revname_unary(op)¶
- pyadi.astvisitor.op_unrevname(op)¶
Module nodes¶
Other utility modules¶
Module runtime¶
Module dtargets¶
- pyadi.dtargets.mkActArgFunction(f, args, inds)[source]¶
Create an inner function of only the arguments given by
inds
, filling in the remaining ones statically in each call.Return the inner function and the remaining arguments.
This function is differentiated automatically by
DiffFor()
andDiffFD()
when active arguments were specified.- Parameters:
function (function) – A function of
args
, for which an inner function is created of only[args[i] for i in inds]
- Returns:
A tuple of the inner function and the remaining arguments.
- Return type:
function, list
- pyadi.dtargets.mkKwFunction(f, f_kw)[source]¶
Create an inner function that adds
**f_kw
to the call tof
.This function is differentiated automatically by
DiffFor()
when the optionf_kw
is used.- Parameters:
function (function) – A function of
args
, for which an inner function is created of only[args[i] for i in inds]
kw (dict) – Dictionary of keyword parameters to be added to the call.
- Returns:
A tuple of the inner function and the remaining arguments.
- Return type:
function