From 3837988ab07e857fe867035420a0c40f2181c593 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 16 Nov 2025 15:18:06 +0100 Subject: [PATCH 1/3] feat: uncharge the IAnalytic functions to make it simpler --- src/shapepy/analytic/__init__.py | 5 + src/shapepy/analytic/base.py | 135 ++--------- src/shapepy/analytic/bezier.py | 140 ++---------- src/shapepy/analytic/polynomial.py | 212 ++++++----------- src/shapepy/analytic/tools.py | 342 ++++++++++++++++++++-------- src/shapepy/geometry/factory.py | 3 +- src/shapepy/geometry/integral.py | 20 +- src/shapepy/geometry/jordancurve.py | 10 +- src/shapepy/geometry/segment.py | 44 ++-- tests/analytic/test_bezier.py | 30 +-- tests/analytic/test_derivate.py | 36 ++- tests/analytic/test_integrate.py | 35 ++- tests/analytic/test_random.py | 5 +- 13 files changed, 441 insertions(+), 576 deletions(-) diff --git a/src/shapepy/analytic/__init__.py b/src/shapepy/analytic/__init__.py index a333e43f..9c74198c 100644 --- a/src/shapepy/analytic/__init__.py +++ b/src/shapepy/analytic/__init__.py @@ -9,5 +9,10 @@ * Bezier """ +from ..tools import To +from .base import IAnalytic from .bezier import Bezier from .polynomial import Polynomial + +To.polynomial = Polynomial +To.bezier = Bezier diff --git a/src/shapepy/analytic/base.py b/src/shapepy/analytic/base.py index ad98a92d..a4110f73 100644 --- a/src/shapepy/analytic/base.py +++ b/src/shapepy/analytic/base.py @@ -7,11 +7,11 @@ from abc import ABC, abstractmethod from functools import lru_cache -from typing import Iterable, Set, Union +from typing import Set, Union -from ..rbool import SubSetR1, WholeR1, from_any +from ..rbool import SubSetR1 from ..scalar.reals import Real -from ..tools import Is +from ..tools import Is, vectorize @lru_cache(maxsize=None) @@ -54,111 +54,42 @@ def domain(self) -> SubSetR1: raise NotImplementedError @abstractmethod - def __call__(self, node: Real, derivate: int = 0) -> Real: - raise NotImplementedError - - @abstractmethod - def __add__(self, other: Union[Real, IAnalytic]) -> IAnalytic: - raise NotImplementedError - - @abstractmethod - def __mul__(self, other: Union[Real, IAnalytic]) -> IAnalytic: - raise NotImplementedError - - @abstractmethod - def shift(self, amount: Real) -> IAnalytic: + def eval(self, node: Real, derivate: int = 0) -> Real: """ - Transforms the analytic p(t) into p(t-d) by - translating the analytic by 'd' to the right. + Evaluates the given analytic function at given node. - Example - ------- - >>> old_poly = Polynomial([0, 0, 0, 1]) - >>> print(old_poly) - t^3 - >>> new_poly = shift(poly, 1) # transform to (t-1)^3 - >>> print(new_poly) - - 1 + 3 * t - 3 * t^2 + t^3 - """ - raise NotImplementedError - - @abstractmethod - def scale(self, amount: Real) -> IAnalytic: - """ - Transforms the analytic p(t) into p(A*t) by - scaling the argument of the analytic by 'A'. + The optional parameter 'derivate' gives if a derivative is required Example ------- - >>> old_poly = Polynomial([0, 2, 0, 1]) - >>> print(old_poly) - 2 * t + t^3 - >>> new_poly = scale(poly, 2) - >>> print(new_poly) - 4 * t + 8 * t^3 + >>> polynomial = Polynomial([1, 2, 3]) # p(x) = 1 + 2 * x + 3 * x^2 + >>> polynomial.eval(0) # p(0) = 1 + 2 * 0 + 3 * 0^2 + 1 + >>> polynomial.eval(1) # p(1) = 1 + 2 * 1 + 3 * 1^2 + 6 + >>> polynomial.eval(1, 1) # p'(1) = 2 + 6 * 1 + 8 """ raise NotImplementedError - @abstractmethod - def clean(self) -> IAnalytic: - """ - Cleans the curve, removing the unnecessary coefficients - """ - raise NotImplementedError + @vectorize(1, 0) + def __call__(self, node: Real) -> Real: + return self.eval(node, 0) @abstractmethod - def integrate(self, times: int = 1) -> IAnalytic: - """ - Integrates the analytic function - """ + def __add__(self, other: Union[Real, IAnalytic]) -> IAnalytic: raise NotImplementedError @abstractmethod - def derivate(self, times: int = 1) -> IAnalytic: - """ - Derivates the analytic function - """ + def __mul__(self, other: Union[Real, IAnalytic]) -> IAnalytic: raise NotImplementedError - -class BaseAnalytic(IAnalytic): - """ - Base class parent of Analytic classes - """ - - def __init__(self, coefs: Iterable[Real], domain: SubSetR1 = WholeR1()): - if not Is.iterable(coefs): - raise TypeError("Expected an iterable of coefficients") - coefs = tuple(coefs) - if len(coefs) == 0: - raise ValueError("Cannot receive an empty tuple") - self.__coefs = coefs - self.domain = domain - - @property - def domain(self) -> SubSetR1: - return self.__domain - - @domain.setter - def domain(self, subset: SubSetR1): - self.__domain = from_any(subset) - - @property - def ncoefs(self) -> int: - """ - Returns the number of coefficients that determines the analytic - """ - return len(self.__coefs) - - def __iter__(self): - yield from self.__coefs - - def __getitem__(self, index): - return self.__coefs[index] - def __neg__(self) -> IAnalytic: return -1 * self + def __sub__(self, other: Union[Real, IAnalytic]) -> IAnalytic: + return self.__add__(-other) + def __pow__(self, exponent: int) -> IAnalytic: if not Is.integer(exponent) or exponent < 0: raise ValueError @@ -171,9 +102,6 @@ def __pow__(self, exponent: int) -> IAnalytic: cache[n] = cache[n // 2] * cache[n - n // 2] return cache[exponent] - def __sub__(self, other: Union[Real, IAnalytic]) -> IAnalytic: - return self.__add__(-other) - def __rsub__(self, other: Real) -> IAnalytic: return (-self).__add__(other) @@ -183,25 +111,8 @@ def __radd__(self, other: Real) -> IAnalytic: def __rmul__(self, other: Real) -> IAnalytic: return self.__mul__(other) - def __repr__(self) -> str: - if self.domain is WholeR1(): - return str(self) - return f"{self.domain}: {self}" - -def is_analytic(obj: object) -> bool: - """ - Tells if given object is an analytic function - """ - return Is.instance(obj, IAnalytic) - - -def derivate_analytic(ana: IAnalytic) -> IAnalytic: +class BaseAnalytic(IAnalytic): """ - Computes the derivative of an Analytic function + Basic class that implements some functions """ - assert is_analytic(ana) - return ana.derivate() - - -Is.analytic = is_analytic diff --git a/src/shapepy/analytic/bezier.py b/src/shapepy/analytic/bezier.py index 310e2267..2adb9fa6 100644 --- a/src/shapepy/analytic/bezier.py +++ b/src/shapepy/analytic/bezier.py @@ -7,12 +7,10 @@ from functools import lru_cache from typing import Iterable, Tuple, Union -from ..loggers import debug -from ..rbool import SubSetR1, WholeR1 +from ..rbool import SubSetR1, WholeR1, from_any from ..scalar.quadrature import inner from ..scalar.reals import Math, Rational, Real -from ..tools import Is, NotExpectedError, To -from .base import BaseAnalytic, IAnalytic +from ..tools import Is, To from .polynomial import Polynomial @@ -53,141 +51,33 @@ def inverse_caract_matrix(degree: int) -> Tuple[Tuple[Rational, ...], ...]: return tuple(map(tuple, matrix)) -def bezier2polynomial(bezier: Bezier) -> Polynomial: +def bezier2polynomial(coefs: Iterable[Real]) -> Iterable[Real]: """ Converts a Bezier instance to Polynomial """ - coefs = tuple(bezier) + coefs = tuple(coefs) matrix = bezier_caract_matrix(len(coefs) - 1) - poly_coefs = (inner(weights, bezier) for weights in matrix) - return Polynomial(poly_coefs, bezier.domain) + return (inner(weights, coefs) for weights in matrix) -def polynomial2bezier(polynomial: Polynomial) -> Bezier: +def polynomial2bezier(coefs: Iterable[Real]) -> Iterable[Real]: """ Converts a Polynomial instance to a Bezier """ - coefs = tuple(polynomial) + coefs = tuple(coefs) matrix = inverse_caract_matrix(len(coefs) - 1) - ctrlpoints = (inner(weights, coefs) for weights in matrix) - return Bezier(ctrlpoints, polynomial.domain) + return (inner(weights, coefs) for weights in matrix) -class Bezier(BaseAnalytic): +class Bezier(Polynomial): """ Defines the Bezier class, that allows evaluating and operating such as adding, subtracting, multiplying, etc """ - def __init__(self, coefs: Iterable[Real], domain: SubSetR1 = WholeR1()): - super().__init__(coefs, domain) - self.__polynomial = bezier2polynomial(self) - - @property - def degree(self) -> int: - """ - Returns the degree of the polynomial, which is the - highest power of t with a non-zero coefficient. - If the polynomial is constant, returns 0. - """ - return self.__polynomial.degree - - def __eq__(self, value: object) -> bool: - if not Is.instance(value, IAnalytic): - if Is.real(value): - return all(ctrlpoint == value for ctrlpoint in self) - return NotImplemented - if self.domain != value.domain: - return False - if isinstance(value, Bezier): - return self.__polynomial == bezier2polynomial(value) - if isinstance(value, Polynomial): - return self.__polynomial == value - raise NotExpectedError - - def __add__(self, other: Union[Real, Polynomial, Bezier]) -> Bezier: - if Is.instance(other, Bezier): - other = bezier2polynomial(other) - sumpoly = self.__polynomial + other - return polynomial2bezier(sumpoly) - - def __mul__(self, other: Union[Real, Polynomial, Bezier]) -> Bezier: - if Is.instance(other, Bezier): - other = bezier2polynomial(other) - mulpoly = self.__polynomial * other - return polynomial2bezier(mulpoly) - - def __call__(self, node: Real, derivate: int = 0) -> Real: - return self.__polynomial(node, derivate) - - def __str__(self): - return str(self.__polynomial) - - @debug("shapepy.analytic.bezier") - def clean(self) -> Bezier: - """ - Decreases the degree of the bezier curve if possible - """ - return polynomial2bezier(bezier2polynomial(self).clean()) - - @debug("shapepy.analytic.bezier") - def scale(self, amount: Real) -> Bezier: - """ - Transforms the polynomial p(t) into p(A*t) by - scaling the argument of the polynomial by 'A'. - - p(t) = a0 + a1 * t + ... + ap * t^p - p(A * t) = a0 + a1 * (A*t) + ... + ap * (A * t)^p - = b0 + b1 * t + ... + bp * t^p - - Example - ------- - >>> old_poly = Polynomial([0, 0, 0, 1]) - >>> print(old_poly) - t^3 - >>> new_poly = scale(poly, 1) # transform to (t-1)^3 - >>> print(new_poly) - - 1 + 3 * t - 3 * t^2 + t^3 - """ - return polynomial2bezier(bezier2polynomial(self).scale(amount)) - - @debug("shapepy.analytic.bezier") - def shift(self, amount: Real) -> Bezier: - """ - Transforms the bezier p(t) into p(t-d) by - translating the bezier by 'd' to the right. - """ - return polynomial2bezier(bezier2polynomial(self).shift(amount)) - - @debug("shapepy.analytic.bezier") - def integrate(self, times: int = 1) -> Bezier: - """ - Integrates the bezier analytic - - Example - ------- - >>> poly = Polynomial([1, 2, 5]) - >>> print(poly) - 1 + 2 * t + 5 * t^2 - >>> ipoly = integrate(poly) - >>> print(ipoly) - t + t^2 + (5/3) * t^3 - """ - return polynomial2bezier(bezier2polynomial(self).integrate(times)) - - @debug("shapepy.analytic.bezier") - def derivate(self, times: int = 1) -> Bezier: - """ - Derivate the bezier curve, giving a new one - """ - return polynomial2bezier(bezier2polynomial(self).derivate(times)) - - -def to_bezier(coefs: Iterable[Real]) -> Bezier: - """ - Creates a Bezier instance - """ - return Bezier(coefs).clean() - - -To.bezier = to_bezier + def __init__( + self, coefs: Iterable[Real], domain: Union[None, SubSetR1] = None + ): + domain = WholeR1() if domain is None else from_any(domain) + poly_coefs = tuple(bezier2polynomial(coefs)) + super().__init__(poly_coefs, domain) diff --git a/src/shapepy/analytic/polynomial.py b/src/shapepy/analytic/polynomial.py index 261e1f8c..1d6da815 100644 --- a/src/shapepy/analytic/polynomial.py +++ b/src/shapepy/analytic/polynomial.py @@ -7,14 +7,13 @@ from numbers import Real from typing import Iterable, List, Union -from ..loggers import debug -from ..rbool import move, scale +from ..rbool import SubSetR1, WholeR1, from_any from ..scalar.reals import Math -from ..tools import Is, To, vectorize -from .base import BaseAnalytic, IAnalytic +from ..tools import Is, To +from .base import IAnalytic -class Polynomial(BaseAnalytic): +class Polynomial(IAnalytic): """ Defines a polynomial with coefficients @@ -37,6 +36,20 @@ class Polynomial(BaseAnalytic): 5 """ + def __init__(self, coefs: Iterable[Real], domain: SubSetR1 = WholeR1()): + if not Is.iterable(coefs): + raise TypeError("Expected an iterable of coefficients") + coefs = tuple(coefs) + if len(coefs) == 0: + raise ValueError("Cannot receive an empty tuple") + degree = max((i for i, v in enumerate(coefs) if v * v > 0), default=0) + self.__coefs = coefs[: degree + 1] + self.__domain = from_any(domain) + + @property + def domain(self) -> SubSetR1: + return self.__domain + @property def degree(self) -> int: """ @@ -44,21 +57,31 @@ def degree(self) -> int: highest power of t with a non-zero coefficient. If the polynomial is constant, returns 0. """ - return self.ncoefs - 1 + return len(self.__coefs) - 1 + + def __iter__(self): + yield from self.__coefs + + def __getitem__(self, index): + return self.__coefs[index] def __eq__(self, other: object) -> bool: - if Is.instance(other, Polynomial): - return tuple(self.clean()) == tuple(other.clean()) - if Is.real(other): - cself = self.clean() - return cself.degree == 0 and cself[0] == other - return NotImplemented + if not Is.instance(other, IAnalytic): + if Is.finite(other): + return self.degree == 0 and self[0] == other + return NotImplemented + return ( + Is.instance(other, Polynomial) + and other.degree == self.degree + and self.domain == other.domain + and tuple(self) == tuple(other) + ) def __add__(self, other: Union[Real, Polynomial]) -> Polynomial: if not Is.instance(other, IAnalytic): coefs = list(self) coefs[0] += other - return self.__class__(coefs, self.domain) + return Polynomial(coefs, self.domain) if not Is.instance(other, Polynomial): return NotImplemented coefs = [0] * (1 + max(self.degree, other.degree)) @@ -66,36 +89,47 @@ def __add__(self, other: Union[Real, Polynomial]) -> Polynomial: coefs[i] += coef for i, coef in enumerate(other): coefs[i] += coef - return self.__class__(coefs, self.domain) + return Polynomial(coefs, self.domain) def __mul__(self, other: Union[Real, Polynomial]) -> Polynomial: if not Is.instance(other, IAnalytic): - return self.__class__((other * coef for coef in self), self.domain) + return Polynomial((other * coef for coef in self), self.domain) if not Is.instance(other, Polynomial): return NotImplemented coefs = [0 * self[0]] * (self.degree + other.degree + 1) for i, coefi in enumerate(self): for j, coefj in enumerate(other): coefs[i + j] += coefi * coefj - return self.__class__(coefs, self.domain & other.domain) + return Polynomial(coefs, self.domain & other.domain) - @vectorize(1, 0) - def __call__(self, node: Real, derivate: int = 0) -> Real: + def eval(self, node: Real, derivate: int = 0) -> Real: + if node not in self.domain: + raise ValueError(f"Node {node} not in {self.domain}") if derivate == 0: - node = To.real(node) - if self.degree == 0: - return self[0] - if Is.infinity(node): - return self[self.degree] * ( - Math.NEGINF - if self.degree % 2 and node == Math.NEGINF - else Math.POSINF - ) - result: Real = 0 * self[0] - for coef in self[::-1]: - result = node * result + coef - return result - return self.derivate(derivate)(node) + coefs = self.__coefs + elif self.degree < derivate: + coefs = (0 * self.__coefs[0],) + else: + coefs = tuple( + Math.factorial(n + derivate) // Math.factorial(n) * coef + for n, coef in enumerate(self.__coefs[derivate:]) + ) + node = To.real(node) + if len(coefs) == 1: + return coefs[0] + if Is.infinity(node): + return coefs[len(coefs) - 1] * ( + Math.NEGINF + if self.degree % 2 and node == Math.NEGINF + else Math.POSINF + ) + result: Real = 0 * coefs[0] + for coef in coefs[::-1]: + result = node * result + coef + return result + + def __repr__(self): + return str(self.domain) + ": " + self.__str__() def __str__(self): if self.degree == 0: @@ -118,117 +152,3 @@ def __str__(self): msg += f"^{i}" msgs.append(msg) return " ".join(msgs) - - @debug("shapepy.analytic.polynomial") - def clean(self) -> Polynomial: - """ - Decreases the degree of the bezier curve if possible - """ - degree = max((i for i, v in enumerate(self) if v * v > 0), default=0) - return Polynomial(self[: degree + 1], self.domain) - - @debug("shapepy.analytic.polynomial") - def scale(self, amount: Real) -> Polynomial: - """ - Transforms the polynomial p(t) into p(A*t) by - scaling the argument of the polynomial by 'A'. - - p(t) = a0 + a1 * t + ... + ap * t^p - p(A * t) = a0 + a1 * (A*t) + ... + ap * (A * t)^p - = b0 + b1 * t + ... + bp * t^p - - Example - ------- - >>> old_poly = Polynomial([0, 2, 0, 1]) - >>> print(old_poly) - 2 * t + t^3 - >>> new_poly = scale(poly, 2) - >>> print(new_poly) - 4 * t + 8 * t^3 - """ - inv = 1 / amount - coefs = tuple(coef * inv**i for i, coef in enumerate(self)) - return Polynomial(coefs, scale(self.domain, amount)) - - @debug("shapepy.analytic.polynomial") - def shift(self, amount: Real) -> Polynomial: - """ - Transforms the polynomial p(t) into p(t-d) by - translating the polynomial by 'd' to the right. - - p(t) = a0 + a1 * t + ... + ap * t^p - p(t-d) = a0 + a1 * (t-d) + ... + ap * (t-d)^p - = b0 + b1 * t + ... + bp * t^p - - Example - ------- - >>> old_poly = Polynomial([0, 0, 0, 1]) - >>> print(old_poly) - t^3 - >>> new_poly = shift(poly, 1) # transform to (t-1)^3 - >>> print(new_poly) - - 1 + 3 * t - 3 * t^2 + t^3 - """ - newcoefs = list(self) - for i, coef in enumerate(self): - for j in range(i): - value = Math.binom(i, j) * (amount ** (i - j)) - if (i + j) % 2: - value *= -1 - newcoefs[j] += coef * value - return Polynomial(newcoefs, move(self.domain, amount)) - - @debug("shapepy.analytic.polynomial") - def integrate(self, times: int = 1) -> Polynomial: - """ - Integrates the polynomial curve - - Example - ------- - >>> poly = Polynomial([1, 2, 5]) - >>> print(poly) - 1 + 2 * t + 5 * t^2 - >>> ipoly = integrate(poly) - >>> print(ipoly) - t + t^2 + (5/3) * t^3 - """ - polynomial = self - for _ in range(times): - newcoefs = [0 * polynomial[0]] - newcoefs += list( - coef / (n + 1) for n, coef in enumerate(polynomial) - ) - polynomial = Polynomial(newcoefs, self.domain) - return polynomial - - @debug("shapepy.analytic.polynomial") - def derivate(self, times: int = 1) -> Polynomial: - """ - Derivate the polynomial curve, giving a new one - - Example - ------- - >>> poly = Polynomial([1, 2, 5]) - >>> print(poly) - 1 + 2 * t + 5 * t^2 - >>> dpoly = poly.derivate() - >>> print(dpoly) - 2 + 10 * t - """ - if self.degree < times: - return Polynomial([0 * self[0]], self.domain) - coefs = ( - Math.factorial(n + times) // Math.factorial(n) * coef - for n, coef in enumerate(self[times:]) - ) - return Polynomial(coefs, self.domain) - - -def to_polynomial(coeficients: Iterable[Real]) -> Polynomial: - """ - Creates a polynomial instance from given coefficients - """ - return Polynomial(coeficients).clean() - - -To.polynomial = to_polynomial diff --git a/src/shapepy/analytic/tools.py b/src/shapepy/analytic/tools.py index dfcb4cb4..1e9aa5ac 100644 --- a/src/shapepy/analytic/tools.py +++ b/src/shapepy/analytic/tools.py @@ -9,99 +9,23 @@ from ..loggers import debug from ..rbool import ( EmptyR1, + IntervalR1, SingleR1, SubSetR1, WholeR1, extract_knots, from_any, + move, + scale, unite, ) from ..scalar.reals import Math, Real from ..tools import Is, NotExpectedError, To -from .base import IAnalytic, derivate_analytic +from .base import IAnalytic from .bezier import Bezier, bezier2polynomial from .polynomial import Polynomial -def find_polynomial_roots( - polynomial: Polynomial, domain: SubSetR1 = WholeR1() -) -> SubSetR1: - """ - Finds all the values of t* such p(t*) = 0 inside given domain - """ - assert Is.instance(polynomial, Polynomial) - polynomial = polynomial.clean() - domain &= polynomial.domain - if polynomial.degree == 0: - return domain if polynomial[0] == 0 else EmptyR1() - if polynomial.degree == 1: - numerator = -To.rational(1, 1) * polynomial[0] - return SingleR1(numerator / polynomial[1]) - if polynomial.degree == 2: - c, b, a = polynomial - delta = b * b - 4 * a * c - if delta < 0: - return EmptyR1() - sqrtdelta = Math.sqrt(delta) - half = To.rational(1, 2) - x0 = half * (-b - sqrtdelta) / a - x1 = half * (-b + sqrtdelta) / a - return from_any({x0, x1}) - roots = np.roots(tuple(polynomial)[::-1]) - roots = (To.real(np.real(r)) for r in roots if abs(np.imag(r)) < 1e-15) - return from_any(set(roots)) - - -def where_minimum_polynomial( - polynomial: Polynomial, domain: SubSetR1 = WholeR1() -) -> SubSetR1: - """ - Finds the value of t* such poly(t*) is minimal - """ - assert Is.instance(polynomial, Polynomial) - domain &= polynomial.domain - if polynomial.degree == 0: - return domain - if domain == WholeR1() and polynomial.degree % 2: - return EmptyR1() - relation = {knot: polynomial(knot) for knot in extract_knots(domain)} - critical = find_roots(derivate_analytic(polynomial), domain) - for knot in extract_knots(critical): - relation[knot] = polynomial(knot) - minvalue = min(relation.values(), default=float("inf")) - relation = { - key: value - for key, value in relation.items() - if value == minvalue and key in domain - } - return unite(set(relation.keys())) - - -def find_minimum_polynomial( - polynomial: Polynomial, domain: SubSetR1 = WholeR1() -) -> Union[Real, None]: - """ - Finds the minimal value of p(t) in the given domain - - If the minimal does not exist, returns None - - If the polynomial goes to -inf, returns -inf - """ - assert Is.instance(polynomial, Polynomial) - if polynomial.degree == 0: - return polynomial[0] - if domain == WholeR1() and polynomial.degree % 2: - return Math.NEGINF - relation = {} - relation = {knot: polynomial(knot) for knot in extract_knots(domain)} - critical = find_roots(derivate_analytic(polynomial), domain) - for knot in extract_knots(critical): - relation[knot] = polynomial(knot) - return min( - (val for key, val in relation.items() if key in domain), default=None - ) - - @debug("shapepy.analytic.tools") def find_roots(analytic: IAnalytic, domain: SubSetR1 = WholeR1()) -> SubSetR1: """ @@ -110,10 +34,8 @@ def find_roots(analytic: IAnalytic, domain: SubSetR1 = WholeR1()) -> SubSetR1: assert Is.instance(analytic, IAnalytic) domain = from_any(domain) if Is.instance(analytic, Polynomial): - return find_polynomial_roots(analytic, domain) - if Is.instance(analytic, Bezier): - return find_roots(bezier2polynomial(analytic), domain) - raise NotExpectedError + return PolynomialFunctions.find_roots(analytic, domain) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") @debug("shapepy.analytic.tools") @@ -126,10 +48,8 @@ def where_minimum( assert Is.instance(analytic, IAnalytic) domain = from_any(domain) if Is.instance(analytic, Polynomial): - return where_minimum_polynomial(analytic, domain) - if Is.instance(analytic, Bezier): - return where_minimum(bezier2polynomial(analytic), domain) - raise NotExpectedError + return PolynomialFunctions.where_minimum(analytic, domain) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") @debug("shapepy.analytic.tools") @@ -142,7 +62,245 @@ def find_minimum( assert Is.instance(analytic, IAnalytic) domain = from_any(domain) if Is.instance(analytic, Polynomial): - return find_minimum_polynomial(analytic, domain) - if Is.instance(analytic, Bezier): - return find_minimum(bezier2polynomial(analytic), domain) - raise NotExpectedError + return PolynomialFunctions.find_minimum(analytic, domain) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") + + +@debug("shapepy.analytic.tools") +def derivate_analytic(analytic: IAnalytic, times: int = 1) -> IAnalytic: + """ + Derivate the analytic curve, giving a new one[] + """ + if Is.instance(analytic, Polynomial): + return PolynomialFunctions.derivate(analytic, times) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") + + +@debug("shapepy.analytic.tools") +def integrate_analytic(analytic: IAnalytic, domain: SubSetR1) -> IAnalytic: + """ + Derivate the analytic curve, giving a new one[] + """ + if domain not in analytic.domain: + raise ValueError( + f"Given domain {domain} is not subset of {analytic.domain}" + ) + if Is.instance(analytic, Polynomial): + return PolynomialFunctions.integrate(analytic, domain) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") + + +def shift_domain(analytic: IAnalytic, amount: Real) -> IAnalytic: + """ + Transforms the analytic p(t) into p(t-d) by + translating the analytic by 'd' to the right. + + Example + ------- + >>> old_poly = Polynomial([0, 0, 0, 1]) + >>> print(old_poly) + t^3 + >>> new_poly = shift_domain(poly, 1) # transform to (t-1)^3 + >>> print(new_poly) + - 1 + 3 * t - 3 * t^2 + t^3 + """ + amount = To.finite(amount) + if Is.instance(analytic, Polynomial): + return PolynomialFunctions.shift_domain(analytic, amount) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") + + +def scale_domain(analytic: IAnalytic, amount: Real) -> IAnalytic: + """ + Transforms the analytic p(t) into p(A*t) by + scaling the argument of the analytic by 'A'. + + Example + ------- + >>> old_poly = Polynomial([0, 2, 0, 1]) + >>> print(old_poly) + 2 * t + t^3 + >>> new_poly = scale_domain(poly, 2) + >>> print(new_poly) + 4 * t + 8 * t^3 + """ + amount = To.finite(amount) + if Is.instance(analytic, Polynomial): + return PolynomialFunctions.scale_domain(analytic, amount) + raise NotExpectedError(f"Invalid analytic: {type(analytic)}") + + +class PolynomialFunctions: + """Static class that stores static functions used for the generics + functions above. This class specifics for Polynomial""" + + @staticmethod + def find_roots(polynomial: Polynomial, domain: SubSetR1) -> SubSetR1: + """ + Finds all the values of t* such p(t*) = 0 inside given domain + """ + assert Is.instance(polynomial, Polynomial) + domain &= polynomial.domain + if polynomial.degree == 0: + return domain if polynomial[0] == 0 else EmptyR1() + if polynomial.degree == 1: + numerator = -To.rational(1, 1) * polynomial[0] + return SingleR1(numerator / polynomial[1]) + if polynomial.degree == 2: + c, b, a = polynomial + delta = b * b - 4 * a * c + if delta < 0: + return EmptyR1() + sqrtdelta = Math.sqrt(delta) + half = To.rational(1, 2) + x0 = half * (-b - sqrtdelta) / a + x1 = half * (-b + sqrtdelta) / a + return from_any({x0, x1}) + roots = np.roots(tuple(polynomial)[::-1]) + roots = (To.real(np.real(r)) for r in roots if abs(np.imag(r)) < 1e-15) + return from_any(set(roots)) + + @staticmethod + def where_minimum(polynomial: Polynomial, domain: SubSetR1) -> SubSetR1: + """ + Finds the value of t* such poly(t*) is minimal + """ + assert Is.instance(polynomial, Polynomial) + domain &= polynomial.domain + if polynomial.degree == 0: + return domain + if domain == WholeR1() and polynomial.degree % 2: + return EmptyR1() + relation = {knot: polynomial(knot) for knot in extract_knots(domain)} + critical = find_roots(derivate_analytic(polynomial), domain) + for knot in extract_knots(critical): + relation[knot] = polynomial(knot) + minvalue = min(relation.values(), default=float("inf")) + relation = { + key: value + for key, value in relation.items() + if value == minvalue and key in domain + } + return unite(set(relation.keys())) + + @staticmethod + def find_minimum( + polynomial: Polynomial, domain: SubSetR1 + ) -> Union[Real, None]: + """ + Finds the minimal value of p(t) in the given domain + + If the minimal does not exist, returns None + + If the polynomial goes to -inf, returns -inf + """ + assert Is.instance(polynomial, Polynomial) + if polynomial.degree == 0: + return polynomial[0] + if domain == WholeR1() and polynomial.degree % 2: + return Math.NEGINF + relation = {} + relation = {knot: polynomial(knot) for knot in extract_knots(domain)} + critical = find_roots(derivate_analytic(polynomial), domain) + for knot in extract_knots(critical): + relation[knot] = polynomial(knot) + return min( + (val for key, val in relation.items() if key in domain), + default=None, + ) + + @staticmethod + def derivate(polynomial: Polynomial, times: int = 1) -> Polynomial: + """ + Derivate the polynomial curve, giving a new one + + Example + ------- + >>> poly = Polynomial([1, 2, 5]) + >>> print(poly) + 1 + 2 * t + 5 * t^2 + >>> dpoly = derivate(poly) + >>> print(dpoly) + 2 + 10 * t + """ + if polynomial.degree < times: + return Polynomial([0 * polynomial[0]], polynomial.domain) + coefs = ( + Math.factorial(n + times) // Math.factorial(n) * coef + for n, coef in enumerate(polynomial[times:]) + ) + return Polynomial(coefs, polynomial.domain) + + @staticmethod + def integrate(polynomial: Polynomial, domain: SubSetR1) -> Polynomial: + """ + Derivate the polynomial curve, giving a new one + + Example + ------- + >>> poly = Polynomial([1, 2, 5]) + >>> print(poly) + 1 + 2 * t + 5 * t^2 + >>> dpoly = derivate(poly) + >>> print(dpoly) + 2 + 10 * t + """ + domain = from_any(domain) + if domain not in polynomial.domain: + raise ValueError( + f"Given domain {domain} is not subset of {polynomial.domain}" + ) + if not Is.instance(domain, IntervalR1): + raise ValueError(f"Cannot integrate over {domain}") + left, right = domain[0], domain[1] + potencias = [1] + result = 0 + for i, coef in enumerate(polynomial): + result += coef * sum(potencias) / (i + 1) + potencias.append(right * potencias[-1]) + for j in range(i + 1): + potencias[j] *= left + return result * (right - left) + + @staticmethod + def shift_domain(polynomial: Polynomial, amount: Real) -> Polynomial: + """ + Transforms the analytic p(t) into p(t-d) by + translating the analytic by 'd' to the right. + + Example + ------- + >>> old_poly = Polynomial([0, 0, 0, 1]) + >>> print(old_poly) + t^3 + >>> new_poly = shift_domain(poly, 1) # transform to (t-1)^3 + >>> print(new_poly) + - 1 + 3 * t - 3 * t^2 + t^3 + """ + newcoefs = list(polynomial) + for i, coef in enumerate(polynomial): + for j in range(i): + value = Math.binom(i, j) * (amount ** (i - j)) + if (i + j) % 2: + value *= -1 + newcoefs[j] += coef * value + return Polynomial(newcoefs, move(polynomial.domain, amount)) + + @staticmethod + def scale_domain(polynomial: Polynomial, amount: Real) -> Polynomial: + """ + Transforms the analytic p(t) into p(A*t) by + scaling the argument of the analytic by 'A'. + + Example + ------- + >>> old_poly = Polynomial([0, 2, 0, 1]) + >>> print(old_poly) + 2 * t + t^3 + >>> new_poly = scale_domain(poly, 2) + >>> print(new_poly) + 4 * t + 8 * t^3 + """ + inv = 1 / amount + coefs = tuple(coef * inv**i for i, coef in enumerate(polynomial)) + return Polynomial(coefs, scale(polynomial.domain, amount)) diff --git a/src/shapepy/geometry/factory.py b/src/shapepy/geometry/factory.py index e1334e8b..d3e40149 100644 --- a/src/shapepy/geometry/factory.py +++ b/src/shapepy/geometry/factory.py @@ -104,8 +104,7 @@ def spline_curve(spline_curve) -> JordanCurve: """ beziers = spline_curve.split(spline_curve.knots) segments = ( - FactorySegment.bezier(bezier.ctrlpoints).clean() - for bezier in beziers + FactorySegment.bezier(bezier.ctrlpoints) for bezier in beziers ) return JordanCurve(map(USegment, segments)) diff --git a/src/shapepy/geometry/integral.py b/src/shapepy/geometry/integral.py index 20830b25..74fcc60e 100644 --- a/src/shapepy/geometry/integral.py +++ b/src/shapepy/geometry/integral.py @@ -4,11 +4,14 @@ from __future__ import annotations -from copy import copy from functools import partial from ..analytic.base import IAnalytic -from ..analytic.tools import find_minimum +from ..analytic.tools import ( + derivate_analytic, + find_minimum, + integrate_analytic, +) from ..scalar.quadrature import AdaptativeIntegrator, IntegratorFactory from ..scalar.reals import Math from ..tools import Is, To @@ -37,11 +40,11 @@ def polynomial(curve: Segment, expx: int, expy: int): assert Is.instance(curve, Segment) xfunc = curve.xfunc yfunc = curve.yfunc - pcrossdp = xfunc * yfunc.derivate() - yfunc * xfunc.derivate() + pcrossdp = xfunc * derivate_analytic(yfunc) + pcrossdp -= yfunc * derivate_analytic(xfunc) function = (xfunc**expx) * (yfunc**expy) * pcrossdp - assert Is.analytic(function) - ipoly = function.integrate() - return (ipoly(1) - ipoly(0)) / (expx + expy + 2) + assert Is.instance(function, IAnalytic) + return integrate_analytic(function, [0, 1]) / (expx + expy + 2) @staticmethod def turns(curve: Segment, point: Point2D) -> float: @@ -61,9 +64,8 @@ def turns(curve: Segment, point: Point2D) -> float: radius_square = deltax * deltax + deltay * deltay if find_minimum(radius_square, [0, 1]) < 1e-6: return To.rational(1, 2) - crossf = ( - deltax * copy(deltay).derivate() - deltay * copy(deltax).derivate() - ) + crossf = deltax * derivate_analytic(deltay) + crossf -= deltay * derivate_analytic(deltax) function = partial( lambda t, cf, rs: cf(t) / rs(t), cf=crossf, rs=radius_square ) diff --git a/src/shapepy/geometry/jordancurve.py b/src/shapepy/geometry/jordancurve.py index 4c96f4d3..b20066f7 100644 --- a/src/shapepy/geometry/jordancurve.py +++ b/src/shapepy/geometry/jordancurve.py @@ -9,6 +9,8 @@ from copy import copy from typing import Iterable, Iterator +from ..analytic import IAnalytic +from ..analytic.tools import derivate_analytic, integrate_analytic from ..loggers import debug from ..scalar.reals import Real from ..tools import CyclicContainer, Is, pairs, reverse @@ -201,10 +203,10 @@ def compute_area(jordan: JordanCurve) -> Real: segment = usegment.parametrize() xfunc = segment.xfunc yfunc = segment.yfunc - poly = xfunc * yfunc.derivate() - yfunc * xfunc.derivate() - assert Is.analytic(poly) - ipoly = poly.integrate() - total += ipoly(1) - ipoly(0) + poly = xfunc * derivate_analytic(yfunc) + poly -= yfunc * derivate_analytic(xfunc) + assert Is.instance(poly, IAnalytic) + total += integrate_analytic(poly, [0, 1]) return total / 2 diff --git a/src/shapepy/geometry/segment.py b/src/shapepy/geometry/segment.py index 9f852452..c414befb 100644 --- a/src/shapepy/geometry/segment.py +++ b/src/shapepy/geometry/segment.py @@ -16,7 +16,12 @@ from typing import Iterable, Optional, Tuple from ..analytic.base import IAnalytic -from ..analytic.tools import find_minimum +from ..analytic.tools import ( + derivate_analytic, + find_minimum, + scale_domain, + shift_domain, +) from ..loggers import debug from ..rbool import IntervalR1, from_any from ..scalar.quadrature import AdaptativeIntegrator, IntegratorFactory @@ -40,8 +45,8 @@ def __init__(self, xfunc: IAnalytic, yfunc: IAnalytic): raise TypeError self.__length = None self.__knots = (To.rational(0, 1), To.rational(1, 1)) - self.__xfunc = xfunc.clean() - self.__yfunc = yfunc.clean() + self.__xfunc = xfunc + self.__yfunc = yfunc def __str__(self) -> str: return f"BS{list(self.knots)}:({self.xfunc}, {self.yfunc})" @@ -68,8 +73,8 @@ def __contains__(self, point: Point2D) -> bool: @vectorize(1, 0) def __call__(self, node: Real, derivate: int = 0) -> Point2D: - xcoord = self.xfunc(node, derivate) - ycoord = self.yfunc(node, derivate) + xcoord = self.xfunc.eval(node, derivate) + ycoord = self.yfunc.eval(node, derivate) return cartesian(xcoord, ycoord) @property @@ -105,8 +110,8 @@ def derivate(self, times: Optional[int] = 1) -> Segment: """ if not Is.integer(times) or times <= 0: raise ValueError(f"Times must be integer >= 1, not {times}") - dxfunc = copy(self.xfunc).derivate(times) - dyfunc = copy(self.yfunc).derivate(times) + dxfunc = derivate_analytic(self.xfunc, times) + dyfunc = derivate_analytic(self.yfunc, times) return Segment(dxfunc, dyfunc) def box(self) -> Box: @@ -120,12 +125,6 @@ def box(self) -> Box: ymax = -find_minimum(-self.yfunc, [0, 1]) return Box(cartesian(xmin, ymin), cartesian(xmax, ymax)) - def clean(self) -> Segment: - """Cleans the segment""" - self.__xfunc = self.__xfunc.clean() - self.__yfunc = self.__yfunc.clean() - return self - def __copy__(self) -> Segment: return self.__deepcopy__(None) @@ -138,8 +137,12 @@ def __invert__(self) -> Segment: If the curve is clockwise, it becomes counterclockwise """ half = To.rational(1, 2) - xfunc = self.__xfunc.shift(-half).scale(-1).shift(half) - yfunc = self.__yfunc.shift(-half).scale(-1).shift(half) + xfunc = shift_domain( + scale_domain(shift_domain(self.__xfunc, -half), -1), half + ) + yfunc = shift_domain( + scale_domain(shift_domain(self.__yfunc, -half), -1), half + ) return Segment(xfunc, yfunc) def split(self, nodes: Iterable[Real]) -> Tuple[Segment, ...]: @@ -157,8 +160,8 @@ def extract(self, interval: IntervalR1) -> Segment: raise TypeError knota, knotb = interval[0], interval[1] denom = 1 / (knotb - knota) - nxfunc = copy(self.xfunc).shift(-knota).scale(denom) - nyfunc = copy(self.yfunc).shift(-knota).scale(denom) + nxfunc = scale_domain(shift_domain(self.xfunc, -knota), denom) + nyfunc = scale_domain(shift_domain(self.yfunc, -knota), denom) return Segment(nxfunc, nyfunc) @@ -168,10 +171,11 @@ def compute_length(segment: Segment) -> Real: Computes the length of the jordan curve """ domain = (0, 1) - dpsquare: IAnalytic = ( - segment.xfunc.derivate() ** 2 + segment.yfunc.derivate() ** 2 + dpsquare = ( + derivate_analytic(segment.xfunc) ** 2 + + derivate_analytic(segment.yfunc) ** 2 ) - assert Is.analytic(dpsquare) + assert Is.instance(dpsquare, IAnalytic) if dpsquare == dpsquare(0): # Check if it's constant return (domain[1] - domain[0]) * Math.sqrt(dpsquare(0)) integrator = IntegratorFactory.clenshaw_curtis(3) diff --git a/tests/analytic/test_bezier.py b/tests/analytic/test_bezier.py index c82e679b..2a14f6ba 100644 --- a/tests/analytic/test_bezier.py +++ b/tests/analytic/test_bezier.py @@ -36,23 +36,7 @@ def test_degree(): bezier = Bezier([1, 2]) assert bezier.degree == 1 bezier = Bezier([1, 2, 3]) - assert bezier.degree == 2 - - -@pytest.mark.order(4) -@pytest.mark.dependency(depends=["test_build"]) -def test_coefficients(): - bezier = Bezier([0]) - assert bezier[0] == 0 - bezier = Bezier([1]) - assert bezier[0] == 1 - bezier = Bezier([1, 2]) - assert bezier[0] == 1 - assert bezier[1] == 2 - bezier = Bezier([1, 2, 3]) - assert bezier[0] == 1 - assert bezier[1] == 2 - assert bezier[2] == 3 + assert bezier.degree == 1 @pytest.mark.order(4) @@ -194,26 +178,22 @@ def test_conversions(): for _ in range(ntests): degree = np.random.randint(0, 6) ctrlpoints = tuple(np.random.randint(-3, 4, degree + 1)) - bezier = Bezier(ctrlpoints) for _ in range(4): - bezier = bezier2polynomial(bezier) - bezier = polynomial2bezier(bezier) - assert bezier == Bezier(ctrlpoints) + poly_coefs = bezier2polynomial(ctrlpoints) + bezier_coefs = tuple(polynomial2bezier(poly_coefs)) + assert bezier_coefs == ctrlpoints @pytest.mark.order(4) @pytest.mark.dependency(depends=["test_build", "test_matrices"]) def test_clean(): - ctrlpoints = [1, 2, 3, 4] - bezier = Bezier(ctrlpoints) - assert bezier.clean() == Bezier([1, 4]) + assert Bezier([1, 2, 3, 4]) == Bezier([1, 4]) @pytest.mark.order(4) @pytest.mark.dependency( depends=[ "test_build", - "test_coefficients", "test_degree", "test_matrices", "test_compare", diff --git a/tests/analytic/test_derivate.py b/tests/analytic/test_derivate.py index be2a21a6..0a9d92ff 100644 --- a/tests/analytic/test_derivate.py +++ b/tests/analytic/test_derivate.py @@ -1,6 +1,8 @@ import pytest +from shapepy.analytic.bezier import Bezier from shapepy.analytic.polynomial import Polynomial +from shapepy.analytic.tools import derivate_analytic from shapepy.tools import To @@ -20,42 +22,36 @@ def test_begin(): @pytest.mark.dependency(depends=["test_begin"]) def test_polynomial(): poly = To.polynomial([0]) - assert poly.derivate(1) == 0 - assert poly.derivate(2) == 0 + assert derivate_analytic(poly) == 0 poly = To.polynomial([3]) - assert poly.derivate(1) == 0 - assert poly.derivate(2) == 0 + assert derivate_analytic(poly) == 0 poly = To.polynomial([1, 1, 1, 1, 1]) - assert poly.derivate(1) == Polynomial([1, 2, 3, 4]) - assert poly.derivate(2) == Polynomial([2, 6, 12]) - assert poly.derivate(3) == Polynomial([6, 24]) + assert derivate_analytic(poly, 1) == Polynomial([1, 2, 3, 4]) + assert derivate_analytic(poly, 2) == Polynomial([2, 6, 12]) + assert derivate_analytic(poly, 3) == Polynomial([6, 24]) - assert poly(0, 1) == 1 - assert poly(1, 1) == 10 - assert poly(0, 2) == 2 - assert poly(1, 2) == 20 + assert poly.eval(0, 1) == 1 + assert poly.eval(1, 1) == 10 + assert poly.eval(0, 2) == 2 + assert poly.eval(1, 2) == 20 @pytest.mark.order(9) @pytest.mark.dependency(depends=["test_begin"]) def test_bezier(): bezier = To.bezier([0]) - assert bezier.derivate(1) == 0 - assert bezier.derivate(2) == 0 + assert derivate_analytic(bezier) == 0 bezier = To.bezier([3]) - assert bezier.derivate(1) == 0 - assert bezier.derivate(2) == 0 + assert derivate_analytic(bezier) == 0 bezier = To.bezier([1, 1, 1, 1, 1]) - assert bezier.derivate(1) == 0 - assert bezier.derivate(2) == 0 - assert bezier.derivate(3) == 0 + assert derivate_analytic(bezier) == 0 - assert bezier(0, 1) == 0 - assert bezier(1, 1) == 0 + assert bezier.eval(0, 1) == 0 + assert bezier.eval(1, 1) == 0 @pytest.mark.order(9) diff --git a/tests/analytic/test_integrate.py b/tests/analytic/test_integrate.py index 7797e701..c93cd888 100644 --- a/tests/analytic/test_integrate.py +++ b/tests/analytic/test_integrate.py @@ -2,6 +2,7 @@ from shapepy.analytic.bezier import Bezier from shapepy.analytic.polynomial import Polynomial +from shapepy.analytic.tools import integrate_analytic from shapepy.tools import To @@ -21,34 +22,30 @@ def test_begin(): @pytest.mark.dependency(depends=["test_begin"]) def test_polynomial(): poly = Polynomial([0]) - assert poly.integrate(1) == 0 - assert poly.integrate(2) == 0 + assert integrate_analytic(poly, [0, 1]) == 0 poly = Polynomial([3]) - assert poly.integrate(1) == Polynomial([0, 3]) - assert poly.integrate(2) == Polynomial([0, 0, 3 / 2]) + assert integrate_analytic(poly, [0, 1]) == 3 + assert integrate_analytic(poly, [0, 2]) == 6 poly = Polynomial([6, 24, 60]) - assert poly.integrate(1) == Polynomial([0, 6, 12, 20]) - assert poly.integrate(2) == Polynomial([0, 0, 3, 4, 5]) - assert poly.integrate(3) == Polynomial([0, 0, 0, 1, 1, 1]) + assert integrate_analytic(poly, [0, 1]) == 38 + assert integrate_analytic(poly, [0, 2]) == 220 @pytest.mark.order(9) @pytest.mark.dependency(depends=["test_begin"]) def test_bezier(): - bezier = To.bezier([0]) - assert bezier.integrate(1) == 0 - assert bezier.integrate(2) == 0 - - bezier = To.bezier([3]) - assert bezier.integrate(1) == Bezier([0, 3]) - assert bezier.integrate(2) == Bezier([0, 0, 1.5]) - - bezier = To.bezier([6, 6, 6, 6, 6]) - assert bezier.integrate(1) == Bezier([0, 6]) - assert bezier.integrate(2) == Bezier([0, 0, 3]) - assert bezier.integrate(3) == Bezier([0, 0, 0, 1]) + bezier = Bezier([0]) + assert integrate_analytic(bezier, [0, 1]) == 0 + + bezier = Bezier([3]) + assert integrate_analytic(bezier, [0, 0.5]) == 3 / 2 + assert integrate_analytic(bezier, [0.5, 1]) == 3 / 2 + + bezier = Bezier([6, 12, 6]) + assert integrate_analytic(bezier, [0, 0.5]) == 4 + assert integrate_analytic(bezier, [0.5, 1]) == 4 @pytest.mark.order(9) diff --git a/tests/analytic/test_random.py b/tests/analytic/test_random.py index cded3be7..537525e8 100644 --- a/tests/analytic/test_random.py +++ b/tests/analytic/test_random.py @@ -5,6 +5,7 @@ from shapepy.analytic.bezier import Bezier from shapepy.analytic.polynomial import Polynomial +from shapepy.analytic.tools import scale_domain, shift_domain def generator_analytic(quantity: int): @@ -176,7 +177,7 @@ def test_shift(): tsample = np.linspace(-1, 1, 17) for funca in generator_analytic(100): amount = random.randint(-5, 5) - funcb = funca.shift(amount) + funcb = shift_domain(funca, amount) valuesa = funca(tsample) valuesb = funcb(amount + tsample) np.testing.assert_allclose(valuesa, valuesb) @@ -200,7 +201,7 @@ def test_scale(): tsample = np.linspace(-1, 1, 17) for funca in generator_analytic(100): amount = random.randint(2, 5) - funcb = funca.scale(amount) + funcb = scale_domain(funca, amount) valuesa = funca(tsample) valuesb = funcb(amount * tsample) np.testing.assert_allclose(valuesa, valuesb, atol=1e-12, rtol=1) From fdd8dfd86554ab18734202e71dabf24925ddd8f8 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 16 Nov 2025 15:20:49 +0100 Subject: [PATCH 2/3] remove unused class and imports --- src/shapepy/analytic/base.py | 6 ------ src/shapepy/analytic/tools.py | 1 - 2 files changed, 7 deletions(-) diff --git a/src/shapepy/analytic/base.py b/src/shapepy/analytic/base.py index a4110f73..14614852 100644 --- a/src/shapepy/analytic/base.py +++ b/src/shapepy/analytic/base.py @@ -110,9 +110,3 @@ def __radd__(self, other: Real) -> IAnalytic: def __rmul__(self, other: Real) -> IAnalytic: return self.__mul__(other) - - -class BaseAnalytic(IAnalytic): - """ - Basic class that implements some functions - """ diff --git a/src/shapepy/analytic/tools.py b/src/shapepy/analytic/tools.py index 1e9aa5ac..7677226e 100644 --- a/src/shapepy/analytic/tools.py +++ b/src/shapepy/analytic/tools.py @@ -22,7 +22,6 @@ from ..scalar.reals import Math, Real from ..tools import Is, NotExpectedError, To from .base import IAnalytic -from .bezier import Bezier, bezier2polynomial from .polynomial import Polynomial From d4a85ad5c145fe7ecd966fc4139f424d33751b61 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 16 Nov 2025 16:04:04 +0100 Subject: [PATCH 3/3] come back some previous change and set composition --- src/shapepy/analytic/__init__.py | 3 - src/shapepy/analytic/base.py | 46 ++++++++ src/shapepy/analytic/polynomial.py | 51 ++++++++- src/shapepy/analytic/tools.py | 167 +--------------------------- src/shapepy/geometry/integral.py | 16 +-- src/shapepy/geometry/jordancurve.py | 7 +- src/shapepy/geometry/segment.py | 32 ++---- tests/analytic/test_derivate.py | 30 +++-- tests/analytic/test_integrate.py | 22 ++-- tests/analytic/test_random.py | 5 +- 10 files changed, 144 insertions(+), 235 deletions(-) diff --git a/src/shapepy/analytic/__init__.py b/src/shapepy/analytic/__init__.py index 9c74198c..170447d7 100644 --- a/src/shapepy/analytic/__init__.py +++ b/src/shapepy/analytic/__init__.py @@ -13,6 +13,3 @@ from .base import IAnalytic from .bezier import Bezier from .polynomial import Polynomial - -To.polynomial = Polynomial -To.bezier = Bezier diff --git a/src/shapepy/analytic/base.py b/src/shapepy/analytic/base.py index 14614852..0898a58d 100644 --- a/src/shapepy/analytic/base.py +++ b/src/shapepy/analytic/base.py @@ -84,6 +84,52 @@ def __add__(self, other: Union[Real, IAnalytic]) -> IAnalytic: def __mul__(self, other: Union[Real, IAnalytic]) -> IAnalytic: raise NotImplementedError + @abstractmethod + def derivate(self, times: int = 1) -> IAnalytic: + """ + Derivate the polynomial curve, giving a new one + + Example + ------- + >>> poly = Polynomial([1, 2, 5]) + >>> print(poly) + 1 + 2 * t + 5 * t^2 + >>> dpoly = derivate(poly) + >>> print(dpoly) + 2 + 10 * t + """ + raise NotImplementedError + + @abstractmethod + def integrate(self, domain: SubSetR1) -> Real: + """ + Derivate the polynomial curve, giving a new one + + Example + ------- + >>> poly = Polynomial([1, 2, 5]) + >>> print(poly) + 1 + 2 * t + 5 * t^2 + >>> dpoly = derivate(poly) + >>> print(dpoly) + 2 + 10 * t + """ + raise NotImplementedError + + @abstractmethod + def compose(self, function: IAnalytic) -> IAnalytic: + """ + Compose the analytic function: h(x) = f(g(x)) + + Example + ------- + >>> f = Polynomial([0, 0, 1]) # f(x) = x^2 + >>> g = Polynomial([-1, 1]) # f(x) = x - 1 + >>> f.compose(g) # h(x) = (x - 1)^2 + 1 - 2 * x + x^2 + """ + raise NotImplementedError + def __neg__(self) -> IAnalytic: return -1 * self diff --git a/src/shapepy/analytic/polynomial.py b/src/shapepy/analytic/polynomial.py index 1d6da815..c462592d 100644 --- a/src/shapepy/analytic/polynomial.py +++ b/src/shapepy/analytic/polynomial.py @@ -7,7 +7,7 @@ from numbers import Real from typing import Iterable, List, Union -from ..rbool import SubSetR1, WholeR1, from_any +from ..rbool import IntervalR1, SubSetR1, WholeR1, from_any, move, scale from ..scalar.reals import Math from ..tools import Is, To from .base import IAnalytic @@ -128,6 +128,55 @@ def eval(self, node: Real, derivate: int = 0) -> Real: result = node * result + coef return result + def derivate(self, times=1): + if self.degree < times: + return Polynomial([0 * self[0]], self.domain) + coefs = ( + Math.factorial(n + times) // Math.factorial(n) * coef + for n, coef in enumerate(self[times:]) + ) + return Polynomial(coefs, self.domain) + + def integrate(self, domain): + domain = from_any(domain) + if domain not in self.domain: + raise ValueError( + f"Given domain {domain} is not subset of {self.domain}" + ) + if not Is.instance(domain, IntervalR1): + raise ValueError(f"Cannot integrate over {domain}") + left, right = domain[0], domain[1] + potencias = [1] + result = 0 + for i, coef in enumerate(self): + result += coef * sum(potencias) / (i + 1) + potencias.append(right * potencias[-1]) + for j in range(i + 1): + potencias[j] *= left + return result * (right - left) + + def compose(self, function: IAnalytic) -> Polynomial: + if not Is.instance(function, Polynomial): + raise TypeError(f"Analytic {type(function)} is not suported") + if function.degree != 1: + raise ValueError("Only polynomials of degree = 1 are allowed") + shift_amount, scale_amount = tuple(function) + coefs = list(self) + domain = self.domain + if scale_amount != 1: + inv = 1 / scale_amount + coefs = list(coef * inv**i for i, coef in enumerate(self)) + domain = scale(domain, scale_amount) + if shift_amount != 0: + for i, coef in enumerate(tuple(coefs)): + for j in range(i): + value = Math.binom(i, j) * (shift_amount ** (i - j)) + if (i + j) % 2: + value *= -1 + coefs[j] += coef * value + domain = move(domain, shift_amount) + return Polynomial(coefs, domain) + def __repr__(self): return str(self.domain) + ": " + self.__str__() diff --git a/src/shapepy/analytic/tools.py b/src/shapepy/analytic/tools.py index 7677226e..4a658c61 100644 --- a/src/shapepy/analytic/tools.py +++ b/src/shapepy/analytic/tools.py @@ -9,14 +9,11 @@ from ..loggers import debug from ..rbool import ( EmptyR1, - IntervalR1, SingleR1, SubSetR1, WholeR1, extract_knots, from_any, - move, - scale, unite, ) from ..scalar.reals import Math, Real @@ -65,70 +62,6 @@ def find_minimum( raise NotExpectedError(f"Invalid analytic: {type(analytic)}") -@debug("shapepy.analytic.tools") -def derivate_analytic(analytic: IAnalytic, times: int = 1) -> IAnalytic: - """ - Derivate the analytic curve, giving a new one[] - """ - if Is.instance(analytic, Polynomial): - return PolynomialFunctions.derivate(analytic, times) - raise NotExpectedError(f"Invalid analytic: {type(analytic)}") - - -@debug("shapepy.analytic.tools") -def integrate_analytic(analytic: IAnalytic, domain: SubSetR1) -> IAnalytic: - """ - Derivate the analytic curve, giving a new one[] - """ - if domain not in analytic.domain: - raise ValueError( - f"Given domain {domain} is not subset of {analytic.domain}" - ) - if Is.instance(analytic, Polynomial): - return PolynomialFunctions.integrate(analytic, domain) - raise NotExpectedError(f"Invalid analytic: {type(analytic)}") - - -def shift_domain(analytic: IAnalytic, amount: Real) -> IAnalytic: - """ - Transforms the analytic p(t) into p(t-d) by - translating the analytic by 'd' to the right. - - Example - ------- - >>> old_poly = Polynomial([0, 0, 0, 1]) - >>> print(old_poly) - t^3 - >>> new_poly = shift_domain(poly, 1) # transform to (t-1)^3 - >>> print(new_poly) - - 1 + 3 * t - 3 * t^2 + t^3 - """ - amount = To.finite(amount) - if Is.instance(analytic, Polynomial): - return PolynomialFunctions.shift_domain(analytic, amount) - raise NotExpectedError(f"Invalid analytic: {type(analytic)}") - - -def scale_domain(analytic: IAnalytic, amount: Real) -> IAnalytic: - """ - Transforms the analytic p(t) into p(A*t) by - scaling the argument of the analytic by 'A'. - - Example - ------- - >>> old_poly = Polynomial([0, 2, 0, 1]) - >>> print(old_poly) - 2 * t + t^3 - >>> new_poly = scale_domain(poly, 2) - >>> print(new_poly) - 4 * t + 8 * t^3 - """ - amount = To.finite(amount) - if Is.instance(analytic, Polynomial): - return PolynomialFunctions.scale_domain(analytic, amount) - raise NotExpectedError(f"Invalid analytic: {type(analytic)}") - - class PolynomialFunctions: """Static class that stores static functions used for the generics functions above. This class specifics for Polynomial""" @@ -171,7 +104,7 @@ def where_minimum(polynomial: Polynomial, domain: SubSetR1) -> SubSetR1: if domain == WholeR1() and polynomial.degree % 2: return EmptyR1() relation = {knot: polynomial(knot) for knot in extract_knots(domain)} - critical = find_roots(derivate_analytic(polynomial), domain) + critical = find_roots(polynomial.derivate(), domain) for knot in extract_knots(critical): relation[knot] = polynomial(knot) minvalue = min(relation.values(), default=float("inf")) @@ -200,106 +133,10 @@ def find_minimum( return Math.NEGINF relation = {} relation = {knot: polynomial(knot) for knot in extract_knots(domain)} - critical = find_roots(derivate_analytic(polynomial), domain) + critical = find_roots(polynomial.derivate(), domain) for knot in extract_knots(critical): relation[knot] = polynomial(knot) return min( (val for key, val in relation.items() if key in domain), default=None, ) - - @staticmethod - def derivate(polynomial: Polynomial, times: int = 1) -> Polynomial: - """ - Derivate the polynomial curve, giving a new one - - Example - ------- - >>> poly = Polynomial([1, 2, 5]) - >>> print(poly) - 1 + 2 * t + 5 * t^2 - >>> dpoly = derivate(poly) - >>> print(dpoly) - 2 + 10 * t - """ - if polynomial.degree < times: - return Polynomial([0 * polynomial[0]], polynomial.domain) - coefs = ( - Math.factorial(n + times) // Math.factorial(n) * coef - for n, coef in enumerate(polynomial[times:]) - ) - return Polynomial(coefs, polynomial.domain) - - @staticmethod - def integrate(polynomial: Polynomial, domain: SubSetR1) -> Polynomial: - """ - Derivate the polynomial curve, giving a new one - - Example - ------- - >>> poly = Polynomial([1, 2, 5]) - >>> print(poly) - 1 + 2 * t + 5 * t^2 - >>> dpoly = derivate(poly) - >>> print(dpoly) - 2 + 10 * t - """ - domain = from_any(domain) - if domain not in polynomial.domain: - raise ValueError( - f"Given domain {domain} is not subset of {polynomial.domain}" - ) - if not Is.instance(domain, IntervalR1): - raise ValueError(f"Cannot integrate over {domain}") - left, right = domain[0], domain[1] - potencias = [1] - result = 0 - for i, coef in enumerate(polynomial): - result += coef * sum(potencias) / (i + 1) - potencias.append(right * potencias[-1]) - for j in range(i + 1): - potencias[j] *= left - return result * (right - left) - - @staticmethod - def shift_domain(polynomial: Polynomial, amount: Real) -> Polynomial: - """ - Transforms the analytic p(t) into p(t-d) by - translating the analytic by 'd' to the right. - - Example - ------- - >>> old_poly = Polynomial([0, 0, 0, 1]) - >>> print(old_poly) - t^3 - >>> new_poly = shift_domain(poly, 1) # transform to (t-1)^3 - >>> print(new_poly) - - 1 + 3 * t - 3 * t^2 + t^3 - """ - newcoefs = list(polynomial) - for i, coef in enumerate(polynomial): - for j in range(i): - value = Math.binom(i, j) * (amount ** (i - j)) - if (i + j) % 2: - value *= -1 - newcoefs[j] += coef * value - return Polynomial(newcoefs, move(polynomial.domain, amount)) - - @staticmethod - def scale_domain(polynomial: Polynomial, amount: Real) -> Polynomial: - """ - Transforms the analytic p(t) into p(A*t) by - scaling the argument of the analytic by 'A'. - - Example - ------- - >>> old_poly = Polynomial([0, 2, 0, 1]) - >>> print(old_poly) - 2 * t + t^3 - >>> new_poly = scale_domain(poly, 2) - >>> print(new_poly) - 4 * t + 8 * t^3 - """ - inv = 1 / amount - coefs = tuple(coef * inv**i for i, coef in enumerate(polynomial)) - return Polynomial(coefs, scale(polynomial.domain, amount)) diff --git a/src/shapepy/geometry/integral.py b/src/shapepy/geometry/integral.py index 74fcc60e..34817b58 100644 --- a/src/shapepy/geometry/integral.py +++ b/src/shapepy/geometry/integral.py @@ -7,11 +7,7 @@ from functools import partial from ..analytic.base import IAnalytic -from ..analytic.tools import ( - derivate_analytic, - find_minimum, - integrate_analytic, -) +from ..analytic.tools import find_minimum from ..scalar.quadrature import AdaptativeIntegrator, IntegratorFactory from ..scalar.reals import Math from ..tools import Is, To @@ -40,11 +36,11 @@ def polynomial(curve: Segment, expx: int, expy: int): assert Is.instance(curve, Segment) xfunc = curve.xfunc yfunc = curve.yfunc - pcrossdp = xfunc * derivate_analytic(yfunc) - pcrossdp -= yfunc * derivate_analytic(xfunc) + pcrossdp = xfunc * yfunc.derivate() + pcrossdp -= yfunc * xfunc.derivate() function = (xfunc**expx) * (yfunc**expy) * pcrossdp assert Is.instance(function, IAnalytic) - return integrate_analytic(function, [0, 1]) / (expx + expy + 2) + return function.integrate([0, 1]) / (expx + expy + 2) @staticmethod def turns(curve: Segment, point: Point2D) -> float: @@ -64,8 +60,8 @@ def turns(curve: Segment, point: Point2D) -> float: radius_square = deltax * deltax + deltay * deltay if find_minimum(radius_square, [0, 1]) < 1e-6: return To.rational(1, 2) - crossf = deltax * derivate_analytic(deltay) - crossf -= deltay * derivate_analytic(deltax) + crossf = deltax * deltay.derivate() + crossf -= deltay * deltax.derivate() function = partial( lambda t, cf, rs: cf(t) / rs(t), cf=crossf, rs=radius_square ) diff --git a/src/shapepy/geometry/jordancurve.py b/src/shapepy/geometry/jordancurve.py index b20066f7..abcf1e02 100644 --- a/src/shapepy/geometry/jordancurve.py +++ b/src/shapepy/geometry/jordancurve.py @@ -10,7 +10,6 @@ from typing import Iterable, Iterator from ..analytic import IAnalytic -from ..analytic.tools import derivate_analytic, integrate_analytic from ..loggers import debug from ..scalar.reals import Real from ..tools import CyclicContainer, Is, pairs, reverse @@ -203,10 +202,10 @@ def compute_area(jordan: JordanCurve) -> Real: segment = usegment.parametrize() xfunc = segment.xfunc yfunc = segment.yfunc - poly = xfunc * derivate_analytic(yfunc) - poly -= yfunc * derivate_analytic(xfunc) + poly = xfunc * yfunc.derivate() + poly -= yfunc * xfunc.derivate() assert Is.instance(poly, IAnalytic) - total += integrate_analytic(poly, [0, 1]) + total += poly.integrate([0, 1]) return total / 2 diff --git a/src/shapepy/geometry/segment.py b/src/shapepy/geometry/segment.py index c414befb..a5c56fda 100644 --- a/src/shapepy/geometry/segment.py +++ b/src/shapepy/geometry/segment.py @@ -16,12 +16,8 @@ from typing import Iterable, Optional, Tuple from ..analytic.base import IAnalytic -from ..analytic.tools import ( - derivate_analytic, - find_minimum, - scale_domain, - shift_domain, -) +from ..analytic.bezier import Bezier +from ..analytic.tools import find_minimum from ..loggers import debug from ..rbool import IntervalR1, from_any from ..scalar.quadrature import AdaptativeIntegrator, IntegratorFactory @@ -110,8 +106,8 @@ def derivate(self, times: Optional[int] = 1) -> Segment: """ if not Is.integer(times) or times <= 0: raise ValueError(f"Times must be integer >= 1, not {times}") - dxfunc = derivate_analytic(self.xfunc, times) - dyfunc = derivate_analytic(self.yfunc, times) + dxfunc = self.xfunc.derivate(times) + dyfunc = self.yfunc.derivate(times) return Segment(dxfunc, dyfunc) def box(self) -> Box: @@ -136,13 +132,9 @@ def __invert__(self) -> Segment: Inverts the direction of the curve. If the curve is clockwise, it becomes counterclockwise """ - half = To.rational(1, 2) - xfunc = shift_domain( - scale_domain(shift_domain(self.__xfunc, -half), -1), half - ) - yfunc = shift_domain( - scale_domain(shift_domain(self.__yfunc, -half), -1), half - ) + composition = Bezier([1, 0]) + xfunc = self.__xfunc.compose(composition) + yfunc = self.__yfunc.compose(composition) return Segment(xfunc, yfunc) def split(self, nodes: Iterable[Real]) -> Tuple[Segment, ...]: @@ -160,8 +152,9 @@ def extract(self, interval: IntervalR1) -> Segment: raise TypeError knota, knotb = interval[0], interval[1] denom = 1 / (knotb - knota) - nxfunc = scale_domain(shift_domain(self.xfunc, -knota), denom) - nyfunc = scale_domain(shift_domain(self.yfunc, -knota), denom) + composition = Bezier([(-knota) * denom, (1 - knota) * denom]) + nxfunc = self.xfunc.compose(composition) + nyfunc = self.yfunc.compose(composition) return Segment(nxfunc, nyfunc) @@ -171,10 +164,7 @@ def compute_length(segment: Segment) -> Real: Computes the length of the jordan curve """ domain = (0, 1) - dpsquare = ( - derivate_analytic(segment.xfunc) ** 2 - + derivate_analytic(segment.yfunc) ** 2 - ) + dpsquare = segment.xfunc.derivate() ** 2 + segment.yfunc.derivate() ** 2 assert Is.instance(dpsquare, IAnalytic) if dpsquare == dpsquare(0): # Check if it's constant return (domain[1] - domain[0]) * Math.sqrt(dpsquare(0)) diff --git a/tests/analytic/test_derivate.py b/tests/analytic/test_derivate.py index 0a9d92ff..3759867a 100644 --- a/tests/analytic/test_derivate.py +++ b/tests/analytic/test_derivate.py @@ -2,8 +2,6 @@ from shapepy.analytic.bezier import Bezier from shapepy.analytic.polynomial import Polynomial -from shapepy.analytic.tools import derivate_analytic -from shapepy.tools import To @pytest.mark.order(9) @@ -21,16 +19,16 @@ def test_begin(): @pytest.mark.order(9) @pytest.mark.dependency(depends=["test_begin"]) def test_polynomial(): - poly = To.polynomial([0]) - assert derivate_analytic(poly) == 0 + poly = Polynomial([0]) + assert poly.derivate() == 0 - poly = To.polynomial([3]) - assert derivate_analytic(poly) == 0 + poly = Polynomial([3]) + assert poly.derivate() == 0 - poly = To.polynomial([1, 1, 1, 1, 1]) - assert derivate_analytic(poly, 1) == Polynomial([1, 2, 3, 4]) - assert derivate_analytic(poly, 2) == Polynomial([2, 6, 12]) - assert derivate_analytic(poly, 3) == Polynomial([6, 24]) + poly = Polynomial([1, 1, 1, 1, 1]) + assert poly.derivate(1) == Polynomial([1, 2, 3, 4]) + assert poly.derivate(2) == Polynomial([2, 6, 12]) + assert poly.derivate(3) == Polynomial([6, 24]) assert poly.eval(0, 1) == 1 assert poly.eval(1, 1) == 10 @@ -41,14 +39,14 @@ def test_polynomial(): @pytest.mark.order(9) @pytest.mark.dependency(depends=["test_begin"]) def test_bezier(): - bezier = To.bezier([0]) - assert derivate_analytic(bezier) == 0 + bezier = Bezier([0]) + assert bezier.derivate() == 0 - bezier = To.bezier([3]) - assert derivate_analytic(bezier) == 0 + bezier = Bezier([3]) + assert bezier.derivate() == 0 - bezier = To.bezier([1, 1, 1, 1, 1]) - assert derivate_analytic(bezier) == 0 + bezier = Bezier([1, 1, 1, 1, 1]) + assert bezier.derivate() == 0 assert bezier.eval(0, 1) == 0 assert bezier.eval(1, 1) == 0 diff --git a/tests/analytic/test_integrate.py b/tests/analytic/test_integrate.py index c93cd888..16f0b878 100644 --- a/tests/analytic/test_integrate.py +++ b/tests/analytic/test_integrate.py @@ -2,8 +2,6 @@ from shapepy.analytic.bezier import Bezier from shapepy.analytic.polynomial import Polynomial -from shapepy.analytic.tools import integrate_analytic -from shapepy.tools import To @pytest.mark.order(9) @@ -22,30 +20,30 @@ def test_begin(): @pytest.mark.dependency(depends=["test_begin"]) def test_polynomial(): poly = Polynomial([0]) - assert integrate_analytic(poly, [0, 1]) == 0 + assert poly.integrate([0, 1]) == 0 poly = Polynomial([3]) - assert integrate_analytic(poly, [0, 1]) == 3 - assert integrate_analytic(poly, [0, 2]) == 6 + assert poly.integrate([0, 1]) == 3 + assert poly.integrate([0, 2]) == 6 poly = Polynomial([6, 24, 60]) - assert integrate_analytic(poly, [0, 1]) == 38 - assert integrate_analytic(poly, [0, 2]) == 220 + assert poly.integrate([0, 1]) == 38 + assert poly.integrate([0, 2]) == 220 @pytest.mark.order(9) @pytest.mark.dependency(depends=["test_begin"]) def test_bezier(): bezier = Bezier([0]) - assert integrate_analytic(bezier, [0, 1]) == 0 + assert bezier.integrate([0, 1]) == 0 bezier = Bezier([3]) - assert integrate_analytic(bezier, [0, 0.5]) == 3 / 2 - assert integrate_analytic(bezier, [0.5, 1]) == 3 / 2 + assert bezier.integrate([0, 0.5]) == 3 / 2 + assert bezier.integrate([0.5, 1]) == 3 / 2 bezier = Bezier([6, 12, 6]) - assert integrate_analytic(bezier, [0, 0.5]) == 4 - assert integrate_analytic(bezier, [0.5, 1]) == 4 + assert bezier.integrate([0, 0.5]) == 4 + assert bezier.integrate([0.5, 1]) == 4 @pytest.mark.order(9) diff --git a/tests/analytic/test_random.py b/tests/analytic/test_random.py index 537525e8..1318e004 100644 --- a/tests/analytic/test_random.py +++ b/tests/analytic/test_random.py @@ -5,7 +5,6 @@ from shapepy.analytic.bezier import Bezier from shapepy.analytic.polynomial import Polynomial -from shapepy.analytic.tools import scale_domain, shift_domain def generator_analytic(quantity: int): @@ -177,7 +176,7 @@ def test_shift(): tsample = np.linspace(-1, 1, 17) for funca in generator_analytic(100): amount = random.randint(-5, 5) - funcb = shift_domain(funca, amount) + funcb = funca.compose(Polynomial([amount, 1])) valuesa = funca(tsample) valuesb = funcb(amount + tsample) np.testing.assert_allclose(valuesa, valuesb) @@ -201,7 +200,7 @@ def test_scale(): tsample = np.linspace(-1, 1, 17) for funca in generator_analytic(100): amount = random.randint(2, 5) - funcb = scale_domain(funca, amount) + funcb = funca.compose(Polynomial([0, amount])) valuesa = funca(tsample) valuesb = funcb(amount * tsample) np.testing.assert_allclose(valuesa, valuesb, atol=1e-12, rtol=1)