Site navigation:


Symbolic classes

Contents

  1. Overview
  2. Symbolic Manipulation
  3. Expression simplification and evaluation
    1. Simplification
    2. Evaluation
    3. Advanced information about functions
    4. More symbolic substitutions: name maps
  4. Symbolic versions of built-in math names
  5. Multi-reference quantities
  6. More about the QuantSpec class
  7. Vectors of symbols
    1. Vectors and multi-reference Quantities
  8. Symbolic differentiation
  9. Specification Types
  10. Creation of Python functions from symbolic expressions
  11. Additional technical information

1. Overview

Symbolic math expressions are supported through the Quantity and QuantSpec classes, defined in Symbolic.py. These classes include the ability to perform numerical or symbolic substitution, expression evaluation, and elementary forms of simplification. The simplifications are partially based on the simplifying nature of the Python math expression parser, and partially on legacy code by Pearu Peterson. These symbolic classes are the ultimate "leaves" in the hierarchy of model specification that is supported by the ModelSpec classes.

In the near future we hope to improve the parsing of PyDSTool symbolic expressions to take full advantage of Python's abstract syntax tree classes, thereby greatly improving the speed of symbolic processing.

The rest of this page describes some of the symbolic manipulation features provided. Further examples can be found by running tests/Symbolic_test.py, tests/Symbolic_Diff_test.py and examining the output.

2. Symbolic Manipulation

The essence of symbolic manipulation is the definition of Quantity objects. There are three types of Quantity, a Var (Variable), Par (Parameter), and Fun (Function).

Let us start by defining a variable named 'v' and a parameter 'p'. 'v' might contain the definition of an auxiliary variable, for instance, in terms of state variables, parameters, or functions.

>>> k=Var('k')   # declaration
>>> v=Var('(((k-(-10)))/(3+4))', 'v')
>>> print v
v
>>> apar=Par('p')
>>> print apar, apar()
p p
>>> apar2=Par('3.5e-3', 'p')
>>> print apar2, apar2()
p 3.5e-3

Notice that the string representation of a Quantity is the Quantity's name. To see what a Quantity is defined to be, we must call it before taking its string representation, as we see for the Var and the Par objects. When a definition is not given to the Quantity, as for 'apar', its definition is merely its name. This usage is more akin to a declaration of a name than a definition. At this time there is no class method to add the specification directly to a Quantity that has only been declared. (Such a method is not really necessary when the overhead in checking the specification is almost identical to that necessary to re-create the object entirely.)

>>> print v()
(((k-(-10)))/(3+4))

In technical terms, symbols appearing in the definition are 'free', and generally refer to other Quantities. They are not treated as 'local parameters', and therefore this is not the same as defining a function. Compare:

>>> v.freeSymbols
['k']
>>> f=Fun('(((k-(-10)))/(3+4))', [k], 'v')
>>> f.freeSymbols
[]
>>> print f(4)
2.0
>>> print f(k)
(k+10)/7
>>> print f(4,5)
ValueError: Invalid number of arguments for auxiliary function

Notice that the function assigned to the Python name 'f' was named 'v', and used the same definition string as the Var Quantity 'v'. Therefore, calling f(4) simply returns the name of the function applied to its arguments. Calling the function with the requisite arguments works as for other Quantities -- it returns the Quantity's definition. The function knows how many arguments it takes, and returns an error if this number is incorrect. The symbol k in square brackets in the second argument of the Fun initialization specifies the "signature" of the function.

3. Expression simplification and evaluation

3.1. Simplification

The simplification of expressions uses evaluation. Algebraic and arithmetical simplification of the definition of a Quantity is done using the object's simplify method. simplify works in-place on the object, and does not return anything. To return a simplified string without altering the original object, call the object's eval method with no arguments.

>>> print v()
(((k-(-10)))/(3+4))
>>> print v.eval()
(k+10)/7
>>> print v()
(((k-(-10)))/(3+4))
>>> v.simplify()
>>> print v()
(k+10)/7

Note that after simplification, the double negative and the extraneous braces have been removed, as well as the arithmetical sub-expression "3+4" simplified to "7".

3.2. Evaluation

Quantities can also be evaluated at specific values for the free symbols, but care must be taken with functions in terms of the order in which the operations will be performed. Evaluations at non-numeric values for a symbol performs symbolic substitution:

>>> print v.eval(k=4)
2
>>> print f(4)
v(4)
>>> print f.eval(k=4)
2
>>> print f(k).eval(k=4)
2
>>> print f.eval(k='kval')
(kval+10)/7
>>> kquantvar = Var('kquant')
>>> print f.eval(k=kquantvar)
(kquant+10)/7

The difference between f.eval(k=4) and f(k).eval(k=4) is subtle, and is dealt with in the next sub-section.

There is another function available for expression substitution, subs, that can act more intelligently regarding defined Parameter quantities. The subs function can also take a dictionary of assignments. The result of the substitution will always be simplified as much as possible.

>>> print subs(v(), {'k': 4})
2
>>> print subs(v(), {'k': '4'})
2
>>> kpar=Par('4', 'k')
>>> print subs(v(), kpar)
2

In the second call, the string "4" has been substituted for the 'k', and the result is the same. In the third, a defined parameter has been provided as a substitution.

Remember to use subs on the correct object, otherwise you might get unexpected results!

>>> print subs(v, {'k': 4})
v
>>> print subs(v, {'v': 4})
4

The first call did nothing because the Quantity assigned to Python name 'v' evaluates to its name, which is also 'v'. This name does not match any of the assignments in the first call to subs, but it does in the second.

Evaluation of one symbolic expression can perform automatic substitution of values defined by expressions in the local scope:

>>> q=Quantity('xv+1','qv')
>>> x=Quantity('3','xv')
>>> print q.eval()
4
>>> a=x/q
>>> print a
xv/qv
>>> print a.eval()
0.75

If QuantSpecs were used in place of Quantity objects in the above code, print a would yield the division in terms of the definitions, namely 3/(xv+1).

Evaluation with an explicitly-supplied scope dictionary is much faster, if the relevant definitions are known. Equivalently, these definitions can be supplied in the call as a comma-separated sequence. For instance:

>>> print a.eval(xv=x)
3/qv
>>> print a.eval(xv=x,qv=q())  # much faster
0.75
>>> print a.eval({'xv': x, 'qv': q()})  # equivalently fast

Note from the above that Quantities only evaluate to their definitions if they are called to reduce them to a QuantSpec object. Otherwise, only the Quantity's "name" will be substituted.

This is also the best way to implement partial evaluation, and when substitution overrides are needed:

>>> print a.eval(xv=5,qv=q())  # override xv=3 from local scope
0.833333333333

Notice that the override even applies to the substitution made into q, which was defined as xv+1, so that the result is 5/6 and not 5/4.

3.3. Advanced information about functions

Compare the behaviour of eval in the following:

>>> a=Var('a');b=Var('b');c=Par('c')   # declarations
>>> gfun=Fun(a+b/c, [a,b], 'g')
>>> print gfun.eval(a=4)
ValueError: All function signature arguments are necessary for evaluation
>>> print gfun.eval(a=4,b=3)
4+3.0/c
>>> print gfun.eval(a=4,b=3,c=3)
5
>>> print gfun(4,b)
4+b/c
>>> print gfun(a,b).eval(a=4)  # equivalent, but too much syntax!
4+b/c

Firstly, gfun is a function of two arguments, 'a' and 'b', while 'c' is a free variable that must refer to a parameter name if the Fun object is to be used in the specification of a right-hand side. When eval is called directly on the function, the full function signature must be supplied. But gfun(a,b) evaluates to the QuantSpec object containing the definition of the function. As this is not a function object any of the free names 'a', 'b', or 'c' can be evaluated using eval.

Note that any evaluations given for names not present in the definition of the object will be ignored.

3.4. More symbolic substitutions: name maps

A more efficient way to make multiple symbolic substitutions which are solely textual, i.e. do not involve algebraic simplification, is to use the mapNames(<name_mapping_dict>) method of a Quantity. This method takes a dictionary of source -> target names. Matching only occurs on whole symbols (i.e. 'tokens'): a dictionary containing the mapping ('a': 'b') will not map the symbol 'ka_2' to 'kb_2'. The method will also map the Quantity's name if it matches a source name! The method works in-place and does not return anything. In other words, the Quantity will be changed directly if any of its constituent symbols were matched.

>>> v.mapNames({'k': 'a/b'})
>>> print v()
(a/b+10)/7
>>> v.mapNames({'a': '44', '10': '-10'})
>>> print v()
(44/b-10)/7
>>> v.mapNames({'10': '20'})
>>> print v()
(44/b-20)/7

4. Symbolic versions of built-in math names

Symbolic versions of basic math functions and constants are exported by Symbolic.py and are also title-cased, in a further parallel to Maple. Calling these functions with regular numeric arguments will presently return the value of the original math function applied to that argument. Calling them with string or QuantSpec arguments will return a QuantSpec.

The functions supported included the trigonometric, exponential, and logarithmic functions, the absolute value (Abs), power (Pow), and square root (Sqrt) functions. The constants include E and Pi.

5. Multi-reference quantities

There is a way to specify a range of related Quantities at once, in a kind of macro. This is useful for repeated definitions, that perhaps are interconnected in a formulaic way. (You may be familiar with this kind of notation in the package XPP.)

>>> v = Var('v'); ipar = Par('ipar')
>>> z = Var('3+v/((1+i)*ipar)', 'z[i,0,5]')

These define six Quantities z0 to z5 that are referenced from the Python object z as z[0] to z[5], e.g. z[0] == 3+v/ipar. We can see this (with a little help from the eval method to simplify):

>>> print z[0]().eval()
x+v/ipar

The following example shows how to define a set of ODE right-hand sides that involve a circular connectivity pattern. Notice that the boundary variables w0 and w3 have to be defined separately, so that the multi-quantity definition refers to valid Quantities for all index values.

>>> w0 = Var('w0-2*(w3-w1)', 'w0', specType='RHSfuncSpec')
>>> w3 = Var('w3-2*(w2-w0)', 'w3', specType='RHSfuncSpec')
>>> w1and2 = Var('w[i]-2*(w[i-1]-w[i+1])', 'w[i,1,2]', specType='RHSfuncSpec')

Note that the Python dereferencing of a multi-def z by z[2] actually creates an instance of z2.

These definitions are also discussed on the page ModelSpec.

6. More about the QuantSpec class

The QuantSpec class was mentioned above. It should be considered a pre-Quantity form of raw symbolic expression, in that it has not yet been committed to a life as any particular Quantity sub-type (Var, Par, or Fun). As such, it can be used in different ways in different Quantity definitions, and can be manipulated symbolically in its own right. For the most consistent results, any QuantSpec results that you want to use again should be defined into a proper Var or Fun object.

>>> q=QuantSpec('q', '1+k/2')
>>> print 2 * q
2 * (1+k/2)
>>> print 1 * q
1+k/2
>>> print -q
-(1+k/2)
>>> qm = -q
>>> opqm = 1 + qm
>>> print opqm
1-(1+k/2)
>>> print opqm.eval()

A major difference to a Quantity object is that the QuantSpec's name is not an important external part of its nature: only it's definition is important. Therefore, calling print on the object always returns its definition. Also, QuantSpecs are not a good place to manipulate vectors of symbols -- these should be converted to full Quantity types.

7. Vectors of symbols

A new feature allows Quantities to define vectors of symbols (of up to rank 2). In practice this simplifies notation significantly, and permits Fun functions of several variables that can be symbolically differentiated. Examples of this are in tests/Symbolic_Diff_test.py and vode_withJac_Symbolic_test.py.

Symbolic vectors can be defined for QuantSpecs and Quantities, but are intended to be used in Quantity objects. This is because "array indexing" notation (using square brackets) is only properly supported for Quantities. However, the fromvector method turns a vector Quantity or QuantSpec into a list of non-vector QuantSpec objects, each defining a component of the vector. It takes an optional integer index argument which selects that component of the vector only. Here's a simple example.

>>> x=Var('x');y=Var('y')
>>> f=Var([-3*Pow((2*x+1),3)+2*(x+y),-y/2], 'f')
>>> print f[1]   # only works if f is a Quantity type
-y/2
>>> print f.fromvector(1)   # equivalent, safe for both types
-y/2
>>> print isinstance(df.fromvector(), list)
True
>>> 

Note that we could have used the infix ** notation for powers instead of the function Pow.

Other utility methods are provided to reduce Quantities whose definitions are entirely numeric to floating point numbers or arrays of such. Here's an example of Jacobian evaluation.

>>> df = Diff(f, [x,y])
>>> print df
[[((-3)*6*Pow((2*x+1),2)+2),2],[0,-0.5]]
>>> dfe = df.eval(x=3,y=10)
>>> print dfe   # this is a QuantSpec representation of the array
[[-880,2],[0,-0.5]]
>>> print dfe.isvector()
True
>>> print dfe.dim
2
>>> print dfe.tonumeric()   # this is an array
[[ -8.80000000e+02   2.00000000e+00]
 [  0.00000000e+00  -5.00000000e-01]]

Here's a demonstration that these methods commute in the expected way.

>>> a = df.fromvector(0).eval(x=3,y=10).tonumeric()
>>> b = df.eval(x=3,y=10).tonumeric()[0]
>>> print a[0]==b[0] and a[1]==b[1]
True

7.1. Vectors and multi-reference Quantities

You may have noticed that Quantities defining multiple variables using the square bracket notation have a clash of notation with vectors. This is easily disambiguated because the Quantity object cannot be both a multiple quantity reference and a vector. It may be vector that contains multiple quantity references, but indexing such a Quantity will return its vector components.

Some features of multiple quantity definitions are not yet fully compatible with symbolic differentiation.

8. Symbolic differentiation

Symbolic differentiation of mathematical expressions involving common mathematical functions is supported through the function Diff. Most of the contents of this file were originally written by Pearu Peterson as part of a separate project, and later adapted by Ryan Gutenkunst. As Pearu and Ryan appear to have abandoned this version of his code in favour of different (and ultimately more efficient approaches that are less compatible with the PyDSTool classes), I have taken the liberty to adapt the code and include it with PyDSTool. For instance, I fixed a few residual bugs and limitations, and extended the code to work with the Quantity and QuantSpec classes.

The Diff function is meant to be a symbolic counterpart to the numerical derivative function diff, implemented in common.py, in the spirit of the Maple symbolic engine.

For many examples see the script PyDSTool/tests/Symbolic_Diff_test.py.

9. Specification Types

There are three specification sub-types of both Quantity and QuantSpec objects, determined by the 'specType' argument at initialization. These types are 'RHSfuncSpec' (the named Quantity is defined as a right-hand side for differential or difference equation), 'ExpFuncSpec' (the named Quantity is defined explicitly by the expression, such as for an auxiliary variable), and 'ImpFuncSpec' (defined implicitly by the expression). The default is 'ExpFuncSpec'. This type does not allow the Quantity's name to appear in the defining expression, whereas the other two types do allow this.

These type strings are used by the ModelConstructor class to automatically determine how to treat each definition in a 'flattened' ModelSpec specification dictionary, and allows it to verify whether the types are compatible with the target Generator in which the specification will be instantiated. For instance, 'ImpFuncSpec' definition types cannot be supplied to an ExplicitFnGen generator class.

10. Creation of Python functions from symbolic expressions

Symbolic expressions can be turned into Python functions using the expr2fun utility. The "free names" of an expression can either be numerically substituted from a supplied name -> value dictionary or left to become formal arguments to the function.

This utility can be handy to graph auxiliary functions from their abstract specifications (e.g., see /tests/CIN.py).

11. Additional technical information

Both Quantity and QuantSpec objects have a usedSymbols attribute that shows which alphanumeric symbols were used in the object's definition (not including its name).

When defining a Quantity, additional information can be provided, such as the valid domain of the variable/parameter/function values. The domain defaults to the floating-point continuous domain [-INF,INF].