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 Lisphead(tail)
in Clike languages like Python, Perl, Fortran, CActually
[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 Clike 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, whilef2
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.

variables
()[source]¶ Compute the direct dependencies of this symbol, i.e. other variables directly occuring in the tail.

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 contextaware head mapping. Compare these examples to the ones given formap_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()
andmap_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 nonsymbols 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, usereturns_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 anyx
:>>> 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 bemap_root=False
in any recursive use. In former instances of this code, it was alwaysmap_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 reallife examples, study for instance the source code of
cpp_exporter
or grep any DDA code formap_tails
.See also
map_heads()
andmap_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 similarminded antoganist as well asmap_heads()
andmap_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 nonsymbols 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 "DDASymbol" { 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.
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
anddefault_symbol
).Type peacemaking: Query a
Symbol()
, get translated tostr()
:>>> 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`
extendscollections.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 compiletime evaluation versus runtime evaluation, when compiletime 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 thestate
. You should not do that. You go best by defining theSymbol
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 correspondingState.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 (seedsl
) 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.

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)

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
andpyGraphViz
installed, you can use this method to draw the variable dependency graph (see methoddependency_graph()
) withDot
/GraphViz
. This method will return thenx.DiGraph()
instance. Ifexport_dot
is set, it will also write a dotfile, calldot
to render it to a bitmap and open that bitmap.Note
Your distribution package
pythongraphviz
is probably notpygraphviz
. 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 islin == 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 nonstrict 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 isstate.name_computing_elements(True) == state.name_computing_elements(s[0]).name_computing_elements(s[1])....name_computing_elements(s[n])
whens
is a boolean array oflen(s)==n
andsum(s) == 1
, i.e. only one occurence ofstrict=True
and all otherFalse
.The linearized state only has entries of a normal form
state[f_i] = f(v1,v2,...)
for a function (term)f
and some variablesv_j
. Furthermore note how evenx = sum_2
in the above example indirects the former assignment ofx = 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 withvariable_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 whilebar
is only a derived quantity. Calls likeconst(float)
remain unchanged since they are already in the normal formf(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 asstate["foo"] = int(Symbol("foo"),...)
. Complex terms such asstate["foo"] = mult(int(foo), int(bar))
will result in intermediate variables called likeint_0
, ``int_1``(seename_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 changescyclic_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 stateq
in time should compute all auxillairy variables in the respective order before computing the actualdq/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 nonstrict 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 nonstrict 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.

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 writestate.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 nondata 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
orcollections.namedtuple
. Both are basically immutable, while this object type is mutable by intention.