Source code for curves.curves

# -*- coding: utf-8 -*-

# curves
# ------
# functional curve algebra (created by auxilium)
#
# Author:   sonntagsgesicht
# Version:  0.1.4, copyright Friday, 11 October 2024
# Website:  https://github.com/sonntagsgesicht/curves
# License:  Apache License 2.0 (see LICENSE file)


[docs] def init(curve, /, *, id=''): """initialize Curve instance :param curve: item to initialize, i.e. turn into a |Curve| instance if **curve** isn't already one. :return: |Curve| instance >>> from curves import init for functions >>> from math import exp >>> f = init(exp) >>> f exp >>> type(f) <class 'curves.curves.Curve'> >>> f == exp False >>> f is init(f) True for numbers >>> n = init(1.23) >>> n 1.23 >>> type(n) <class 'curves.curves.Curve'> >>> n == 1.23 False >>> n is init(n) True """ if isinstance(curve, Curve): return curve if callable(curve): return Curve(curve) if not isinstance(curve, (float, int, str)): cls = curve.__class__.__qualname__ msg = f"float or callable required but type {cls} given" raise TypeError(msg) return Curve(curve, id=id)
def no_logging(*_): return
[docs] class Curve: _cache = {} logger = no_logging """ logging function to enable logging of elementary instance operations for instances with an **id** attribute >>> from curves import Curve >>> Curve.logger = print >>> c = Curve(99, id='MyCurve') Curve(99, id='MyCurve') >>> _ = c(0) 99 = MyCurve(0) >>> c.curve = 100 MyCurve = 100 >>> _ = float(c) 100.0 = float(MyCurve) >>> c += 4 MyCurve += 4 """ def __init__(self, curve=None, /, *, id='', _op=None, _other=None): """Curve object with algebraic operations :param curve: inner curve or curve value or curve variable :param id: id of curve (optional), if given, **id** must be unique for all curve instances This turns any function (aka curve) into an algebraic object which can handle operators +, -, * , / and @. >>> from curves import Curve >>> eye = Curve() # identity function >>> eye(123.456) 123.456 >>> zero = Curve(0.0) # constant function >>> zero(123.456) 0.0 >>> one = Curve(1.0) >>> one(123.456) 1.0 >>> X = Curve('X') # variable >>> X X >>> p = 2 * X ** 2 + 3 * X + 1 >>> p 2 * X ** 2 + 3 * X + 1 >>> p(123.456) 30854.135872 >>> q = p(X - 1) >>> q (2 * X ** 2 + 3 * X + 1)(X - 1) >>> q1 = p @ (X - 1) >>> q1 (2 * X ** 2 + 3 * X + 1)(X - 1) >>> q2 = 2 * (X - 1) ** 2 + 3 * (X - 1) + 1 >>> q2 2 * (X - 1) ** 2 + 3 * (X - 1) + 1 >>> q(123.456) 30359.311872 >>> q1(123.456) 30359.311872 >>> q2(123.456) 30359.311872 and for constant curves >>> int(Curve(1)) 1 >>> float(Curve(1)) 1.0 >>> int(Curve(1.7)) 1 >>> float(Curve(1.7)) 1.7 Using identifier argument **id** >>> y = Curve(1., id='γ') >>> str(y) 'γ' but >>> repr(y) '1.0' Any identifier must be unique for all curve instances >>> Curve(2., id='γ') Traceback (most recent call last): ... ValueError: Curve(γ) already defined """ if id in self._cache: raise ValueError(f"Curve({id}) already defined") self.curve = curve self.id = id self._inplace_ops = [] self._op = _op self._other = _other if self.id: self._cache[id] = self _ops = f", _op={_op!r}, _other={_other}" if _op else '' self.logger(f"{self.__class__.__name__}({curve}, id={id!r}{_ops})") @staticmethod def _apply(op, other=None, x=None, y=None): other = (lambda _: _) if other is None else other try: match op: case 'abs': return abs(y) case 'neg': return -y case '+': return y + other(x) case '-': return y - other(x) case '*': return y * other(x) case '/': return y / other(x) case '**': return y ** other case '@': return other(y) except TypeError as e: raise TypeError(f"{y} {op} [{other}]({x}) failed for {e}") raise ValueError(f"operation {op} not fount.") @staticmethod def _embrace(s, ops='+-/*'): s = str(s) if not any(f" {_} " in s for _ in ops): return s ignore = '(|abcdefghijklmnopqrstuvwxyz' if (any(s.lower().startswith(_) for _ in ignore) and not s.startswith('X')): return s return f"({s})" def _repr(self, /, *, sep=''): if self.curve is None: s = f"{self.__class__.__name__}()" elif callable(self.curve): s = f"{getattr(self.curve, '__name__', repr(self.curve))!s}" elif isinstance(self.curve, (int, float, str)): s = str(self.curve) else: s = f"{self.__class__.__name__}({self.curve!r})" # for op, other in [(self._op, self._other)] + self._inplace_ops: r = repr for op, other in [(self._op, self._other)]: if op == 'neg': s = f"({sep}{s}{sep})" if '**' in s else self._embrace(s) s = f"-{s}" elif op == 'abs': s = f"abs({sep}{s}{sep})" # if use_repr or sep else f"|{s}|" elif op == '@': other = getattr(other, '__name__', r(other)) s = self._embrace(s) s = f"{s}({sep}{other}{sep})" elif op == '**': other = getattr(other, '__name__', r(other)) other = self._embrace(other) s = self._embrace(s) s = f"{s}{sep} {op} {other}" elif op == '*' or op == '/': other = getattr(other, '__name__', r(other)) other = self._embrace(other, '+-') s = self._embrace(s, '+-') s = f"{s}{sep} {op} {other}" elif op is not None: other = getattr(other, '__name__', r(other)) s = f"{s}{sep} {op} {other}" for op, other in self._inplace_ops: if op not in '+-': s = f"({s})" other = getattr(other, '__name__', r(other)) s = f"{s}{sep} {op} {other}" return s def __call__(self, x): if not isinstance(x, (int, float)): return self @ x if self._op == '@': x = self._other(x) if self.curve is None or isinstance(self.curve, str): y = x elif callable(self.curve): y = self.curve(x) else: y = self.curve if self._op and not self._op == '@': op, other = self._op, self._other y = self._apply(op, other, x, y) for op, other in self._inplace_ops: y = self._apply(op, other, x, y) if self.id: self.logger(f"{y} = {self.id}({x})") return y def __eq__(self, other): return (repr(self) == repr(other) and str(self) == str(other) and type(self) == type(other) # noqa E721 and self.curve == other.curve) def __copy__(self): new = self.__class__(self.curve, _op=self._op, _other=self._other) new._inplace_ops = list(self._inplace_ops) return new def __int__(self): y = int(self.curve) if self._op or self._inplace_ops: raise RuntimeError("Type casting does not work" " with assigned Operations.") if self.id: self.logger(f"{y} = int({self.id})") return y def __float__(self): y = float(self.curve) if self._op or self._inplace_ops: raise RuntimeError("Type casting does not work " "with assigned Operations.") if self.id: self.logger(f"{y} = float({self.id})") return y def __str__(self): return self.id if self.id else repr(self) def __repr__(self): s = self._repr() return s if len(s) < 80 else self._repr(sep='\n') def __abs__(self): return self.__class__(self, _op='abs') def __neg__(self): return self.__class__(self, _op='neg') def __add__(self, other): return self.__class__(self, _op='+', _other=init(other)) def __sub__(self, other): return self.__class__(self, _op='-', _other=init(other)) def __mul__(self, other): return self.__class__(self, _op='*', _other=init(other)) def __truediv__(self, other): return self.__class__(self, _op='/', _other=init(other)) def __pow__(self, power, modulo=None): return self.__class__(self, _op='**', _other=power) def __matmul__(self, other): return self.__class__(self, _op='@', _other=init(other)) def __radd__(self, other): return self.__class__(other).__add__(self) def __rsub__(self, other): return self.__class__(other).__sub__(self) def __rmul__(self, other): return self.__class__(other).__mul__(self) def __rtruediv__(self, other): return self.__class__(other).__truediv__(self) def __rmatmul__(self, other): return self.__class__(other).__matmul__(self) def __iadd__(self, other): if self._inplace_ops: op, oth = self._inplace_ops[-1] if op == '-' and oth == init(other): self._inplace_ops.pop(-1) return self self._inplace_ops.append(('+', init(other))) if self.id: self.logger(f"{self.id} += {other}") return self def __isub__(self, other): if self._inplace_ops: op, oth = self._inplace_ops[-1] if op == '+' and oth == init(other): self._inplace_ops.pop(-1) return self self._inplace_ops.append(('-', init(other))) if self.id: self.logger(f"{self.id} -= {other}") return self def __imul__(self, other): if self._inplace_ops: op, oth = self._inplace_ops[-1] if op == '/' and oth == init(other): self._inplace_ops.pop(-1) return self self._inplace_ops.append(('*', init(other))) if self.id: self.logger(f"{self.id} *= {other}") return self def __itruediv__(self, other): if self._inplace_ops: op, oth = self._inplace_ops[-1] if op == '*' and oth == init(other): self._inplace_ops.pop(-1) return self self._inplace_ops.append(('/', init(other))) if self.id: self.logger(f"{self.id} /= {other}") return self def __ipow__(self, other): self._inplace_ops.append(('**', other)) if self.id: self.logger(f"{self.id} **= {other}") return self def __imatmul__(self, other): self._inplace_ops.append(('@', init(other))) if self.id: self.logger(f"{self.id} @= {other}") return self def __setattr__(self, key, value): if key == 'curve' and getattr(self, 'id', ''): self.logger(f"{self.id} = {value}") super().__setattr__(key, value)