Abstract Syntax tree

The minimalistic pythonic standalone abstract syntax tree (AST) representation in this module is the heart of the PyDDA package. The code has no external dependencies, especially it does not rely on a Computer Algebra System or even on SymPy.

The Symbol object represents a node in a AST and the edges to it’s children. In order to simplify mass symbol generation, symbols() can be used.

The State object represents a list (set) of equations. It basically maps variables to their expressions. The State represents a (traditional) DDA file. From a python perspective, a State is not much more then a dictionary on stereoids.

class dda.ast.Symbol(head, *tail)[source]

A symbol is similar to a LISP atom which has a Head and a tail, where tail is a list. Common notations for such a type are

  • head[tail] in Mathematica,

  • (head, tail) in Lisp

  • head(tail) in C-like languages like Python, Perl, Fortran, C

  • Actually [head, *tail] in Python, but we don’t use that.

A symbol also represents a vertex (node) and it’s childs in an ordered tree. Think of head being the vertex and tail the (edge) list of children. We use the Symbol class to represent the abstract syntax tree (AST) of the DDA language for describing ODEs and circuitery.

When you call str() or similar on instances of this class, it will print its representation in the C-like notation. This notation is identical to the “classical” DDA language.

There are two types of Symbols: Variables have no tail, they just consist of a head:

>>> x = Symbol("x")
>>> print(x)
x
>>> x.head
'x'
>>> x.tail
()

In contrast, Terms have a tail:

>>> f = Symbol("f", Symbol("x"), Symbol("y"))
>>> print(f)
f(x, y)
>>> f.head
'f'
>>> f.tail
(x, y)

Variables can be used to create complex expressions for which they then serve for as a head:

>>> f,x,y,z = Symbol("f"), Symbol("x"), Symbol("y"), Symbol("z")
>>> f(x,y)
f(x, y)
>>> # example for kind of nonsensical terms:
>>> x(x,f,x)
x(x, f, x)

Calling a symbol will always replace it’s tail:

>>> f(x)(y)
f(y)

Symbols are equal to each other if their head and tail equals:

>>> a1, a2 = Symbol("a"), Symbol("a")
>>> a1 == a2
True
>>> f(x) == f(x)
True
>>> f(x) == f(x,x)
False

Symbols can be used as dictionary keys, since they hash trivially due to their unique canonical (pythonic) string interpretation.

Note

In order to avoid confusion between Python Strings and Symbols, you should

  • always use strings as Symbol heads but

  • never use strings in Symbol tails. Instead, use there Symbols only.

Think of Symbol implementing the following type (hint): Tuple[str, List[Symbol]].

The DDA code helps you to follow this guide. For instance, the representation of f1 shows that it is a symbol with two string arguments, while f2 has symbol arguments:

>>> f1 = Symbol("f", "x", "y")
>>> f2 = Symbol("f", Symbol("x"), Symbol("y"))
>>> f1
f('x', 'y')
>>> f2
f(x, y)

And DDA prevents you from shooting in your foot:

>>> f, x, y = symbols("f,x,y")
>>> f3 = Symbol(f,x,y)
Traceback (most recent call last):
...
TypeError: Trying to initialize Symbol f(x, y) but head f is a Symbol, not a String.

In previous versions of DDA, the thin line between strings and symbols hasn’t been made so clear and tracing errors was harder.

Summing up, it is a good convention to only have Symbols and floats/integers being part of the Symbol tails.

is_variable()[source]

A variable is a symbol without a tail.

is_term()[source]

A term is a symbol with a tail.

variables()[source]

Compute the direct dependencies of this symbol, i.e. other variables directly occuring in the tail.

all_variables()[source]

Like :meth:variables, but also find variables in all children.

all_terms()[source]

Like :meth:all_variables, but for terms: Returns a list of all terms in all children of this node.

map_heads(mapping)[source]

Call a mapping function on all heads in all (nested) subexpressions. The mapping is effectively carried out on the head (ie. maps strings) Returns a new mapped Symbol. This routine is suitable for renaming variables and terms within an AST. Example usage:

>>> Symbol("x", Symbol("y"), 2).map_heads(lambda head: head+"foo")
xfoo(yfoo, 2)

The mapping is unaware of the AST context, so you have to distinguish between variables and terms yourself if you need to. See also map_variables() for context-aware head mapping. Compare these examples to the ones given for map_variables():

>>> x, map, y = Symbol("x"), lambda _: "y", Symbol("y")
>>> x.map_heads(map) == x.map_variables(map)  # == y
True
>>> x(x,x).map_heads(map) == y(y,y)
True
>>> x(x, x(x)).map_heads(map) == y(y, y(y))
True
map_variables(mapping, returns_symbol=False)[source]

Calls a mapping function on all variables within the (nested) subexpressions. The mapping is effectively carried out on the head (ie. maps strings). This is a mixture between map_heads() and map_tails().

Returns a new mapped Symbol. This routine is suitable for renaming variables but not terms within the AST. Examples:

>>> x, map, y = Symbol("x"), lambda _: "y", Symbol("y")
>>> x.map_variables(map) == y
True
>>> x(x,x).map_variables(map) == x(y,y)
True
>>> x(x, x(x)).map_variables(map) == x(y, x(y))
True

This function ignores non-symbols as they cannot be variables. This is the same as map_tails() does and is handy when you have numbers within your expressions:

>>> x = Symbol("x")
>>> expr = x(123, x(9.1), x, x(x, 0.1, x))
>>> res1 = expr.map_variables(lambda xx: "y")
>>> res2 = expr.map_variables(lambda xx: Symbol("y"), returns_symbol=True)
>>> res1 == res2
True
>>> res1
x(123, x(9.1), y, x(y, 0.1, y))

If you want to use map_variables to change a variable to a term, and/or if your mapping function does not return strings but Symbols, use returns_symbol=True:

>>> Symbol("x").map_variables(lambda x: Symbol("y", 123), returns_symbol=True)
y(123)
>>> Symbol("x").map_variables(lambda x: Symbol("y", 123)) # this won't work
Traceback (most recent call last):
...
TypeError: Trying to initialize Symbol y(123) but head y(123) is a Symbol, not a String.
map_tails(mapping, map_root=False)[source]

Calls a mapping function on all tails in all (nested) subexpressions. The mapping is carried out on the tail symbols (ie. maps Symbols). Returns a new mapped Symbol. The routine is suitable for AST walking, adding/removing stuff in the tails while preserving the root symbol. This could also be called map_symbols, c.f. map_terms().

Example for recursively wrapping all function calls:

>>> x,y,z = symbols("x,y,z")
>>> x(y, z(x), x(y)).map_tails(lambda smb: Symbol("foo")(smb))
x(foo(y), foo(z(foo(x))), foo(x(foo(y))))
>>> x(y, z(x), x(y)).map_tails(lambda smb: Symbol("foo")(smb), map_root=True)
foo(x(foo(y), foo(z(foo(x))), foo(x(foo(y)))))

Example for recursively removing certain unary functions z(x) for any x:

>>> remover = lambda head: lambda x: x.tail[0] if isinstance(x,Symbol) and x.head==head else x
>>> x,y,z = symbols("x,y,z")
>>> x(y, z(x), x(z(y),x)).map_tails(remover("z"))
x(y, x, x(y, x))

The argument map_root decides whether the map is run on the root node or not. It will be map_root=False in any recursive use. In former instances of this code, it was always map_root=False. Example:

>>> (a, b), flip = symbols("a,b"), lambda smb: b if smb.head==a.head else a
>>> a(b,a,b).map_tails(flip, map_root=True)
b
>>> a(b,a,b).map_tails(flip, map_root=False)
a(a, b, a)

Note how the flip function cuts every tail and returns variables only. Here is a variant which perserves any tail:

>>> (a, b) = symbols("a,b")
>>> flipper = lambda smb: (b if smb.head==a.head else a)(*smb.tail)
>>> a(b,a,b).map_tails(flipper, map_root=True)
b(a, b, a)

Here is another example which highlights how map_tails can convert terms to variables:

>>> x, map, y = Symbol("x"), lambda _: Symbol("y"), Symbol("y")
>>> x(x,x).map_tails(map, map_root=False)
x(y, y)
>>> x(x,x(x,x)).map_tails(map, map_root=False)
x(y, y)

For real-life examples, study for instance the source code of cpp_exporter or grep any DDA code for map_tails.

See also map_heads() and map_variables() for variants.

map_terms(mapping, returns_symbol=False)[source]

Calls a mapping function on all terms within the (nested) subexpressions. The mapping is effectively carried out on the term head (ie. maps strings). See map_variables() for the similar-minded antoganist as well as map_heads() and map_tails() for more low level minded variants.

Returns a new mapped Symbol. This routine is suitable for renaming terms but not variables within the AST. Examples:

>>> x, map, y = Symbol("x"), lambda _: "y", Symbol("y")
>>> x.map_terms(map) == x
True
>>> x(x,x).map_terms(map) == y(x,x)
True
>>> x(x, x(x)).map_terms(map) == y(x, y(x))
True

This function ignores non-symbols as they cannot be variables, similar to map_variables().

It is basically map_terms(map) = map_tails(lambda smb: Symbol(map(smb.head))(smbl.tail) if symb.is_variable() else smb).

The argument returns_symbol allows to discriminate between mapping functions which return strings (for symbol heads) or symbols. The later allows for manipulating expressions.

draw_graph(graph=None)[source]

Uses graphviz to draw the AST down from this symbol.

See also :method:`State.draw_dependency_graph` for similar draph drawing code and notes on python library dependencies.

Note

This method constructs the graph by drawing edges between similar named symbols. This will not represent the abstract syntax tree if a single symbol head, regardless of whether variable or term, appears twice.

If you want to draw the actual AST with this function, you have to make each symbol (head) unique by giving them distinct names.

Simple usage example:

>>> x,y,z = symbols("x,y,z")
>>> expression = x(1,y,2,z(3,4))
>>> graph = expression.draw_graph()
>>> print(graph)  
digraph "DDA-Symbol" {
   node [shape=doublecircle]
   x
   node [shape=circle]
   x -> 1
   node [shape=doublecircle]
   y
   node [shape=circle]
   x -> y
   x -> 2
   node [shape=doublecircle]
   z
   node [shape=circle]
   z -> 3
   z -> 4
   x -> z
}
>>> graph.view() # Call this to draw the graph  
dda.ast.is_symbol(smbl)[source]

Convenience function

dda.ast.symbols(*query)[source]

Quickly make symbol objects. Usage similar to sympy’s symbol function:

>>> a, b = symbols("a", "b")
>>> x, y, z = symbols("x, y, z")
dda.ast.topological_sort(dependency_pairs)[source]

Sort a graph (given as edge list) subject to dependency constraints. The result are two lists: One for the sorted nodes, one for the unsortable (cyclically dependent) nodes.

Implementation shamelessly stolen from https://stackoverflow.com/a/42359401

class dda.ast.State(initialdata={}, type_peacemaking=True, default_symbol=True)[source]

A state is a dictionary which is by convention a mapping from variable names (as strings) to their symbolic meaning, i.e. a Symbol(). We refer to the keys in the dictionary as the Left Hand Side (LHS) and the values in the dictionary as the Right Hand Side (RHS), in analogy to an Equation.

Note

Since Symbol() spawns an AST, a state is a list of variable definitions. A DDA file is a collection of equations. Therefore, a state holds the content of a DDA file.

This class collects a number of basic helper routines for dealing with states.

In order to simplify writing DDA files in Python, this class extends the dictionary idiom with the following optional features, which are turned on by default (but can be disabled by constructor arguments type_peacemaking and default_symbol).

  • Type peacemaking: Query a Symbol(), get translated to str():

    >>> State({ "foo": Symbol("bar")})[Symbol("foo")] == Symbol("bar")
    True
    
  • Default Symbol: Automatically add an entry when unknown:

    >>> State()["foo"] == Symbol("foo")
    True
    

Note

By intention, the keys of the State are always strings, never Symbols. This also should make sure you don’t use complex ASTs for keys, such as Symbol("foo", "bar").

As State` extends collections.UserDict, you can access the underlying dictionary:

>>> x,y = symbols("x,y")
>>> add, integrate = symbols("add", "integrate")
>>> eqs = { x: add(y,y), y: integrate(x) }
>>> state = State(eqs); print(state)
State({'x': add(y, y), 'y': integrate(x)})
>>> state.data
{'x': add(y, y), 'y': integrate(x)}

Warning

Don’t be fooled by refering to the state while constructing the state. This will end up in overly complex expressions. By rule of thumb, only use Symbols at the state definition (or in particular on the right hand side). For instance, you do want to construct a state like

>>> state = State()
>>> state["x"] = Symbol("add", Symbol("y"), Symbol("y"))
>>> state["y"] = Symbol("int", Symbol("x"))
>>> state
State({'x': add(y, y), 'y': int(x)})

In contrast, this is most likely not what you want:

>>> state = State()
>>> state["x"] = Symbol("add", Symbol("y"), Symbol("y"))
>>> state["y"] = Symbol("int", state["x"])
>>> state
State({'x': add(y, y), 'y': int(add(y, y))})

This time, you did not exploit the definition of state["x"] by referencing on DDA level but instead inserted the expression by referencing on Python level. This is like compile-time evaluation versus runtime evaluation, when compile-time is at python and runtime is when evaluating the DDA expressions in some time evolution code.

Summing up, the mistake above is to reference to state while constructing7 the state. You should not do that. You go best by defining the Symbol instances before and then only using them all over the place:

>>> x, y, add, int = symbols("x, y, add, int")
>>> state = State()
>>> state[x] = add(y,y)
>>> state[y] = int(x)
>>> state
State({'x': add(y, y), 'y': int(x)})

Note

Why the name? The class name State seems arbitrary and quirky, System may be a better choice (given that the class instances hold an equation system). However, one could also argue that the class instances hold the definition for a system in a particular state. especially, State.keys() are the state variables which undergo a definition by their corresponding State.values(). Most CAS do not have a special class for collections of equations. Instead, they typically have some equation type and equation systems are sets or lists of equations. In PyDDA, we don’t have an equation type because the DDA domain specific language (see dsl) doesn’t provide advanced treatments of equations but is basically only a lengthy definition of a set of equations, which you could understand as a mapping/dictionary data type defining the state of the system. That’s why State is actually an enriched dict.

classmethod from_string(*string_or_list_of_strings)[source]

Shorthand for dsl.read_traditional_dda(). Returns new instance.

to_string()[source]

Shorthand for dsl.to_traditional_dda(). Returns a string (representation).

update([E, ]**F) → None. Update D from mapping/iterable E and F.[source]

If E present and has a .keys() method, does: for k in E: D[k] = E[k] If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v In either case, this is followed by: for k, v in F.items(): D[k] = v

equation_adder()[source]

Syntactic sugar for adding new equations to the system. Usage:

>>> state = State()
>>> x,y,z,add,int = symbols("x,y,z,add,int")
>>> eq = state.equation_adder()
>>> eq(y=int(x))
>>> eq(x=add(y,z), z=int(x,0,0.1))
>>> state
State({'x': add(y, z), 'y': int(x), 'z': int(x, 0, 0.1)})

Known limitations: This doesn’t work any better then the BreveState below because keywords must not be variables, they will always resolve to their string representation.

>>> foo = Symbol("bar")
>>> s1, s2 = State(), State()
>>> eq1 = s1.equation_adder()
>>> eq1(foo=42)
>>> s2[foo] = 42
>>> s1
State({'foo': 42})
>>> s2
State({'bar': 42})
map_tails(mapper, map_root=True)[source]

Apply Symbol.map_tails() on all right hand sides.

map_heads(mapper)[source]

This function is suitable for renaming variables. mapper is always executed on the string variable names (Symbol heads)

symbols(*query)[source]

Same as symbols() above, but register at self (state)

constant_validity()[source]

Check validity of numeric constants in the state. Depending on context, values -1 < t < +1 are illegal.

(Not yet implemented!)

dependency_graph()[source]

Returns the edge list of the variable dependency graph of this state. We can call topological_sort() on the result of this method.

A weird example including some corner cases:

>>> s1 = State.from_string("foo = const(0.7)", "bar=mult(bar,baz)", "baz=f(bar)")
>>> s1.dependency_graph()
[('bar', 'bar'), ('bar', 'baz'), ('baz', 'bar')]

Another even more weird example which exploits raw value assignment, something which is not following the foo=call(bar) requirement for DDA files:

>>> a,b,c,d,f = symbols("a,b,c,d,f")
>>> s2 = State({ a: f(0.7), b: c(b,b), c: 42, d: c })
>>> s2.dependency_graph()
[('b', 'b'), ('d', 'c')]

Note that this function always returns list of tuples of strings. No more symbols. See also draw_dependency_graph() for a quick way of exporting or plotting this graph.

draw_dependency_graph(export_dot=True, dot_filename='test.dot')[source]

If you have networkx and pyGraphViz installed, you can use this method to draw the variable dependency graph (see method dependency_graph()) with Dot/GraphViz. This method will return the nx.DiGraph() instance. If export_dot is set, it will also write a dotfile, call dot to render it to a bitmap and open that bitmap.

Note

Your distribution package python-graphviz is probably not pygraphviz. You are on the safe side if you run: pip install pygraphviz

name_computing_elements(strict=False)[source]

Name all computing elements / intermediate expressions. Returns a new State which is linearized in a way that the numbering proposes a computing order.

Linearization is an idempotent operation, i.e. for any lin = state.name_computing_elements() it is lin == lin.name_computing_elements(). Mathematically, it is a projection of the state on its linearized one.

Linearization means to define a evaluation order and to give unique names to all terms occuring. Note that all depends on the strictness (strict=True vs. the default strict=False):

>>> x, y, sum, mult = symbols("x, y, sum, mult")
>>> ns = State({ x: sum(x,y, sum(y, mult(y,x))), y: mult(x) })
>>> print(ns.name_computing_elements().to_string())              
mult_1 = mult(y, x)
sum_1 = sum(y, mult_1)
x = sum(x, y, sum_1)
y = mult(x)
>>> print(ns.name_computing_elements(strict=True).to_string())   
mult_1 = mult(y, x)
mult_2 = mult(x)
sum_1 = sum(y, mult_1)
sum_2 = sum(x, y, sum_1)
x = sum_2
y = mult_2

Here, strict means that really every term is labeled, even if this yields in “dumb” assignments such as x = sum_2. You want a strict naming when enumerating computing elements, while a non-strict naming is preferable for brief evaluation. Also note that

>>> x,const = symbols("x,const")
>>> State({ x: const(42) }).name_computing_elements(strict=False)
State({'x': const(42)})
>>> State({ x: const(42) }).name_computing_elements(strict=True)
State({'const_1': const(42), 'x': const_1})
>>> State({ x: const(42) }).name_computing_elements(strict=True).name_computing_elements(strict=True) 
/.../ast.py:819: UserWarning: State.named_computing_elements(): While counting const, I notice that  const_1 is already part of the state. Maybe you want to run name_computing_elements(strict=False) for idempotence.
  warnings.warn(...)
State({'const_1': const_1_, 'const_1_': const(42), 'x': const_1})

On this mini example, one especially sees that idempotence is only given when strict=False. It is state.name_computing_elements(True) == state.name_computing_elements(s[0]).name_computing_elements(s[1])....name_computing_elements(s[n]) when s is a boolean array of len(s)==n and sum(s) == 1, i.e. only one occurence of strict=True and all other False.

The linearized state only has entries of a normal form state[f_i] = f(v1,v2,...) for a function (term) f and some variables v_j. Furthermore note how even x = sum_2 in the above example indirects the former assignment of x = sum(x,y...). Again, for any value in the linearized state, the tail only contains variables, no terms. This is handy for many things, such as circuit drawing, imperative evaluation (in combination with variable_ordering(), cf. cpp_exporter) and determination of integrands/actual variables. For instance

>>> s = State.from_string("foo = const(0.7)", "baz=mult(bar,bar)", "bar = neg(int(neg(baz), foo, 0.3))")
>>> s
State({'bar': neg(int(neg(baz), foo, 0.3)), 'baz': mult(bar, bar), 'foo': const(0.7)})
>>> print(s.name_computing_elements(strict=True).to_string())    
bar = neg_2
baz = mult_1
const_1 = const(0.7)
foo = const_1
int_1 = int(neg_1, foo, 0.3)
mult_1 = mult(bar, bar)
neg_1 = neg(baz)
neg_2 = neg(int_1)
>>> print(s.name_computing_elements(strict=False).to_string())    
bar = neg(int_1)
baz = mult(bar, bar)
foo = const(0.7)
int_1 = int(neg_1, foo, 0.3)
neg_1 = neg(baz)

Here one sees immediately that int_1 is the actual integral solution while bar is only a derived quantity. Calls like const(float) remain unchanged since they are already in the normal form f(v1,v2,...).

Here is a more complex example:

>>> from dda.computing_elements import neg,int,mult
>>> dda_state = State({"x": neg(int(neg(int(neg(mult(1, Symbol("x")), 0.005, 1)), 0.005, 0))) })
>>> dda_state.name_computing_elements().variable_ordering().where_is 
{'x': 'vars.aux.sorted',
'mult_1': 'vars.aux.sorted',
'neg_2': 'vars.aux.sorted',
'neg_1': 'vars.aux.sorted',
'int_1': 'vars.evolved',
'int_2': 'vars.evolved'}
variable_ordering()[source]

Will perform an analysis of all variables occuring in this state (especially in the RHS). This is based on the linarized variant of this state (see name_computing_elements()).

The return value is an object (actually a types.SimpleNamespace instance) which contains lists of variable names (as strings). The properties (categories) are primarly

  • explicit constants: Any entry state["foo"] = const(1.234)

  • State variables/evolved variables: Any outcome of a time integration, i.e. int(...), i.e. Symbol("int"). This can be as simple as state["foo"] = int(Symbol("foo"),...). Complex terms such as state["foo"] = mult(int(foo), int(bar)) will result in intermediate variables called like int_0, ``int_1``(see name_computing_elements() for the code which invents these names), which are the actual evolved variables.

  • Auxilliary variables: Any other variables which are required to compute evolved variables.

By intention, we sort only the aux. variables. One should check that they DO NOT have any cyclic dependency, because feedback loops are only useful on integrators at this level of circuit modeling.

We differntiate the auxilliaries further into:

  • sorted_aux_vars: Auxilliaries required to compute the state variable changes

  • cyclic_aux_vars: Auxilliaries which have a cyclic dependency on each other (this should not happen as it won’t lead to a stable circuit)

  • unneeded_auxers: Auxilliaries which are not required to compute the state. These are probably used in postprocessing. If they depend on the state variables, further work is neccessary.

Given an ODE problem dq/dt = f(q), an imperative code for evolving the state q in time should compute all auxillairy variables in the respective order before computing the actual dq/dt. The dependency is basically, in pseudo code:

>>> aux  = function_of(aux, state)                    
>>> dqdt = function_of(aux, state)                    

and in the numerical integration schema step

>>> state = function_of(dqdt)                         

This method returns a namespace object, which is basically a fancy dictionary. It is used over a simple dictionary just for shorter syntax.

The following examples demonstrate a deeply nested corner case, i.e. a compute graph consisting of a single “long” Euler cycle. By breaking up this cycle at the integrations, variable_ordering() can linearize these cycles correctly. This works both for non-strict and strict element naming.

>>> from dda.computing_elements import neg,int,mult
>>> dda_state = State({"x": neg(int(neg(int(neg(mult(1, Symbol("x")), 0.005, 1)), 0.005, 0))) })
>>> # variable ordering is made based on non-strict naming:
>>> dda_state.name_computing_elements(strict=False) 
State({'int_1': int(neg_1),
       'int_2': int(neg_2),
       'mult_1': mult(1, x),
       'neg_1': neg(mult_1, 0.005, 1),
       'neg_2': neg(int_1, 0.005, 0),
       'x': neg(int_2)})
>>> dda_state.name_computing_elements(strict=True) 
State({'int_1': int(neg_1),
'int_2': int(neg_2),
'mult_1': mult(1, x),
'neg_1': neg(mult_1, 0.005, 1),
'neg_2': neg(int_1, 0.005, 0),
'neg_3': neg(int_2),
'x': neg_3})
>>> # This is how the full output looks like
>>> dda_state.variable_ordering()  
namespace(aux=namespace(all=['mult_1', 'neg_1', 'neg_2', 'x'],
                        sorted=['x', 'mult_1', 'neg_2', 'neg_1'],
                        cyclic=[],
                        unneeded=set()),
          evolved=['int_1', 'int_2'],
          explicit_constants=[],
          all=['int_1', 'int_2', 'mult_1', 'neg_1', 'neg_2', 'x'],
          ordering=OrderedDict([('vars.explicit_constants', []),
                                ('vars.aux.sorted',
                                 ['x', 'mult_1', 'neg_2', 'neg_1']),
                                ('vars.aux.cyclic', []),
                                ('vars.evolved', ['int_1', 'int_2']),
                                ('vars.aux.unneeded', set())]),
          where_is={'int_1': 'vars.evolved',
                    'int_2': 'vars.evolved',
                    'mult_1': 'vars.aux.sorted',
                    'neg_1': 'vars.aux.sorted',
                    'neg_2': 'vars.aux.sorted',
                    'x': 'vars.aux.sorted'})
>>> # Compare the strict and nonstrict orderings:
>>> for k, v in dda_state.variable_ordering().ordering.items(): print(f"{k:25s}: {v}")
vars.explicit_constants  : []
vars.aux.sorted          : ['x', 'mult_1', 'neg_2', 'neg_1']
vars.aux.cyclic          : []
vars.evolved             : ['int_1', 'int_2']
vars.aux.unneeded        : set()
>>> for k, v in dda_state.name_computing_elements(strict=True).variable_ordering().ordering.items(): print(f"{k:25s}: {v}")
vars.explicit_constants  : []
vars.aux.sorted          : ['neg_3', 'x', 'mult_1', 'neg_2', 'neg_1']
vars.aux.cyclic          : []
vars.evolved             : ['int_1', 'int_2']
vars.aux.unneeded        : set()
remove_duplicates()[source]

Assuming a linearized state, this function simplifies the system by removing/resolving duplicate entries. No further renaming takes place: Always the first encounter of a term determines the name for all equivalent terms.

Returns a new state.

term_statistics()[source]

Assuming a linearized state, tells how much each term occurs within this state. Returns instance of collections.Counter. For simplicity, the term heads are returned as strings.

Typical usage is like state.name_computing_elements().remove_duplicates().term_statistics()

export(to, **passed_args)[source]

Syntactic sugar for dda.export(), for convenience

class dda.ast.BreveState(initialdata={}, type_peacemaking=True, default_symbol=True)[source]

This subclass of a state adds syntactic sugar by allowing attribute/member access notation. Instead of state["foo"] you can write state.foo on instances of this class. Example usage:

>>> x,y,z = symbols("x,y,z")
>>> state = BreveState()
>>> state.x = x(y,z)
>>> state.y = y(x,z)
>>> state.z = z(x,y)
>>> print(state)
BreveState({'x': x(y, z), 'y': y(x, z), 'z': z(x, y)})

Warning

Known limitations:

  • Breaks Python class introspection (for instance tab completion in iPython)

  • Of course users cannot add any non-data related attribute (or method)

See also State.equation_adder() for similar sugar which might have unexpected effects:

>>> s, b = State(), BreveState()
>>> foo = Symbol("bar") # in this context, similar to a string foo = "bar"
>>> s[foo] = 42         # foo resolves to string represntation "bar"
>>> b.foo = 42          # equals b["foo"], thus has nothing to do with variable foo
>>> s
State({'bar': 42})
>>> b
BreveState({'foo': 42})

If you find this class useful, you also might like types.SimpleNamespace or collections.namedtuple. Both are basically immutable, while this object type is mutable by intention.