Source code for sigmaepsilon.math.approx.lagrange

from typing import Iterable, Callable

import sympy as sy
from sympy import latex
import numpy as np

from sigmaepsilon.deepdict import DeepDict


__all__ = ["gen_Lagrange_1d", "approx_Lagrange_1d"]


def _var_tmpl(i: int):
    return r"\Delta_{}".format(i)


def _var_str(inds, i: int):
    return _var_tmpl(inds[i])


def _diff(xvar, fnc: sy.Expr):
    return fnc.diff(xvar).expand().simplify().factor().simplify()


[docs] def gen_Lagrange_1d( *_, x: Iterable | None = None, i: Iterable[int] = None, xsym: str | None = None, fsym: str | None = None, sym: bool = False, N: int | None = None, lambdify: bool = False, out: dict | None = None ) -> dict: """ Generates Lagrange polynomials and their derivatives up to 3rd, for approximation in 1d space, based on N input pairs of position and value. Geometrical parameters can be numeric or symbolic. Parameters ---------- x: Iterable, Optional The locations of the data points. If not specified and `sym=False`, a range of [-1, 1] is assumed and the locations are generated as `np.linspace(-1, 1, N)`, where N is the number of data points. If `sym=True`, the calculation is entirely symbolic. Default is None. i: Iterable[int] If not specified, indices are assumed as [1, ..., N], but this is only relevant for symbolic representation, not the calculation itself, which only cares about the number of data points, regardless of their actual indices. xsym: str, Optional Symbol of the variable in the symbolic representation of the generated functions. Default is :math:`x`. fsym: str, Optional Symbol of the function in the symbolic representation of the generated functions. Default is 'f'. sym: bool, Optional. If True, locations of the data points are left in a symbolic state. This requires the inversion of a symbolic matrix, which has some reasonable limitations. Default is False. N: int, Optional If neither 'x' nor 'i' is specified, this controls the number of functions to generate. Default is None. lambdify: bool, Optional If True, the functions are turned into `NumPy` functions via `sympy.lambdify` and stored in the output for each index with keyword 'fnc'. Default is False. out: dict, Optional A dictionary to store the values in. Default is None. Returns ------- dict A dictionary containing the generated functions for the reuested nodes. The keys of the dictionary are the indices of the points, the values are dictionaries with the following keys and values: 'symbol' : the `SymPy` symbol of the function 0 : the function 1 : the first derivative as a `SymPy` expression 2 : the second derivative as a `SymPy` expression 3 : the third derivative as a `SymPy` expression Examples -------- >>> from sigmaepsilon.math.approx import gen_Lagrange_1d To generate approximation functions for a 2-noded line: >>> functions = gen_Lagrange_1d(x=[-1, 1]) or equivalently >>> functions = gen_Lagrange_1d(N=2) To generate the same functions in symbolic form: >>> functions = gen_Lagrange_1d(i=[1, 2], sym=True) Notes ----- Inversion of a heavily symbolic matrix may take quite some time, and is not suggested for N > 3. Fixing the locations as constant real numbers symplifies the process and makes the solution much faster. """ xsym = xsym if xsym is not None else r"x" fsym = fsym if fsym is not None else r"\phi" module_data = DeepDict() if not isinstance(out, dict) else out xvar = sy.symbols(xsym) if not isinstance(N, int): assert (x is not None) or (i is not None), "'N', 'x' or 'i' must be provided!" N = len(x) if x is not None else len(i) inds = list(range(1, N + 1)) if i is None else i coeffs = sy.symbols(", ".join(["c_{}".format(i + 1) for i in range(N)])) variables = sy.symbols(", ".join([_var_str(inds, i) for i in range(N)])) if x is None: if not sym: x = np.linspace(-1, 1, N) else: symbols = [xsym + "_{}".format(i + 1) for i in range(N)] x = sy.symbols(", ".join(symbols)) poly = sum([c * xvar**i for i, c in enumerate(coeffs)]) evals = [poly.subs({xsym: x[i]}) for i in range(N)] A = sy.zeros(N, N) for i in range(N): A[i, :] = sy.Matrix([evals[i].coeff(c) for c in coeffs]).T coeffs_new = A.inv() * sy.Matrix(variables) subs = {coeffs[i]: coeffs_new[i] for i in range(N)} approx = poly.subs(subs).simplify().expand() shp = [approx.coeff(v).factor().simplify() for v in variables] dshp1 = [_diff(xvar, fnc) for fnc in shp] dshp2 = [_diff(xvar, fnc) for fnc in dshp1] dshp3 = [_diff(xvar, fnc) for fnc in dshp2] for i, ind in enumerate(inds): fnc_str = latex(sy.symbols(fsym + "_{}".format(ind))) module_data[ind]["symbol"] = fnc_str module_data[ind][0] = shp[i] module_data[ind][1] = dshp1[i] module_data[ind][2] = dshp2[i] module_data[ind][3] = dshp3[i] if lambdify: assert not sym, "If 'lambdify' is True, 'sym' must be False" for ind in inds: for j in range(4): fnc = module_data[ind][j] module_data[ind]["fnc"][j] = sy.lambdify(xvar, fnc, "numpy") if isinstance(module_data, DeepDict): module_data.lock() return module_data
[docs] def approx_Lagrange_1d( points: Iterable, values: Iterable, lambdify: bool = False ) -> Callable: """ Returns a callable that maps from 'source' to 'target' in 1d. Parameters ---------- points: Iterable[float] The locations of the data points. values: Iterable[float] The values at the data points. lambdify: bool, Optional If `True`, the returned function is turned into a `NumPy` function via `sympy.lambdify`, otherwise it is left as a `SymPy` expression. Default is `False`. Returns ------- Callable A vectorized function if 'lambdify' is True, or a SymPy expression otherwise. Example ------- >>> from sigmaepsilon.math.approx import approx_Lagrange_1d >>> approx = approx_Lagrange_1d([-1, 1], [0, 10], lambdify=True) >>> approx(-1), approx(1), approx(0) (0, 10, 5) A symbolic example: >>> import sympy as sy >>> L = sy.symbols('L', real=True, positive=True) >>> fnc = approx_Lagrange_1d([-1, 1], [0, L]) >>> str(fnc) 'L*(x/2 + 1/2)' To get the Jacobian of the transformation [0, L] -> [-1, 1]: >>> L = sy.symbols('L', real=True, positive=True) >>> fnc = approx_Lagrange_1d([0, L], [-1, 1]) >>> dfnc = fnc.diff('x') >>> str(dfnc) '2/L' Or the other way: >>> L = sy.symbols('L', real=True, positive=True) >>> fnc = approx_Lagrange_1d([-1, 1], [0, L]) >>> dfnc = fnc.diff('x') >>> str(dfnc) 'L/2' """ xsym = "x" assert len(points) == len(values), "'source' and 'target' must have the same length" indices = list(range(len(points))) basis = gen_Lagrange_1d(x=points, i=indices, xsym=xsym, lambdify=False) shp0 = [basis[i, 0] for i in indices] fnc0 = sum([a * f for a, f in zip(values, shp0)]) if lambdify: return sy.lambdify(xsym, fnc0, "numpy") else: return fnc0