Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/shapepy/analytic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
* Bezier
"""

from ..tools import To
from .base import IAnalytic
from .bezier import Bezier
from .polynomial import Polynomial
149 changes: 50 additions & 99 deletions src/shapepy/analytic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -54,9 +54,28 @@ def domain(self) -> SubSetR1:
raise NotImplementedError

@abstractmethod
def __call__(self, node: Real, derivate: int = 0) -> Real:
def eval(self, node: Real, derivate: int = 0) -> Real:
"""
Evaluates the given analytic function at given node.

The optional parameter 'derivate' gives if a derivative is required

Example
-------
>>> 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

@vectorize(1, 0)
def __call__(self, node: Real) -> Real:
return self.eval(node, 0)

@abstractmethod
def __add__(self, other: Union[Real, IAnalytic]) -> IAnalytic:
raise NotImplementedError
Expand All @@ -66,99 +85,57 @@ def __mul__(self, other: Union[Real, IAnalytic]) -> IAnalytic:
raise NotImplementedError

@abstractmethod
def shift(self, amount: Real) -> IAnalytic:
def derivate(self, times: int = 1) -> IAnalytic:
"""
Transforms the analytic p(t) into p(t-d) by
translating the analytic by 'd' to the right.
Derivate the polynomial curve, giving a new one

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
>>> 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 scale(self, amount: Real) -> IAnalytic:
def integrate(self, domain: SubSetR1) -> Real:
"""
Transforms the analytic p(t) into p(A*t) by
scaling the argument of the analytic by 'A'.
Derivate the polynomial curve, giving a new one

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
"""
raise NotImplementedError

@abstractmethod
def clean(self) -> IAnalytic:
"""
Cleans the curve, removing the unnecessary coefficients
>>> 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, times: int = 1) -> IAnalytic:
def compose(self, function: IAnalytic) -> IAnalytic:
"""
Integrates the analytic function
"""
raise NotImplementedError
Compose the analytic function: h(x) = f(g(x))

@abstractmethod
def derivate(self, times: int = 1) -> IAnalytic:
"""
Derivates the analytic function
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


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
Expand All @@ -171,9 +148,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)

Expand All @@ -182,26 +156,3 @@ 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:
"""
Computes the derivative of an Analytic function
"""
assert is_analytic(ana)
return ana.derivate()


Is.analytic = is_analytic
140 changes: 15 additions & 125 deletions src/shapepy/analytic/bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
Loading