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
36 changes: 33 additions & 3 deletions src/shapepy/analytic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from functools import lru_cache
from typing import Iterable, Set, Union

from ..scalar.reals import Real
from rbool import SubSetR1, Whole, from_any, infimum, supremum

from ..scalar.reals import Math, Real
from ..tools import Is


Expand Down Expand Up @@ -44,6 +46,14 @@ class IAnalytic(ABC):
Interface Class for Analytic classes
"""

@property
@abstractmethod
def domain(self) -> SubSetR1:
"""
Defines the domain in which the Analytic function is defined
"""
raise NotImplementedError

@abstractmethod
def __call__(self, node: Real, derivate: int = 0) -> Real:
raise NotImplementedError
Expand Down Expand Up @@ -121,13 +131,22 @@ class BaseAnalytic(IAnalytic):
Base class parent of Analytic classes
"""

def __init__(self, coefs: Iterable[Real]):
def __init__(self, coefs: Iterable[Real], domain: SubSetR1 = Whole()):
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:
Expand Down Expand Up @@ -170,7 +189,18 @@ def __rmul__(self, other: Real) -> IAnalytic:
return self.__mul__(other)

def __repr__(self) -> str:
return str(self)
if self.domain is Whole():
return str(self)
return f"{self.domain}: {self}"


def is_bounded(subset: SubSetR1) -> bool:
"""
Tells if the given subset on real line is bounded

Meaning, returns false if -inf or +inf are inside subset
"""
return infimum(subset) != Math.NEGINF and supremum(subset) != Math.POSINF


def is_analytic(obj: object) -> bool:
Expand Down
49 changes: 34 additions & 15 deletions src/shapepy/analytic/bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
from functools import lru_cache
from typing import Iterable, Tuple, Union

from rbool import Interval, from_any, infimum, move, scale, supremum

from ..scalar.quadrature import inner
from ..scalar.reals import Math, Rational, Real
from ..tools import Is, To
from .base import BaseAnalytic
from ..tools import Is, NotExpectedError, To
from .base import BaseAnalytic, IAnalytic, is_bounded
from .polynomial import Polynomial


Expand Down Expand Up @@ -56,18 +58,20 @@ def bezier2polynomial(bezier: Bezier) -> Polynomial:
"""
Converts a Bezier instance to Polynomial
"""
matrix = bezier_caract_matrix(bezier.degree)
coefs = tuple(bezier)
matrix = bezier_caract_matrix(len(coefs) - 1)
poly_coefs = (inner(weights, bezier) for weights in matrix)
return To.polynomial(poly_coefs)
return Polynomial(poly_coefs, bezier.domain)


def polynomial2bezier(polynomial: Polynomial) -> Bezier:
"""
Converts a Polynomial instance to a Bezier
"""
matrix = inverse_caract_matrix(polynomial.degree)
ctrlpoints = tuple(inner(weights, polynomial) for weights in matrix)
return Bezier(ctrlpoints)
coefs = tuple(polynomial)
matrix = inverse_caract_matrix(len(coefs) - 1)
ctrlpoints = (inner(weights, coefs) for weights in matrix)
return Bezier(ctrlpoints, polynomial.domain)


class Bezier(BaseAnalytic):
Expand All @@ -76,8 +80,15 @@ class Bezier(BaseAnalytic):
such as adding, subtracting, multiplying, etc
"""

def __init__(self, coefs: Iterable[Real]):
super().__init__(coefs)
def __init__(
self, coefs: Iterable[Real], domain: Union[Interval, None] = None
):
domain = Interval(0, 1) if domain is None else from_any(domain)
if not is_bounded(domain):
raise ValueError(f"Invalid domain {domain} for Bezier")
self.__start = infimum(domain)
self.__end = supremum(domain)
super().__init__(coefs, domain)
self.__polynomial = bezier2polynomial(self)

@property
Expand All @@ -87,16 +98,20 @@ 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 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
if Is.real(value):
return all(ctrlpoint == value for ctrlpoint in self)
return NotImplemented
raise NotExpectedError

def __add__(self, other: Union[Real, Polynomial, Bezier]) -> Bezier:
if Is.instance(other, Bezier):
Expand All @@ -117,6 +132,7 @@ def __matmul__(self, other: Union[Real, Polynomial, Bezier]) -> Bezier:
return polynomial2bezier(matmulpoly)

def __call__(self, node: Real, derivate: int = 0) -> Real:
node = (node - self.__start) / (self.__end - self.__start)
return self.__polynomial(node, derivate)

def __str__(self):
Expand Down Expand Up @@ -146,14 +162,14 @@ def scale(self, amount: Real) -> Bezier:
>>> print(new_poly)
- 1 + 3 * t - 3 * t^2 + t^3
"""
return polynomial2bezier(bezier2polynomial(self).scale(amount))
return Bezier(self, scale(self.domain, amount))

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))
return Bezier(self, move(self.domain, amount))

def integrate(self, times: int = 1) -> Bezier:
"""
Expand Down Expand Up @@ -181,11 +197,14 @@ def split(bezier: Bezier, nodes: Iterable[Real]) -> Iterable[Bezier]:
"""
Splits the bezier curve into segments
"""
assert Is.instance(bezier, Bezier)
nodes = (node for node in nodes if 0 < node < 1)
nodes = tuple([0] + sorted(nodes) + [1])
poly = bezier2polynomial(bezier)
assert poly.domain == bezier.domain
for knota, knotb in zip(nodes, nodes[1:]):
newpoly = copy(poly).shift(-knota).scale(knotb - knota)
newpoly.domain = poly.domain
yield polynomial2bezier(newpoly)


Expand Down
27 changes: 14 additions & 13 deletions src/shapepy/analytic/polynomial.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from numbers import Real
from typing import Iterable, List, Union

from rbool import move, scale

from ..scalar.reals import Math
from ..tools import Is, To, vectorize
from .base import BaseAnalytic
Expand Down Expand Up @@ -56,32 +58,31 @@ def __add__(self, other: Union[Real, Polynomial]) -> Polynomial:
if not Is.instance(other, Polynomial):
coefs = list(self)
coefs[0] += other
return self.__class__(coefs)
return self.__class__(coefs, self.domain)
coefs = [0] * (1 + max(self.degree, other.degree))
for i, coef in enumerate(self):
coefs[i] += coef
for i, coef in enumerate(other):
coefs[i] += coef
return self.__class__(coefs)
return self.__class__(coefs, self.domain)

def __mul__(self, other: Union[Real, Polynomial]) -> Polynomial:
if Is.instance(other, Polynomial):
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
else:
coefs = tuple(other * coef for coef in self)
return self.__class__(coefs)
return self.__class__(coefs, self.domain & other.domain)
return self.__class__((other * coef for coef in self), self.domain)

def __matmul__(self, other: Union[Real, Polynomial]) -> Polynomial:
if not isinstance(other, Polynomial):
return self.__class__((coef @ other for coef in self))
return self.__class__((coef @ other for coef in self), self.domain)
coefs = [0 * (self[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)
return self.__class__(coefs, self.domain & other.domain)

@vectorize(1, 0)
def __call__(self, node: Real, derivate: int = 0) -> Real:
Expand Down Expand Up @@ -138,7 +139,7 @@ def clean(self) -> Polynomial:
degree = max((i for i, v in enumerate(self) if v), default=0)
else:
degree = max((i for i, v in enumerate(self) if v @ v), default=0)
return Polynomial(self[: degree + 1])
return Polynomial(self[: degree + 1], self.domain)

def scale(self, amount: Real) -> Polynomial:
"""
Expand All @@ -159,7 +160,7 @@ def scale(self, amount: Real) -> Polynomial:
4 * t + 8 * t^3
"""
coefs = tuple(coef * amount**i for i, coef in enumerate(self))
return Polynomial(coefs)
return Polynomial(coefs, scale(self.domain, amount))

def shift(self, amount: Real) -> Polynomial:
"""
Expand All @@ -186,7 +187,7 @@ def shift(self, amount: Real) -> Polynomial:
if (i + j) % 2:
value *= -1
newcoefs[j] += coef * value
return Polynomial(newcoefs)
return Polynomial(newcoefs, move(self.domain, amount))

def integrate(self, times: int = 1) -> Polynomial:
"""
Expand All @@ -207,7 +208,7 @@ def integrate(self, times: int = 1) -> Polynomial:
newcoefs += list(
coef / (n + 1) for n, coef in enumerate(polynomial)
)
polynomial = Polynomial(newcoefs)
polynomial = Polynomial(newcoefs, self.domain)
return polynomial

def derivate(self, times: int = 1) -> Polynomial:
Expand All @@ -224,12 +225,12 @@ def derivate(self, times: int = 1) -> Polynomial:
2 + 10 * t
"""
if self.degree < times:
return Polynomial([0 * self[0]])
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)
return Polynomial(coefs, self.domain)


def to_polynomial(coeficients: Iterable[Real]) -> Polynomial:
Expand Down
2 changes: 2 additions & 0 deletions src/shapepy/analytic/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def find_polynomial_roots(
"""
assert Is.instance(polynomial, Polynomial)
polynomial = polynomial.clean()
domain &= polynomial.domain
if polynomial.degree == 0:
return domain if polynomial[0] == 0 else Empty()
if polynomial.degree == 1:
Expand Down Expand Up @@ -57,6 +58,7 @@ def where_minimum_polynomial(
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 == Whole() and polynomial.degree % 2:
Expand Down
Loading
Loading