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 C-like 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 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
f1shows that it is a symbol with two string arguments, whilef2has 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 context-aware 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 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_variablesto 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_rootdecides whether the map is run on the root node or not. It will bemap_root=Falsein 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
flipfunction 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_tailscan 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_exporteror 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 similar-minded 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 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_symbolallows 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.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_peacemakinganddefault_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
Symbolsat 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
statewhile constructing7 thestate. You should not do that. You go best by defining theSymbolinstances 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
Stateseems arbitrary and quirky,Systemmay 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
BreveStatebelow 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
networkxandpyGraphVizinstalled, you can use this method to draw the variable dependency graph (see methoddependency_graph()) withDot/GraphViz. This method will return thenx.DiGraph()instance. Ifexport_dotis set, it will also write a dotfile, calldotto render it to a bitmap and open that bitmap.Note
Your distribution package
python-graphvizis 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 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 isstate.name_computing_elements(True) == state.name_computing_elements(s[0]).name_computing_elements(s[1])....name_computing_elements(s[n])whensis a boolean array oflen(s)==nandsum(s) == 1, i.e. only one occurence ofstrict=Trueand all otherFalse.The linearized state only has entries of a normal form
state[f_i] = f(v1,v2,...)for a function (term)fand some variablesv_j. Furthermore note how evenx = sum_2in 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_1is the actual integral solution whilebaris 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 stateqin 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 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.
-
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.fooon 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.SimpleNamespaceorcollections.namedtuple. Both are basically immutable, while this object type is mutable by intention.