#
# Copyright (c) 2020 anabrid GmbH
# Contact: https://www.anabrid.com/licensing/
#
# This file is part of the DDA module of the PyAnalog toolkit.
#
# ANABRID_BEGIN_LICENSE:GPL
# Commercial License Usage
# Licensees holding valid commercial anabrid licenses may use this file in
# accordance with the commercial license agreement provided with the
# Software or, alternatively, in accordance with the terms contained in
# a written agreement between you and Anabrid GmbH. For licensing terms
# and conditions see https://www.anabrid.com/licensing. For further
# information use the contact form at https://www.anabrid.com/contact.
#
# GNU General Public License Usage
# Alternatively, this file may be used under the terms of the GNU
# General Public License version 3 as published by the Free Software
# Foundation and appearing in the file LICENSE.GPL3 included in the
# packaging of this file. Please review the following information to
# ensure the GNU General Public License version 3 requirements
# will be met: https://www.gnu.org/licenses/gpl-3.0.html.
# For Germany, additional rules exist. Please consult /LICENSE.DE
# for further agreements.
# ANABRID_END_LICENSE
#
"""
This module provides interplay with the SymPy package. SymPy is a lightweight
pure-python computer algebra system which is bundled with SciPy.
An adapter to/from SymPy allows to use powerful Computer Algebra basic
functions such as expression simplification.
We use this currently to provide a lean latex representation for the
cumbersome DDA expressions.
"""
from . import Symbol, dda
import builtins
identity = lambda x:x
[docs]def from_sympy(sympy_equation_list):
"""
Import a state from a set of equations from SymPy.
This function expects a python list of sympy equations where there
is a single sympy symbol on one hand and an expression on the other
hand (see examples below).
The mapping basically follows the `SymPy key invariant
<https://docs.sympy.org/latest/tutorial/manipulation.html#args>`_:
"Every well-formed SymPy expression must either have empty ``args``
or satisfy ``expr == expr.func(*expr.args)``".
Therefore we basically map a sympy expression ``(expr.func,
expr.args)`` to the DDA ``(head, tail)`` notation. While the heads
are easy to map (for instance, ``sympy.Mul`` equals ``dda.mult``),
special attention must be given to the tails, for instance SymPys
``Mul(a,b,c)`` translates to DDAs ``mult(mult(a,b),c)`` (in DDA
we always assume commutative real-valued variables). Also in DDA
there is ``neg(x)`` or ``div(x,y)`` which is represented in SymPy as
``Mul(Integer(-1), x)`` and ``Mul(Symbol('x'), Pow(Symbol('y'), Integer(-1)))``,
respectively.
"""
import sympy
raise ValueError("Not yet implemented!")
# This is terribly nontrivial.
# final mappings without simplifications
sympy2dda = {
sympy.Mul: dda.mult,
sympy.sqrt: dda.sqrt,
sympy.Abs: dda.abs,
sympy.Add: lambda *x: dda.neg(dda.sum(*x)),
sympy.exp: dda.exp,
sympy.floor: dda.floor,
sympy.integral: lambda function, *symbols: dda.int(function)
}
dda_Symbol = Symbol # just to be verbose
atom = sympy.Wild("x", properties=[lambda k: k.is_Symbol])
# Do a depth-first-traversal mapping sympy to dda expressions
def expr2dda(expr):
if len(expr.args) == 0: # reached leaf
if expr.is_number:
return float(expr)
else:
raise ValueError(f"Found {expr} but don't know how to handle.\n(srepr: {srepr(expr)})")
else: # map compound expression
# First, try shorthands:
# expressions 1/x for atom x:
#( sympy.Mul(sympy.Integer(-1), atom), lambda match: dda.inv(
# border cases which are really mad to catch:
# srepr(-3*b) -> Mul(Integer(-1), Integer(3), Symbol('b'))
# Last, try anything else
if type(expr) in sympy2dda:
return sympy2dda[type(expr)](*expr.args)
else:
raise ValueError(f"Found compound expression of type {type(expr)}, don't know how to handle. It is: {expr}\n(srepr: {srepr(expr)})")
for arg in expr.args:
expr2dda(arg)
for eq in sympy_equation_list:
eq = eq.canonical # ensure symbol on the left
lhs, rhs = eq.args
if not isinstance(lhs, sympy.Symbol):
raise ValueError(f"Missing single symbol on LHS of Sympy equation {eq}")
dda_lhs = dda_Symbol(lhs.name)
[docs]def to_sympy(state, symbol_mapper=identity, round_n=15):
"""
Export a state to a set of equations for SymPy.
Returns a list of ``sympy.Eq`` objects.
Of course it requires Sympy installed/available.
.. note::
The heart of this function is a mapping from :class:`ast.Symbol`
terms (functions) to Sympy functions, for instance by mapping
``Symbol("int")(...)`` to ``-sympy.Integral(sympy.Add(...), t)``.
Thanks to the ease of the computing elements, this mapping does
not require pattern matching but can be performed on a basic
level. However, not all terms are yet supported.
The argument `symbol_mapper` allows to apply another mapping
on the DDA Symbol heads. By default, it is the identity
function.
With Sympy, you can do all funny things, such as:
>>> from dda import *
>>> x,int,neg=symbols("x,int,neg")
>>> state = State({'x': int(neg(x), 0.2, 1)})
>>> to_sympy(state)
[Eq(x, -Integral(1.2 - x, t))]
"""
import sympy
sympy_Symbol = lambda label: sympy.Symbol(symbol_mapper(label))
# round large floats. Doesn't work yet.
rhs_rounder = lambda rhs: sympy.N(rhs, n=round_n)
int = builtins.int # just to go sure
t = sympy_Symbol("t")
def todo(*x):
raise ValueError("Requried DDA 2 Sympy function yet implemented")
dda2sympy = {
"const": lambda x: x,
"neg": lambda x: -x,
"div": lambda x,y: x/y,
"int": lambda *x: - sympy.Integral(sympy.Add(*x), t),
"sum": lambda *x: - sympy.Add(*x),
"mult": sympy.Mul,
"sqrt": sympy.sqrt,
"abs": sympy.Abs,
"exp": sympy.exp,
"floor": sympy.floor,
}
def symbol2sympy(smbl):
if isinstance(smbl, float) or isinstance(smbl, int):
return smbl # let Sympy handle the numbers
if not isinstance(smbl, Symbol): raise TypeError(f"Expecting symbol, got {smbl}")
if smbl.is_variable():
return sympy_Symbol(smbl.head) # just a variable
else: # symbl.is_term()
if smbl.head in dda2sympy:
return dda2sympy[smbl.head](*map(symbol2sympy, smbl.tail))
else:
raise ValueError(f"DDA Symbol {smbl.head} in expression {smbl} not (yet) implemented.")
equation_list = [ sympy.Eq(sympy_Symbol(lhs), rhs_rounder(symbol2sympy(state[lhs]))) for lhs in sorted(state) ]
return equation_list
[docs]def to_latex(state, chunk_n=None):
"""
Export to latex, using sympy.
This mostly differs from ``sympy.latex`` for large equation
systems where we use the align latex environment instead of
a single equation. For the above example:
>>> import sympy, dda
>>> x,int,neg=dda.symbols("x,int,neg")
>>> state = dda.State({'x': int(neg(x), 0.2, 1)})
>>> print(sympy.latex(to_sympy(state)))
\\left[ x = - \\int \\left(1.2 - x\\right)\\, dt\\right]
>>> print(to_latex(state))
\\begin{align}
x &= - \\int \\left(1.2 - x\\right)\\, dt
\\end{align}
"""
import sympy
equation_list = to_sympy(state)
# Pretty-print set of equations. That's nicer then just
# sympy.latex(self.equation_list)
s = [ f"{sympy.latex(eq.lhs)} &= {sympy.latex(eq.rhs)}" for eq in equation_list ]
equation_list_to_align = lambda t: r"\begin{align}"+"\n" + (r" \\"+"\n").join(t) + "\n" + r"\end{align}"
def chunks(lst, n):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i:i + n]
latex = "\n".join(map(equation_list_to_align, chunks(s,chunk_n) if chunk_n else [s]))
return latex