#
# 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
#
"""
PyDDA is a small library to write and generate DDA code in Python.
DDA stands for *digital differential analyzer*. In this context, it is
a code for solving ordinary differential equations given in a domain
specific language description (i.e. an electrical circuit).
For further details, please see the ``doc/`` directory (Sphinx
documentation).
"""
import sys
assert sys.version_info >= (3, 6), \
"PyDDA uses f-strings all the ways, which require Python 3.6."
[docs]def export(state, to, **kw):
"""
Convenience function to export (transform) a state to some other
programming language.
Possible formats (allowed values for ``to``) supported so far are:
* C/C++ (via :py:mod:`dda.cpp_exporter`)
* DDA (via :py:mod:`dda.dsl`)
* SymPy (via :py:mod:`dda.sympy`)
* Latex (via :py:mod:`dda.sympy`)
This function shall be nice, so it accepts many spelling/notation
of these language names.
The return value are typically strings or tuples, dicts. There should
be no side effects.
"""
import re
from .cpp_exporter import to_cpp
from .cpp_exporter import Solver as CppSolver
from .dsl import to_traditional_dda
from .sympy import to_sympy, to_latex
from .scipy import to_scipy
exporters = {
"CppSolver": CppSolver,
r"c(\+\+|pp)?": to_cpp,
"dda": to_traditional_dda,
"sympy": to_sympy,
"latex": to_latex,
"scipy": to_scipy,
}
for k,v in exporters.items():
if re.match(k, to, re.IGNORECASE):
return v(state, **kw)
raise ValueError(f"Export format {to} not known. Valid (regexps) are {list(exporters.keys())}.")
[docs]def clean(thing, target="C"):
"""
Cleans an identifier for being compatible with the `target` language. This can be something like
`C`, `python` or `dda` (cf. languages supported by :py:meth:`dda.export`) or also `latex`.
It will basically try to transliterate all Unicode to ASCII and then try to ensure that the
identifier is a valid C variable name (i.e. don't start with numbers, etc.).
This function is nice, if you pass a :py:class:`dda.State` or :py:class:`dda.Symbol`, it will
map the whole State/Symbol. Otherwise, it expects a string.
Examples:
>>> clean("\\frac{x}{y}") # backslashes are just removed # doctest: +SKIP
'fracxy'
>>> clean(r'a^{-1}')
'a__1'
>>> clean('a^b_c^{ef}')
'a_b_c_ef'
>>> clean(u'µ²') # only if python package "unidecode" is installed # doctest: +SKIP
'u2'
>>> clean('77%alc') # well, you can use numbers at the beginning of strings
'_77alc'
"""
import re, functools
wants = lambda expr: re.match(expr, target, re.IGNORECASE)
if isinstance(thing, State) or isinstance(thing, Symbol):
return thing.map_heads(functools.partial(clean, target=target))
elif not isinstance(thing, str):
raise TypeError(f"Can only operate on strings (or States, Symbols, for convenience), but got '{thing}', which is of type {type(thing)}.")
else:
identifier = thing
original = identifier # keep a copy
# First, compose a decoder to remove any non-ascii stuff:
try:
from unidecode import unidecode
identifier = unidecode(identifier)
except ImportError as e:
identifier = identifier.encode("ascii", errors="ignore").decode()
if wants(r"c(\+\+|pp)?|python") or wants("dda"):
if re.match(r"^\d", identifier):
identifier = "_" + identifier
identifier = identifier.replace("{","").replace("}","").replace("^","_").replace("\\","")
# a^{-1} gets a_m1 and -a gets _a but a-b-c gets a_b_c
identifer = re.sub(r"(\W)-", r"\1m", identifier)
identifer = re.sub(r"^-", r"m", identifier)
identifier = identifier.replace("-","_")
# Remove anything left
allowed = "_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY"
identifier = "".join(c for c in identifier if c in allowed)
elif wants("tex"):
# Don't do anything, expect the user to be able to write valid latex identifiers.
pass
if not identifier: # empty string?
raise ValueError(f"Identifier '{original}' is so weird that it was chopped to an empty string by the clean() function. Please choose a more subtle identifier.")
return identifier
# populate namespace with some useful objects which should
# live in the package namespace.
from .ast import Symbol, State, BreveState, symbols, is_symbol
from .computing_elements import dda, dda_functions, dda_symbols
from .sympy import to_sympy
from .dsl import read_traditional_dda, to_traditional_dda, read_traditional_dda_file